Merge f40ab574b22ce7619c07367eb28e8ffcd8bd17b3 into bf031fd999c1fc3bd62c3761d27f8ea750dabce4
This commit is contained in:
commit
9ca5d5a98b
@ -263,10 +263,13 @@ const std::string& Epub::getTitle() const {
|
||||
}
|
||||
|
||||
std::string Epub::getCoverBmpPath() const { return cachePath + "/cover.bmp"; }
|
||||
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb.bmp"; }
|
||||
|
||||
bool Epub::generateCoverBmp(bool thumb) const {
|
||||
std::string path = thumb ? getThumbBmpPath() : getCoverBmpPath();
|
||||
|
||||
bool Epub::generateCoverBmp() const {
|
||||
// Already generated, return true
|
||||
if (SD.exists(getCoverBmpPath().c_str())) {
|
||||
if (SD.exists(path.c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -298,11 +301,11 @@ bool Epub::generateCoverBmp() const {
|
||||
}
|
||||
|
||||
File coverBmp;
|
||||
if (!FsHelpers::openFileForWrite("EBP", getCoverBmpPath(), coverBmp)) {
|
||||
if (!FsHelpers::openFileForWrite("EBP", path, coverBmp)) {
|
||||
coverJpg.close();
|
||||
return false;
|
||||
}
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp);
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, thumb ? 1 : 2, thumb ? 90 : 480, thumb ? 120 : 800);
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
SD.remove(coverJpgTempPath.c_str());
|
||||
|
||||
@ -39,8 +39,9 @@ class Epub {
|
||||
const std::string& getCachePath() const;
|
||||
const std::string& getPath() const;
|
||||
const std::string& getTitle() const;
|
||||
std::string getThumbBmpPath() const;
|
||||
std::string getCoverBmpPath() const;
|
||||
bool generateCoverBmp() const;
|
||||
bool generateCoverBmp(bool thumb) const;
|
||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
|
||||
@ -80,6 +80,74 @@ 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 centered, const bool black, const EpdFontStyle style) const {
|
||||
const int lineHeight = getLineHeight(fontId);
|
||||
const int spaceWidth = getSpaceWidth(fontId);
|
||||
int xpos = x;
|
||||
int ypos = y + lineHeight;
|
||||
if (centered) {
|
||||
int textWidth = getTextWidth(fontId, text, style);
|
||||
if (textWidth < w) {
|
||||
// Center if text on single line
|
||||
xpos = x + (w - textWidth) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
int ellipsisWidth = 0;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
const int charWidth = getTextWidth(fontId, reinterpret_cast<const char*>(&cp), style);
|
||||
if (xpos + charWidth + ellipsisWidth > x + w) {
|
||||
if (ellipsisWidth > 0) {
|
||||
// Draw ellipsis and exit
|
||||
int dotX = xpos;
|
||||
renderChar(font, '.', &dotX, &ypos, black, style);
|
||||
dotX += spaceWidth/3;
|
||||
renderChar(font, '.', &dotX, &ypos, black, style);
|
||||
dotX += spaceWidth/3;
|
||||
renderChar(font, '.', &dotX, &ypos, black, style);
|
||||
break;
|
||||
} else {
|
||||
// TODO center when more than one line
|
||||
// if (centered) {
|
||||
// int textWidth = getTextWidth(fontId, text, style);
|
||||
// if (textWidth < w) {
|
||||
// xpos = x + (w - textWidth) / 2;
|
||||
// }
|
||||
// }
|
||||
xpos = x;
|
||||
ypos += lineHeight;
|
||||
if (h > 0 && ypos - y > h) {
|
||||
// Overflowing box height
|
||||
break;
|
||||
}
|
||||
if (h > 0 && ypos + lineHeight - y > h) {
|
||||
// Last line, prepare ellipsis
|
||||
ellipsisWidth = spaceWidth * 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -101,6 +169,12 @@ void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const bool state) con
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawLine(int x1, int y1, int x2, int y2, const int lineWidth, const bool state) const {
|
||||
for (int i = 0; i < lineWidth; i++) {
|
||||
drawLine(x1, y1 + i, x2, y2 + i, state);
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||
drawLine(x, y, x + width - 1, y, state);
|
||||
drawLine(x + width - 1, y, x + width - 1, y + height - 1, state);
|
||||
@ -108,17 +182,132 @@ void GfxRenderer::drawRect(const int x, const int y, const int width, const int
|
||||
drawLine(x, y, x, y + height - 1, state);
|
||||
}
|
||||
|
||||
// Border is inside the rectangle
|
||||
void GfxRenderer::drawRect(const int x, const int y, const int width, const int height, const int lineWidth, const bool state) const {
|
||||
for (int i = 0; i < lineWidth; i++) {
|
||||
drawLine(x + i, y + i, x + width - i, y + i, state);
|
||||
drawLine(x + width - i, y + i, x + width - i, y + height - i, state);
|
||||
drawLine(x + width - i, y + height - i, x + i, y + height - i, state);
|
||||
drawLine(x + i, y + height - i, x + i, y + i, state);
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::drawArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir, const int lineWidth, const bool state) const {
|
||||
const int stroke = std::min(lineWidth, maxRadius);
|
||||
const int innerRadius = std::max(maxRadius - stroke, 0);
|
||||
const int outerRadiusSq = maxRadius * maxRadius;
|
||||
const int innerRadiusSq = innerRadius * innerRadius;
|
||||
for (int dy = 0; dy <= maxRadius; ++dy) {
|
||||
for (int dx = 0; dx <= maxRadius; ++dx) {
|
||||
const int distSq = dx * dx + dy * dy;
|
||||
if (distSq > outerRadiusSq || distSq < innerRadiusSq) {
|
||||
continue;
|
||||
}
|
||||
const int px = cx + xDir * dx;
|
||||
const int py = cy + yDir * dy;
|
||||
drawPixel(px, py, state);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Border is inside the rectangle, rounded corners
|
||||
void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, const int height, const int lineWidth, const int cornerRadius, const bool state) const {
|
||||
if (lineWidth <= 0 || width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const int maxRadius = std::min({cornerRadius, width / 2, height / 2});
|
||||
if (maxRadius <= 0) {
|
||||
drawRect(x, y, width, height, lineWidth, state);
|
||||
return;
|
||||
}
|
||||
|
||||
const int stroke = std::min(lineWidth, maxRadius);
|
||||
const int right = x + width - 1;
|
||||
const int bottom = y + height - 1;
|
||||
|
||||
const int horizontalWidth = width - 2 * maxRadius;
|
||||
if (horizontalWidth > 0) {
|
||||
fillRect(x + maxRadius, y, horizontalWidth, stroke, state);
|
||||
fillRect(x + maxRadius, bottom - stroke + 1, horizontalWidth, stroke, state);
|
||||
}
|
||||
|
||||
const int verticalHeight = height - 2 * maxRadius;
|
||||
if (verticalHeight > 0) {
|
||||
fillRect(x, y + maxRadius, stroke, verticalHeight, state);
|
||||
fillRect(right - stroke + 1, y + maxRadius, stroke, verticalHeight, state);
|
||||
}
|
||||
|
||||
drawArc(maxRadius, x + maxRadius, y + maxRadius, -1, -1, lineWidth, state); // TL
|
||||
drawArc(maxRadius, right - maxRadius, y + maxRadius, 1, -1, lineWidth, state); // TR
|
||||
drawArc(maxRadius, right - maxRadius, bottom - maxRadius, 1, 1, lineWidth, state); // BR
|
||||
drawArc(maxRadius, x + maxRadius, bottom - maxRadius, -1, 1, lineWidth, state); // BL
|
||||
}
|
||||
|
||||
void GfxRenderer::fillRect(const int x, const int y, const int width, const int height, const bool state) const {
|
||||
for (int fillY = y; fillY < y + height; fillY++) {
|
||||
drawLine(x, fillY, x + width - 1, fillY, state);
|
||||
}
|
||||
}
|
||||
|
||||
// Use Bayer matrix 4x4 dithering to fill the rectangle with a grey level - 0 white to 15 black
|
||||
void GfxRenderer::fillRectGrey(const int x, const int y, const int width, const int height, const int greyLevel) const {
|
||||
static constexpr uint8_t bayer4x4[4][4] = {
|
||||
{0, 8, 2, 10},
|
||||
{12, 4, 14, 6},
|
||||
{3, 11, 1, 9},
|
||||
{15, 7, 13, 5},
|
||||
};
|
||||
static constexpr int matrixSize = 4;
|
||||
static constexpr int matrixLevels = matrixSize * matrixSize;
|
||||
|
||||
const int normalizedGrey = (greyLevel * 255) / (matrixLevels - 1);
|
||||
const int clampedGrey = std::max(0, std::min(normalizedGrey, 255));
|
||||
const int threshold = (clampedGrey * (matrixLevels + 1)) / 256;
|
||||
|
||||
for (int dy = 0; dy < height; ++dy) {
|
||||
const int screenY = y + dy;
|
||||
const int matrixY = screenY & (matrixSize - 1);
|
||||
for (int dx = 0; dx < width; ++dx) {
|
||||
const int screenX = x + dx;
|
||||
const int matrixX = screenX & (matrixSize - 1);
|
||||
const uint8_t patternValue = bayer4x4[matrixY][matrixX];
|
||||
const bool black = patternValue < threshold;
|
||||
drawPixel(screenX, screenY, black);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Color -1 white, 0 clear, 1 black
|
||||
void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir, const int insideColor, const int outsideColor) const {
|
||||
const int radiusSq = maxRadius * maxRadius;
|
||||
for (int dy = 0; dy <= maxRadius; ++dy) {
|
||||
for (int dx = 0; dx <= maxRadius; ++dx) {
|
||||
const int distSq = dx * dx + dy * dy;
|
||||
const int px = cx + xDir * dx;
|
||||
const int py = cy + yDir * dy;
|
||||
if (distSq > radiusSq) {
|
||||
if (outsideColor != 0) {
|
||||
drawPixel(px, py, outsideColor == 1);
|
||||
}
|
||||
} else {
|
||||
if (insideColor != 0) {
|
||||
drawPixel(px, py, insideColor == 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||
// Flip X and Y for portrait mode
|
||||
einkDisplay.drawImage(bitmap, y, x, height, width);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
||||
einkDisplay.drawImage(bitmap, y, getScreenWidth() - width - x, height, width);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth,
|
||||
const int maxHeight) const {
|
||||
float scale = 1.0f;
|
||||
|
||||
@ -45,15 +45,23 @@ class GfxRenderer {
|
||||
// Drawing
|
||||
void drawPixel(int x, int y, bool state = true) const;
|
||||
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
|
||||
void drawLine(int x1, int y1, int x2, int y2, int lineWidth, bool state = true) const;
|
||||
void drawRect(int x, int y, int width, int height, bool state = true) const;
|
||||
void drawRect(int x, int y, int width, int height, int lineWidth, bool state) const;
|
||||
void drawArc(int maxRadius, int cx, int cy, int xDir, int yDir, int lineWidth, bool state) const;
|
||||
void drawRoundedRect(int x, int y, int width, int height, int lineWidth, int cornerRadius, bool state) const;
|
||||
void fillRect(int x, int y, int width, int height, bool state = true) const;
|
||||
void fillRectGrey(int x, int y, int width, int height, int greyLevel) const;
|
||||
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir, int insideColor, int outsideColor) const;
|
||||
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||
void drawIcon(const uint8_t bitmap[], int x, int y, int width, int height) const;
|
||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||
|
||||
// Text
|
||||
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 centered, bool black = true, EpdFontStyle style = REGULAR) const;
|
||||
int getSpaceWidth(int fontId) const;
|
||||
int getLineHeight(int fontId) const;
|
||||
|
||||
|
||||
@ -16,7 +16,6 @@ struct JpegReadContext {
|
||||
// ============================================================================
|
||||
// IMAGE PROCESSING OPTIONS - Toggle these to test different configurations
|
||||
// ============================================================================
|
||||
constexpr bool USE_8BIT_OUTPUT = false; // true: 8-bit grayscale (no quantization), false: 2-bit (4 levels)
|
||||
// Dithering method selection (only one should be true, or all false for simple quantization):
|
||||
constexpr bool USE_ATKINSON = true; // Atkinson dithering (cleaner than F-S, less error diffusion)
|
||||
constexpr bool USE_FLOYD_STEINBERG = false; // Floyd-Steinberg error diffusion (can cause "worm" artifacts)
|
||||
@ -74,16 +73,41 @@ static inline int adjustPixel(int gray) {
|
||||
return gray;
|
||||
}
|
||||
|
||||
// Simple quantization without dithering - just divide into 4 levels
|
||||
static inline uint8_t quantizeSimple(int gray) {
|
||||
// Quantize a brightness-adjusted gray value into evenly spaced levels
|
||||
static inline uint8_t quantizeAdjustedSimple(int gray, int levelCount) {
|
||||
if (levelCount <= 1) return 0;
|
||||
if (gray < 0) gray = 0;
|
||||
if (gray > 255) gray = 255;
|
||||
int level = (gray * levelCount) >> 8; // Divide by 256
|
||||
if (level >= levelCount) level = levelCount - 1;
|
||||
return static_cast<uint8_t>(level);
|
||||
}
|
||||
|
||||
// Quantize adjusted gray and also return the reconstructed 0-255 value
|
||||
static inline uint8_t quantizeAdjustedWithValue(int gray, int levelCount, int& quantizedValue) {
|
||||
if (levelCount <= 1) {
|
||||
quantizedValue = 0;
|
||||
return 0;
|
||||
}
|
||||
if (gray < 0) gray = 0;
|
||||
if (gray > 255) gray = 255;
|
||||
int level = (gray * levelCount) >> 8;
|
||||
if (level >= levelCount) level = levelCount - 1;
|
||||
const int denom = levelCount - 1;
|
||||
quantizedValue = denom > 0 ? (level * 255) / denom : 0;
|
||||
return static_cast<uint8_t>(level);
|
||||
}
|
||||
|
||||
// Simple quantization without dithering - divide into 2^bits levels
|
||||
static inline uint8_t quantizeSimple(int gray, int levelCount) {
|
||||
gray = adjustPixel(gray);
|
||||
// Simple 2-bit quantization: 0-63=0, 64-127=1, 128-191=2, 192-255=3
|
||||
return static_cast<uint8_t>(gray >> 6);
|
||||
return quantizeAdjustedSimple(gray, levelCount);
|
||||
}
|
||||
|
||||
// Hash-based noise dithering - survives downsampling without moiré artifacts
|
||||
// Uses integer hash to generate pseudo-random threshold per pixel
|
||||
static inline uint8_t quantizeNoise(int gray, int x, int y) {
|
||||
static inline uint8_t quantizeNoise(int gray, int x, int y, int levelCount) {
|
||||
if (levelCount <= 1) return 0;
|
||||
gray = adjustPixel(gray);
|
||||
|
||||
// Generate noise threshold using integer hash (no regular pattern to alias)
|
||||
@ -91,24 +115,23 @@ static inline uint8_t quantizeNoise(int gray, int x, int y) {
|
||||
hash = (hash ^ (hash >> 13)) * 1274126177u;
|
||||
const int threshold = static_cast<int>(hash >> 24); // 0-255
|
||||
|
||||
// Map gray (0-255) to 4 levels with dithering
|
||||
const int scaled = gray * 3;
|
||||
|
||||
if (scaled < 255) {
|
||||
return (scaled + threshold >= 255) ? 1 : 0;
|
||||
} else if (scaled < 510) {
|
||||
return ((scaled - 255) + threshold >= 255) ? 2 : 1;
|
||||
} else {
|
||||
return ((scaled - 510) + threshold >= 255) ? 3 : 2;
|
||||
// Map gray (0-255) to N levels with dithering
|
||||
const int scaled = gray * levelCount;
|
||||
int level = scaled >> 8;
|
||||
if (level >= levelCount) level = levelCount - 1;
|
||||
const int remainder = scaled & 0xFF;
|
||||
if (level < levelCount - 1 && remainder + threshold >= 256) {
|
||||
level++;
|
||||
}
|
||||
return static_cast<uint8_t>(level);
|
||||
}
|
||||
|
||||
// Main quantization function - selects between methods based on config
|
||||
static inline uint8_t quantize(int gray, int x, int y) {
|
||||
static inline uint8_t quantize(int gray, int x, int y, int levelCount) {
|
||||
if (USE_NOISE_DITHERING) {
|
||||
return quantizeNoise(gray, x, y);
|
||||
return quantizeNoise(gray, x, y, levelCount);
|
||||
} else {
|
||||
return quantizeSimple(gray);
|
||||
return quantizeSimple(gray, levelCount);
|
||||
}
|
||||
}
|
||||
|
||||
@ -120,7 +143,7 @@ static inline uint8_t quantize(int gray, int x, int y) {
|
||||
// Less error buildup = fewer artifacts than Floyd-Steinberg
|
||||
class AtkinsonDitherer {
|
||||
public:
|
||||
AtkinsonDitherer(int width) : width(width) {
|
||||
AtkinsonDitherer(int width, int levelCount) : width(width), levelCount(levelCount) {
|
||||
errorRow0 = new int16_t[width + 4](); // Current row
|
||||
errorRow1 = new int16_t[width + 4](); // Next row
|
||||
errorRow2 = new int16_t[width + 4](); // Row after next
|
||||
@ -142,21 +165,8 @@ class AtkinsonDitherer {
|
||||
if (adjusted > 255) adjusted = 255;
|
||||
|
||||
// Quantize to 4 levels
|
||||
uint8_t quantized;
|
||||
int quantizedValue;
|
||||
if (adjusted < 43) {
|
||||
quantized = 0;
|
||||
quantizedValue = 0;
|
||||
} else if (adjusted < 128) {
|
||||
quantized = 1;
|
||||
quantizedValue = 85;
|
||||
} else if (adjusted < 213) {
|
||||
quantized = 2;
|
||||
quantizedValue = 170;
|
||||
} else {
|
||||
quantized = 3;
|
||||
quantizedValue = 255;
|
||||
}
|
||||
int quantizedValue = 0;
|
||||
uint8_t quantized = quantizeAdjustedWithValue(adjusted, levelCount, quantizedValue);
|
||||
|
||||
// Calculate error (only distribute 6/8 = 75%)
|
||||
int error = (adjusted - quantizedValue) >> 3; // error/8
|
||||
@ -188,6 +198,7 @@ class AtkinsonDitherer {
|
||||
|
||||
private:
|
||||
int width;
|
||||
int levelCount;
|
||||
int16_t* errorRow0;
|
||||
int16_t* errorRow1;
|
||||
int16_t* errorRow2;
|
||||
@ -203,7 +214,7 @@ class AtkinsonDitherer {
|
||||
// 7/16 X
|
||||
class FloydSteinbergDitherer {
|
||||
public:
|
||||
FloydSteinbergDitherer(int width) : width(width), rowCount(0) {
|
||||
FloydSteinbergDitherer(int width, int levelCount) : width(width), levelCount(levelCount), rowCount(0) {
|
||||
errorCurRow = new int16_t[width + 2](); // +2 for boundary handling
|
||||
errorNextRow = new int16_t[width + 2]();
|
||||
}
|
||||
@ -216,6 +227,7 @@ class FloydSteinbergDitherer {
|
||||
// Process a single pixel and return quantized 2-bit value
|
||||
// x is the logical x position (0 to width-1), direction handled internally
|
||||
uint8_t processPixel(int gray, int x, bool reverseDirection) {
|
||||
gray = adjustPixel(gray);
|
||||
// Add accumulated error to this pixel
|
||||
int adjusted = gray + errorCurRow[x + 1];
|
||||
|
||||
@ -223,22 +235,9 @@ class FloydSteinbergDitherer {
|
||||
if (adjusted < 0) adjusted = 0;
|
||||
if (adjusted > 255) adjusted = 255;
|
||||
|
||||
// Quantize to 4 levels (0, 85, 170, 255)
|
||||
uint8_t quantized;
|
||||
int quantizedValue;
|
||||
if (adjusted < 43) {
|
||||
quantized = 0;
|
||||
quantizedValue = 0;
|
||||
} else if (adjusted < 128) {
|
||||
quantized = 1;
|
||||
quantizedValue = 85;
|
||||
} else if (adjusted < 213) {
|
||||
quantized = 2;
|
||||
quantizedValue = 170;
|
||||
} else {
|
||||
quantized = 3;
|
||||
quantizedValue = 255;
|
||||
}
|
||||
// Quantize to the requested level count
|
||||
int quantizedValue = 0;
|
||||
uint8_t quantized = quantizeAdjustedWithValue(adjusted, levelCount, quantizedValue);
|
||||
|
||||
// Calculate error
|
||||
int error = adjusted - quantizedValue;
|
||||
@ -292,6 +291,7 @@ class FloydSteinbergDitherer {
|
||||
|
||||
private:
|
||||
int width;
|
||||
int levelCount;
|
||||
int rowCount;
|
||||
int16_t* errorCurRow;
|
||||
int16_t* errorNextRow;
|
||||
@ -316,12 +316,38 @@ inline void write32Signed(Print& out, const int32_t value) {
|
||||
out.write((value >> 24) & 0xFF);
|
||||
}
|
||||
|
||||
inline void writeIndexedPixel(uint8_t* rowBuffer, int x, int bitsPerPixel, uint8_t value) {
|
||||
const int bitPos = x * bitsPerPixel;
|
||||
const int byteIndex = bitPos >> 3;
|
||||
const int bitOffset = 8 - bitsPerPixel - (bitPos & 7);
|
||||
rowBuffer[byteIndex] |= static_cast<uint8_t>(value << bitOffset);
|
||||
}
|
||||
|
||||
int getBytesPerRow(int width, int bitsPerPixel) {
|
||||
if (bitsPerPixel == 8) {
|
||||
return (width + 3) / 4 * 4; // 8 bits per pixel, padded
|
||||
} else if (bitsPerPixel == 2) {
|
||||
return (width * 2 + 31) / 32 * 4; // 2 bits per pixel, round up
|
||||
}
|
||||
return (width + 31) / 32 * 4; // 1 bit per pixel, round up
|
||||
}
|
||||
|
||||
int getColorsUsed(int bitsPerPixel) {
|
||||
if (bitsPerPixel == 8) {
|
||||
return 256;
|
||||
} else if (bitsPerPixel == 2) {
|
||||
return 4;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Helper function: Write BMP header with 8-bit grayscale (256 levels)
|
||||
void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
|
||||
void writeBmpHeader(Print& bmpOut, const int width, const int height, int bitsPerPixel) {
|
||||
// Calculate row padding (each row must be multiple of 4 bytes)
|
||||
const int bytesPerRow = (width + 3) / 4 * 4; // 8 bits per pixel, padded
|
||||
const int bytesPerRow = getBytesPerRow(width, bitsPerPixel);
|
||||
const int colorsUsed = getColorsUsed(bitsPerPixel);
|
||||
const int paletteSize = colorsUsed * 4; // Size of color palette
|
||||
const int imageSize = bytesPerRow * height;
|
||||
const uint32_t paletteSize = 256 * 4; // 256 colors * 4 bytes (BGRA)
|
||||
const uint32_t fileSize = 14 + 40 + paletteSize + imageSize;
|
||||
|
||||
// BMP File Header (14 bytes)
|
||||
@ -336,60 +362,45 @@ void writeBmpHeader8bit(Print& bmpOut, const int width, const int height) {
|
||||
write32Signed(bmpOut, width);
|
||||
write32Signed(bmpOut, -height); // Negative height = top-down bitmap
|
||||
write16(bmpOut, 1); // Color planes
|
||||
write16(bmpOut, 8); // Bits per pixel (8 bits)
|
||||
write16(bmpOut, bitsPerPixel); // Bits per pixel (8 bits)
|
||||
write32(bmpOut, 0); // BI_RGB (no compression)
|
||||
write32(bmpOut, imageSize);
|
||||
write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI)
|
||||
write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI)
|
||||
write32(bmpOut, 256); // colorsUsed
|
||||
write32(bmpOut, 256); // colorsImportant
|
||||
write32(bmpOut, colorsUsed); // colorsUsed
|
||||
write32(bmpOut, colorsUsed); // colorsImportant
|
||||
|
||||
// Color Palette (256 grayscale entries x 4 bytes = 1024 bytes)
|
||||
for (int i = 0; i < 256; i++) {
|
||||
bmpOut.write(static_cast<uint8_t>(i)); // Blue
|
||||
bmpOut.write(static_cast<uint8_t>(i)); // Green
|
||||
bmpOut.write(static_cast<uint8_t>(i)); // Red
|
||||
bmpOut.write(static_cast<uint8_t>(0)); // Reserved
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function: Write BMP header with 2-bit color depth
|
||||
void JpegToBmpConverter::writeBmpHeader(Print& bmpOut, const int width, const int height) {
|
||||
// Calculate row padding (each row must be multiple of 4 bytes)
|
||||
const int bytesPerRow = (width * 2 + 31) / 32 * 4; // 2 bits per pixel, round up
|
||||
const int imageSize = bytesPerRow * height;
|
||||
const uint32_t fileSize = 70 + imageSize; // 14 (file header) + 40 (DIB header) + 16 (palette) + image
|
||||
|
||||
// BMP File Header (14 bytes)
|
||||
bmpOut.write('B');
|
||||
bmpOut.write('M');
|
||||
write32(bmpOut, fileSize); // File size
|
||||
write32(bmpOut, 0); // Reserved
|
||||
write32(bmpOut, 70); // Offset to pixel data
|
||||
|
||||
// DIB Header (BITMAPINFOHEADER - 40 bytes)
|
||||
write32(bmpOut, 40);
|
||||
write32Signed(bmpOut, width);
|
||||
write32Signed(bmpOut, -height); // Negative height = top-down bitmap
|
||||
write16(bmpOut, 1); // Color planes
|
||||
write16(bmpOut, 2); // Bits per pixel (2 bits)
|
||||
write32(bmpOut, 0); // BI_RGB (no compression)
|
||||
write32(bmpOut, imageSize);
|
||||
write32(bmpOut, 2835); // xPixelsPerMeter (72 DPI)
|
||||
write32(bmpOut, 2835); // yPixelsPerMeter (72 DPI)
|
||||
write32(bmpOut, 4); // colorsUsed
|
||||
write32(bmpOut, 4); // colorsImportant
|
||||
|
||||
// Color Palette (4 colors x 4 bytes = 16 bytes)
|
||||
// Format: Blue, Green, Red, Reserved (BGRA)
|
||||
uint8_t palette[16] = {
|
||||
0x00, 0x00, 0x00, 0x00, // Color 0: Black
|
||||
0x55, 0x55, 0x55, 0x00, // Color 1: Dark gray (85)
|
||||
0xAA, 0xAA, 0xAA, 0x00, // Color 2: Light gray (170)
|
||||
0xFF, 0xFF, 0xFF, 0x00 // Color 3: White
|
||||
};
|
||||
for (const uint8_t i : palette) {
|
||||
bmpOut.write(i);
|
||||
if (bitsPerPixel == 8) {
|
||||
// Color Palette (256 grayscale entries x 4 bytes = 1024 bytes)
|
||||
for (int i = 0; i < 256; i++) {
|
||||
bmpOut.write(static_cast<uint8_t>(i)); // Blue
|
||||
bmpOut.write(static_cast<uint8_t>(i)); // Green
|
||||
bmpOut.write(static_cast<uint8_t>(i)); // Red
|
||||
bmpOut.write(static_cast<uint8_t>(0)); // Reserved
|
||||
}
|
||||
return;
|
||||
} else if (bitsPerPixel == 2) {
|
||||
// Color Palette (4 colors x 4 bytes = 16 bytes)
|
||||
// Format: Blue, Green, Red, Reserved (BGRA)
|
||||
uint8_t palette[16] = {
|
||||
0x00, 0x00, 0x00, 0x00, // Color 0: Black
|
||||
0x55, 0x55, 0x55, 0x00, // Color 1: Dark gray (85)
|
||||
0xAA, 0xAA, 0xAA, 0x00, // Color 2: Light gray (170)
|
||||
0xFF, 0xFF, 0xFF, 0x00 // Color 3: White
|
||||
};
|
||||
for (const uint8_t i : palette) {
|
||||
bmpOut.write(i);
|
||||
}
|
||||
} else {
|
||||
// Color Palette (2 colors x 4 bytes = 8 bytes)
|
||||
// Format: Blue, Green, Red, Reserved (BGRA)
|
||||
uint8_t palette[8] = {
|
||||
0x00, 0x00, 0x00, 0x00, // Color 0: Black
|
||||
0xFF, 0xFF, 0xFF, 0x00 // Color 1: White
|
||||
};
|
||||
for (const uint8_t i : palette) {
|
||||
bmpOut.write(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -425,10 +436,19 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un
|
||||
return 0; // Success
|
||||
}
|
||||
|
||||
// Core function: Convert JPEG file to 2-bit BMP
|
||||
bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
||||
return jpegFileToBmpStream(jpegFile, bmpOut, 2, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT);
|
||||
}
|
||||
|
||||
// Core function: Convert JPEG file to BMP
|
||||
bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut, int bitsPerPixel, int targetWidth, int targetHeight) {
|
||||
Serial.printf("[%lu] [JPG] Converting JPEG to BMP\n", millis());
|
||||
|
||||
if (bitsPerPixel != 1 && bitsPerPixel != 2 && bitsPerPixel != 8) {
|
||||
Serial.printf("[%lu] [JPG] Unsupported bitsPerPixel: %d\n", millis(), bitsPerPixel);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setup context for picojpeg callback
|
||||
JpegReadContext context = {.file = jpegFile, .bufferPos = 0, .bufferFilled = 0};
|
||||
|
||||
@ -462,10 +482,10 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
||||
uint32_t scaleY_fp = 65536;
|
||||
bool needsScaling = false;
|
||||
|
||||
if (USE_PRESCALE && (imageInfo.m_width > TARGET_MAX_WIDTH || imageInfo.m_height > TARGET_MAX_HEIGHT)) {
|
||||
if (USE_PRESCALE && (imageInfo.m_width > targetWidth || imageInfo.m_height > targetHeight)) {
|
||||
// Calculate scale to fit within target dimensions while maintaining aspect ratio
|
||||
const float scaleToFitWidth = static_cast<float>(TARGET_MAX_WIDTH) / imageInfo.m_width;
|
||||
const float scaleToFitHeight = static_cast<float>(TARGET_MAX_HEIGHT) / imageInfo.m_height;
|
||||
const float scaleToFitWidth = static_cast<float>(targetWidth) / imageInfo.m_width;
|
||||
const float scaleToFitHeight = static_cast<float>(targetHeight) / imageInfo.m_height;
|
||||
const float scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight;
|
||||
|
||||
outWidth = static_cast<int>(imageInfo.m_width * scale);
|
||||
@ -482,19 +502,15 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
||||
needsScaling = true;
|
||||
|
||||
Serial.printf("[%lu] [JPG] Pre-scaling %dx%d -> %dx%d (fit to %dx%d)\n", millis(), imageInfo.m_width,
|
||||
imageInfo.m_height, outWidth, outHeight, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT);
|
||||
imageInfo.m_height, outWidth, outHeight, targetWidth, targetHeight);
|
||||
}
|
||||
|
||||
// Write BMP header with output dimensions
|
||||
int bytesPerRow;
|
||||
if (USE_8BIT_OUTPUT) {
|
||||
writeBmpHeader8bit(bmpOut, outWidth, outHeight);
|
||||
bytesPerRow = (outWidth + 3) / 4 * 4;
|
||||
} else {
|
||||
writeBmpHeader(bmpOut, outWidth, outHeight);
|
||||
bytesPerRow = (outWidth * 2 + 31) / 32 * 4;
|
||||
}
|
||||
|
||||
writeBmpHeader(bmpOut, outWidth, outHeight, bitsPerPixel);
|
||||
const int bytesPerRow = getBytesPerRow(outWidth, bitsPerPixel);
|
||||
const int levelCount = 1 << bitsPerPixel;
|
||||
const bool indexedOutput = bitsPerPixel != 8;
|
||||
|
||||
// Allocate row buffer
|
||||
auto* rowBuffer = static_cast<uint8_t*>(malloc(bytesPerRow));
|
||||
if (!rowBuffer) {
|
||||
@ -522,15 +538,15 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create ditherer if enabled (only for 2-bit output)
|
||||
// Create ditherer if enabled (only for indexed output)
|
||||
// Use OUTPUT dimensions for dithering (after prescaling)
|
||||
AtkinsonDitherer* atkinsonDitherer = nullptr;
|
||||
FloydSteinbergDitherer* fsDitherer = nullptr;
|
||||
if (!USE_8BIT_OUTPUT) {
|
||||
if (indexedOutput) {
|
||||
if (USE_ATKINSON) {
|
||||
atkinsonDitherer = new AtkinsonDitherer(outWidth);
|
||||
atkinsonDitherer = new AtkinsonDitherer(outWidth, levelCount);
|
||||
} else if (USE_FLOYD_STEINBERG) {
|
||||
fsDitherer = new FloydSteinbergDitherer(outWidth);
|
||||
fsDitherer = new FloydSteinbergDitherer(outWidth, levelCount);
|
||||
}
|
||||
}
|
||||
|
||||
@ -612,7 +628,7 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
||||
// No scaling - direct output (1:1 mapping)
|
||||
memset(rowBuffer, 0, bytesPerRow);
|
||||
|
||||
if (USE_8BIT_OUTPUT) {
|
||||
if (!indexedOutput) {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
||||
rowBuffer[x] = adjustPixel(gray);
|
||||
@ -620,17 +636,15 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
||||
} else {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
const uint8_t gray = mcuRowBuffer[bufferY * imageInfo.m_width + x];
|
||||
uint8_t twoBit;
|
||||
uint8_t indexedValue;
|
||||
if (atkinsonDitherer) {
|
||||
twoBit = atkinsonDitherer->processPixel(gray, x);
|
||||
indexedValue = atkinsonDitherer->processPixel(gray, x);
|
||||
} else if (fsDitherer) {
|
||||
twoBit = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow());
|
||||
indexedValue = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow());
|
||||
} else {
|
||||
twoBit = quantize(gray, x, y);
|
||||
indexedValue = quantize(gray, x, y, levelCount);
|
||||
}
|
||||
const int byteIndex = (x * 2) / 8;
|
||||
const int bitOffset = 6 - ((x * 2) % 8);
|
||||
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
||||
writeIndexedPixel(rowBuffer, x, bitsPerPixel, indexedValue);
|
||||
}
|
||||
if (atkinsonDitherer)
|
||||
atkinsonDitherer->nextRow();
|
||||
@ -675,7 +689,7 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
||||
if (srcY_fp >= nextOutY_srcStart && currentOutY < outHeight) {
|
||||
memset(rowBuffer, 0, bytesPerRow);
|
||||
|
||||
if (USE_8BIT_OUTPUT) {
|
||||
if (!indexedOutput) {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||
rowBuffer[x] = adjustPixel(gray);
|
||||
@ -683,17 +697,15 @@ bool JpegToBmpConverter::jpegFileToBmpStream(File& jpegFile, Print& bmpOut) {
|
||||
} else {
|
||||
for (int x = 0; x < outWidth; x++) {
|
||||
const uint8_t gray = (rowCount[x] > 0) ? (rowAccum[x] / rowCount[x]) : 0;
|
||||
uint8_t twoBit;
|
||||
uint8_t indexedValue;
|
||||
if (atkinsonDitherer) {
|
||||
twoBit = atkinsonDitherer->processPixel(gray, x);
|
||||
indexedValue = atkinsonDitherer->processPixel(gray, x);
|
||||
} else if (fsDitherer) {
|
||||
twoBit = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow());
|
||||
indexedValue = fsDitherer->processPixel(gray, x, fsDitherer->isReverseRow());
|
||||
} else {
|
||||
twoBit = quantize(gray, x, currentOutY);
|
||||
indexedValue = quantize(gray, x, currentOutY, levelCount);
|
||||
}
|
||||
const int byteIndex = (x * 2) / 8;
|
||||
const int bitOffset = 6 - ((x * 2) % 8);
|
||||
rowBuffer[byteIndex] |= (twoBit << bitOffset);
|
||||
writeIndexedPixel(rowBuffer, x, bitsPerPixel, indexedValue);
|
||||
}
|
||||
if (atkinsonDitherer)
|
||||
atkinsonDitherer->nextRow();
|
||||
|
||||
@ -5,11 +5,11 @@
|
||||
class ZipFile;
|
||||
|
||||
class JpegToBmpConverter {
|
||||
static void writeBmpHeader(Print& bmpOut, int width, int height);
|
||||
// [COMMENTED OUT] static uint8_t grayscaleTo2Bit(uint8_t grayscale, int x, int y);
|
||||
static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size,
|
||||
unsigned char* pBytes_actually_read, void* pCallback_data);
|
||||
|
||||
public:
|
||||
static bool jpegFileToBmpStream(File& jpegFile, Print& bmpOut);
|
||||
static bool jpegFileToBmpStream(File& jpegFile, Print& bmpOut, int bitsPerPixel, int targetWidth, int targetHeight);
|
||||
};
|
||||
|
||||
@ -29,6 +29,9 @@ class CrossPointSettings {
|
||||
uint8_t extraParagraphSpacing = 1;
|
||||
// Duration of the power button press
|
||||
uint8_t shortPwrBtn = 0;
|
||||
// UI Theme
|
||||
enum UI_THEME { LIST = 0, GRID = 1 };
|
||||
uint8_t uiTheme = GRID;
|
||||
|
||||
~CrossPointSettings() = default;
|
||||
|
||||
|
||||
@ -182,7 +182,7 @@ void SleepActivity::renderCoverSleepScreen() const {
|
||||
return renderDefaultSleepScreen();
|
||||
}
|
||||
|
||||
if (!lastEpub.generateCoverBmp()) {
|
||||
if (!lastEpub.generateCoverBmp(false)) {
|
||||
Serial.println("[SLP] Failed to generate cover bmp");
|
||||
return renderDefaultSleepScreen();
|
||||
}
|
||||
|
||||
309
src/activities/home/GridBrowserActivity.cpp
Normal file
309
src/activities/home/GridBrowserActivity.cpp
Normal file
@ -0,0 +1,309 @@
|
||||
#include "GridBrowserActivity.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <SD.h>
|
||||
#include <InputManager.h>
|
||||
#include <Epub.h>
|
||||
|
||||
#include "config.h"
|
||||
#include "../../images/FolderIcon.h"
|
||||
#include "../util/Window.h"
|
||||
|
||||
namespace {
|
||||
constexpr int PAGE_ITEMS = 9;
|
||||
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;
|
||||
constexpr int GRID_OFFSET_LEFT = 37;
|
||||
constexpr int GRID_OFFSET_TOP = 125;
|
||||
} // namespace
|
||||
|
||||
inline int min(const int a, const int b) { return a < b ? a : b; }
|
||||
|
||||
void GridBrowserActivity::sortFileList(std::vector<FileInfo>& 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::displayTaskTrampoline(void* param) {
|
||||
auto* self = static_cast<GridBrowserActivity*>(param);
|
||||
self->displayTaskLoop();
|
||||
}
|
||||
|
||||
void GridBrowserActivity::loadThumbsTaskTrampoline(void* param) {
|
||||
auto* self = static_cast<GridBrowserActivity*>(param);
|
||||
self->loadThumbsTaskLoop();
|
||||
}
|
||||
|
||||
void GridBrowserActivity::loadThumbsTaskLoop() {
|
||||
while (true) {
|
||||
if (thumbsLoadingRequired) {
|
||||
xSemaphoreTake(loadThumbsMutex, portMAX_DELAY);
|
||||
loadThumbs();
|
||||
xSemaphoreGive(loadThumbsMutex);
|
||||
thumbsLoadingRequired = false;
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void GridBrowserActivity::loadThumbs() {
|
||||
int thumbsCount = min(PAGE_ITEMS, files.size() - page * PAGE_ITEMS);
|
||||
for (int i = 0; i < thumbsCount; i++) {
|
||||
const auto file = files[i + page * PAGE_ITEMS];
|
||||
if (file.type == F_EPUB) {
|
||||
if (file.thumbPath.empty()) {
|
||||
Serial.printf("[%lu] Loading thumb for epub: %s\n", millis(), file.name.c_str());
|
||||
std::string thumbPath = loadEpubThumb(basepath + "/" + file.name);
|
||||
if (!thumbPath.empty()) {
|
||||
files[i + page * PAGE_ITEMS].thumbPath = thumbPath;
|
||||
}
|
||||
renderRequired = true;
|
||||
taskYIELD();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string GridBrowserActivity::loadEpubThumb(std::string path) {
|
||||
File file;
|
||||
Epub epubFile(path, "/.crosspoint");
|
||||
if (!epubFile.load()) {
|
||||
Serial.printf("[%lu] Failed to load epub: %s\n", millis(), path.c_str());
|
||||
return "";
|
||||
}
|
||||
if (!epubFile.generateCoverBmp(true)) {
|
||||
Serial.printf("[%lu] Failed to generate epub thumb\n", millis());
|
||||
return "";
|
||||
}
|
||||
std::string thumbPath = epubFile.getThumbBmpPath();
|
||||
Serial.printf("[%lu] epub has thumb at %s\n", millis(), thumbPath.c_str());
|
||||
return thumbPath;
|
||||
}
|
||||
|
||||
void GridBrowserActivity::loadFiles() {
|
||||
files.clear();
|
||||
selectorIndex = 0;
|
||||
previousSelectorIndex = -1;
|
||||
page = 0;
|
||||
auto root = SD.open(basepath.c_str());
|
||||
int count = 0;
|
||||
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_first_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
|
||||
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c){ return std::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();
|
||||
count++;
|
||||
}
|
||||
root.close();
|
||||
GridBrowserActivity::sortFileList(files);
|
||||
}
|
||||
|
||||
void GridBrowserActivity::onEnter() {
|
||||
renderingMutex = xSemaphoreCreateMutex();
|
||||
loadThumbsMutex = xSemaphoreCreateMutex();
|
||||
|
||||
page = 0;
|
||||
loadFiles();
|
||||
onPageChanged();
|
||||
|
||||
xTaskCreate(&GridBrowserActivity::displayTaskTrampoline, "GridFileBrowserTask",
|
||||
8192, // Stack size
|
||||
this, // Parameters
|
||||
2, // Priority
|
||||
&displayTaskHandle // Task handle
|
||||
);
|
||||
xTaskCreate(&GridBrowserActivity::loadThumbsTaskTrampoline, "LoadThumbsTask",
|
||||
8192, // Stack size
|
||||
this, // Parameters
|
||||
1, // Priority
|
||||
&loadThumbsTaskHandle // 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;
|
||||
|
||||
if (loadThumbsTaskHandle) {
|
||||
vTaskDelete(loadThumbsTaskHandle);
|
||||
loadThumbsTaskHandle = nullptr;
|
||||
}
|
||||
vSemaphoreDelete(loadThumbsMutex);
|
||||
loadThumbsMutex = nullptr;
|
||||
|
||||
files.clear();
|
||||
}
|
||||
|
||||
void GridBrowserActivity::onPageChanged() {
|
||||
selectorIndex = 0;
|
||||
previousSelectorIndex = -1;
|
||||
renderRequired = true;
|
||||
thumbsLoadingRequired = true;
|
||||
}
|
||||
|
||||
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;
|
||||
const int selected = selectorIndex + page * PAGE_ITEMS;
|
||||
|
||||
if (inputManager.wasPressed(InputManager::BTN_CONFIRM)) {
|
||||
if (files.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (basepath.back() != '/') {
|
||||
basepath += "/";
|
||||
}
|
||||
if (files[selected].type == F_DIRECTORY) {
|
||||
// open subfolder
|
||||
basepath += files[selected].name;
|
||||
loadFiles();
|
||||
onPageChanged();
|
||||
} else {
|
||||
onSelect(basepath + files[selected].name);
|
||||
}
|
||||
} else if (inputManager.wasPressed(InputManager::BTN_BACK)) {
|
||||
if (basepath != "/") {
|
||||
basepath.resize(basepath.rfind('/'));
|
||||
if (basepath.empty()) basepath = "/";
|
||||
loadFiles();
|
||||
onPageChanged();
|
||||
} else {
|
||||
// At root level, go back home
|
||||
onGoHome();
|
||||
}
|
||||
} else if (prevReleased) {
|
||||
previousSelectorIndex = selectorIndex;
|
||||
if (selectorIndex == 0 || skipPage) {
|
||||
if (page > 0) {
|
||||
page--;
|
||||
onPageChanged();
|
||||
}
|
||||
} else {
|
||||
selectorIndex--;
|
||||
updateRequired = true;
|
||||
}
|
||||
} else if (nextReleased) {
|
||||
previousSelectorIndex = selectorIndex;
|
||||
if (selectorIndex == min(PAGE_ITEMS, files.size() - page * PAGE_ITEMS) - 1 || skipPage) {
|
||||
if (page < files.size() / PAGE_ITEMS) {
|
||||
page++;
|
||||
onPageChanged();
|
||||
}
|
||||
} else {
|
||||
selectorIndex++;
|
||||
updateRequired = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GridBrowserActivity::displayTaskLoop() {
|
||||
while (true) {
|
||||
if (renderRequired || updateRequired) {
|
||||
bool didRequireRender = renderRequired;
|
||||
renderRequired = false;
|
||||
updateRequired = false;
|
||||
xSemaphoreTake(renderingMutex, portMAX_DELAY);
|
||||
render(didRequireRender);
|
||||
xSemaphoreGive(renderingMutex);
|
||||
}
|
||||
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||
}
|
||||
}
|
||||
|
||||
void GridBrowserActivity::render(bool clear) const {
|
||||
if (clear) {
|
||||
renderer.clearScreen();
|
||||
auto folderName = basepath == "/" ? "SD card" : basepath.substr(basepath.rfind('/') + 1).c_str();
|
||||
drawFullscreenWindowFrame(renderer, folderName);
|
||||
}
|
||||
|
||||
if (!files.empty()) {
|
||||
for (size_t i = 0; i < min(PAGE_ITEMS, files.size() - page * PAGE_ITEMS); i++) {
|
||||
const auto file = files[i + page * PAGE_ITEMS];
|
||||
|
||||
const int16_t tileX = GRID_OFFSET_LEFT + i % 3 * TILE_W;
|
||||
const int16_t tileY = GRID_OFFSET_TOP + i / 3 * TILE_H;
|
||||
|
||||
if (file.type == F_DIRECTORY) {
|
||||
constexpr int iconOffsetX = (TILE_W - FOLDERICON_WIDTH) / 2;
|
||||
constexpr int iconOffsetY = (TILE_H - TILE_TEXT_H - FOLDERICON_HEIGHT) / 2;
|
||||
renderer.drawIcon(FolderIcon, tileX + iconOffsetX, tileY + iconOffsetY, FOLDERICON_WIDTH, FOLDERICON_HEIGHT);
|
||||
}
|
||||
|
||||
if (!file.thumbPath.empty()) {
|
||||
Serial.printf("Rendering file thumb: %s\n", file.thumbPath.c_str());
|
||||
File bmpFile = SD.open(file.thumbPath.c_str());
|
||||
if (bmpFile) {
|
||||
Bitmap bitmap(bmpFile);
|
||||
if (bitmap.parseHeaders() == BmpReaderError::Ok) {
|
||||
constexpr int thumbOffsetX = (TILE_W - THUMB_W) / 2;
|
||||
constexpr int thumbOffsetY = (TILE_H - TILE_TEXT_H - THUMB_H) / 2;
|
||||
renderer.drawBitmap(bitmap, tileX + thumbOffsetX, tileY + thumbOffsetY, THUMB_W, THUMB_H);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(), true);
|
||||
}
|
||||
|
||||
update(false);
|
||||
renderer.displayBuffer();
|
||||
}
|
||||
}
|
||||
|
||||
void GridBrowserActivity::drawSelectionRectangle(int tileIndex, bool black) const {
|
||||
renderer.drawRoundedRect(GRID_OFFSET_LEFT + tileIndex % 3 * TILE_W, GRID_OFFSET_TOP + tileIndex / 3 * TILE_H, TILE_W, TILE_H, 2, 5, black);
|
||||
}
|
||||
|
||||
void GridBrowserActivity::update(bool render) const {
|
||||
// Redraw only changed tiles
|
||||
// renderer.clearScreen();
|
||||
if (previousSelectorIndex >= 0) {
|
||||
drawSelectionRectangle(previousSelectorIndex, false);
|
||||
}
|
||||
drawSelectionRectangle(selectorIndex, true);
|
||||
}
|
||||
69
src/activities/home/GridBrowserActivity.h
Normal file
69
src/activities/home/GridBrowserActivity.h
Normal file
@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
#include <freertos/task.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#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;
|
||||
std::string thumbPath;
|
||||
};
|
||||
|
||||
class GridBrowserActivity final : public Activity {
|
||||
TaskHandle_t displayTaskHandle = nullptr;
|
||||
SemaphoreHandle_t renderingMutex = nullptr;
|
||||
TaskHandle_t loadThumbsTaskHandle = nullptr;
|
||||
SemaphoreHandle_t loadThumbsMutex = nullptr;
|
||||
std::string basepath = "/";
|
||||
std::vector<FileInfo> files;
|
||||
int selectorIndex = 0;
|
||||
int previousSelectorIndex = -1;
|
||||
int page = 0;
|
||||
bool updateRequired = false;
|
||||
bool renderRequired = false;
|
||||
bool thumbsLoadingRequired = false;
|
||||
const std::function<void(const std::string&)> onSelect;
|
||||
const std::function<void()> onGoHome;
|
||||
|
||||
static void displayTaskTrampoline(void* param);
|
||||
[[noreturn]] void displayTaskLoop();
|
||||
static void loadThumbsTaskTrampoline(void* param);
|
||||
void render(bool clear) const;
|
||||
void update(bool render) const;
|
||||
void loadFiles();
|
||||
void drawSelectionRectangle(int tileIndex, bool black) const;
|
||||
std::string loadEpubThumb(std::string path);
|
||||
void loadThumbsTaskLoop();
|
||||
void loadThumbs();
|
||||
void onPageChanged();
|
||||
|
||||
public:
|
||||
explicit GridBrowserActivity(GfxRenderer& renderer, InputManager& inputManager,
|
||||
const std::function<void(const std::string&)>& onSelect,
|
||||
const std::function<void()>& onGoHome,
|
||||
std::string initialPath = "/")
|
||||
: Activity("FileSelection", renderer, inputManager),
|
||||
onSelect(onSelect),
|
||||
onGoHome(onGoHome),
|
||||
basepath(initialPath.empty() ? "/" : std::move(initialPath)) {}
|
||||
void onEnter() override;
|
||||
void onExit() override;
|
||||
void loop() override;
|
||||
private:
|
||||
static void sortFileList(std::vector<FileInfo>& strs);
|
||||
};
|
||||
@ -5,7 +5,9 @@
|
||||
#include "Epub.h"
|
||||
#include "EpubReaderActivity.h"
|
||||
#include "FileSelectionActivity.h"
|
||||
#include "../home/GridBrowserActivity.h"
|
||||
#include "activities/util/FullScreenMessageActivity.h"
|
||||
#include "../../CrossPointSettings.h"
|
||||
|
||||
std::string ReaderActivity::extractFolderPath(const std::string& filePath) {
|
||||
const auto lastSlash = filePath.find_last_of('/');
|
||||
@ -51,8 +53,13 @@ void ReaderActivity::onGoToFileSelection(const std::string& fromEpubPath) {
|
||||
exitActivity();
|
||||
// If coming from a book, start in that book's folder; otherwise start from root
|
||||
const auto initialPath = fromEpubPath.empty() ? "/" : extractFolderPath(fromEpubPath);
|
||||
enterNewActivity(new FileSelectionActivity(
|
||||
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack, initialPath));
|
||||
if (SETTINGS.uiTheme == CrossPointSettings::GRID) {
|
||||
enterNewActivity(new GridBrowserActivity(
|
||||
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack, initialPath));
|
||||
} else {
|
||||
enterNewActivity(new FileSelectionActivity(
|
||||
renderer, inputManager, [this](const std::string& path) { onSelectEpubFile(path); }, onGoBack, initialPath));
|
||||
}
|
||||
}
|
||||
|
||||
void ReaderActivity::onGoToEpubReader(std::unique_ptr<Epub> epub) {
|
||||
|
||||
@ -9,14 +9,15 @@
|
||||
|
||||
// Define the static settings list
|
||||
namespace {
|
||||
constexpr int settingsCount = 5;
|
||||
constexpr int settingsCount = 6;
|
||||
const SettingInfo settingsList[settingsCount] = {
|
||||
// Should match with SLEEP_SCREEN_MODE
|
||||
{"Sleep Screen", SettingType::ENUM, &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover"}},
|
||||
{"Status Bar", SettingType::ENUM, &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}},
|
||||
{"Extra Paragraph Spacing", SettingType::TOGGLE, &CrossPointSettings::extraParagraphSpacing, {}},
|
||||
{"Short Power Button Click", SettingType::TOGGLE, &CrossPointSettings::shortPwrBtn, {}},
|
||||
{"Check for updates", SettingType::ACTION, nullptr, {}},
|
||||
{"UI Theme", SettingType::ENUM, &CrossPointSettings::uiTheme, {"List", "Grid"}},
|
||||
{"Check for updates", SettingType::ACTION, nullptr, {}}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
|
||||
77
src/activities/util/Window.cpp
Normal file
77
src/activities/util/Window.cpp
Normal file
@ -0,0 +1,77 @@
|
||||
#include "./Window.h"
|
||||
#include "Battery.h"
|
||||
#include "config.h"
|
||||
|
||||
namespace {
|
||||
constexpr int windowCornerRadius = 16;
|
||||
constexpr int windowBorderWidth = 2;
|
||||
constexpr int fullscreenWindowMargin = 20;
|
||||
constexpr int windowHeaderHeight = 50;
|
||||
constexpr int statusBarHeight = 50;
|
||||
constexpr int batteryWidth = 15;
|
||||
constexpr int batteryHeight = 10;
|
||||
} // namespace
|
||||
|
||||
void drawWindowFrame(GfxRenderer& renderer, int xMargin, int y, int height, bool hasShadow, const char* title) {
|
||||
const int windowWidth = GfxRenderer::getScreenWidth() - 2 * xMargin;
|
||||
|
||||
if (title) { // Header background
|
||||
renderer.fillRectGrey(xMargin, y, windowWidth, windowHeaderHeight, 5);
|
||||
renderer.fillArc(windowCornerRadius, xMargin + windowCornerRadius, y + windowCornerRadius, -1, -1, 0, -1); // TL
|
||||
renderer.fillArc(windowCornerRadius, windowWidth + xMargin - windowCornerRadius, y + windowCornerRadius, 1, -1, 0, -1); // TR
|
||||
}
|
||||
|
||||
renderer.drawRoundedRect(xMargin, y, windowWidth, height, windowBorderWidth, windowCornerRadius, true);
|
||||
|
||||
if (hasShadow) {
|
||||
renderer.drawLine(windowWidth + xMargin, y + windowCornerRadius + 2, windowWidth + xMargin, y + height - windowCornerRadius, windowBorderWidth, true);
|
||||
renderer.drawLine(xMargin + windowCornerRadius + 2, y + height, windowWidth + xMargin - windowCornerRadius, y + height, windowBorderWidth, true);
|
||||
renderer.drawArc(windowCornerRadius + windowBorderWidth, windowWidth + xMargin - 1 - windowCornerRadius, y + height - 1 - windowCornerRadius, 1, 1, windowBorderWidth, true);
|
||||
renderer.drawPixel(xMargin + windowCornerRadius + 1, y + height, true);
|
||||
}
|
||||
|
||||
if (title) { // Header
|
||||
const int titleWidth = renderer.getTextWidth(UI_FONT_ID, title);
|
||||
const int titleX = (GfxRenderer::getScreenWidth() - titleWidth) / 2;
|
||||
const int titleY = y + 10;
|
||||
renderer.drawText(UI_FONT_ID, titleX, titleY, title, true, REGULAR);
|
||||
renderer.drawLine(xMargin, y + windowHeaderHeight, windowWidth + xMargin, y + windowHeaderHeight, windowBorderWidth, true);
|
||||
}
|
||||
}
|
||||
|
||||
void drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title) {
|
||||
drawStatusBar(renderer);
|
||||
drawWindowFrame(renderer, fullscreenWindowMargin, statusBarHeight, GfxRenderer::getScreenHeight() - fullscreenWindowMargin - statusBarHeight, true, title);
|
||||
}
|
||||
|
||||
void drawStatusBar(GfxRenderer& renderer) {
|
||||
constexpr auto textY = 18;
|
||||
|
||||
// Left aligned battery icon and percentage
|
||||
const uint16_t percentage = battery.readPercentage();
|
||||
const auto percentageText = std::to_string(percentage) + "%";
|
||||
renderer.drawText(SMALL_FONT_ID, fullscreenWindowMargin + batteryWidth + 5, textY, percentageText.c_str());
|
||||
|
||||
// 1 column on left, 2 columns on right, 5 columns of battery body
|
||||
constexpr int x = fullscreenWindowMargin;
|
||||
constexpr int y = textY + 5;
|
||||
|
||||
// Top line
|
||||
renderer.drawLine(x, y, x + batteryWidth - 4, y);
|
||||
// Bottom line
|
||||
renderer.drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1);
|
||||
// Left line
|
||||
renderer.drawLine(x, y, x, y + batteryHeight - 1);
|
||||
// Battery end
|
||||
renderer.drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1);
|
||||
renderer.drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 1, y + 2);
|
||||
renderer.drawLine(x + batteryWidth - 3, y + batteryHeight - 3, x + batteryWidth - 1, y + batteryHeight - 3);
|
||||
renderer.drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3);
|
||||
|
||||
// The +1 is to round up, so that we always fill at least one pixel
|
||||
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
|
||||
if (filledWidth > batteryWidth - 5) {
|
||||
filledWidth = batteryWidth - 5; // Ensure we don't overflow
|
||||
}
|
||||
renderer.fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2);
|
||||
}
|
||||
6
src/activities/util/Window.h
Normal file
6
src/activities/util/Window.h
Normal file
@ -0,0 +1,6 @@
|
||||
#pragma once
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
void drawWindowFrame(GfxRenderer& renderer, int xMargin, int y, int height, bool hasShadow, const char* title);
|
||||
void drawFullscreenWindowFrame(GfxRenderer& renderer, const char* title);
|
||||
void drawStatusBar(GfxRenderer& renderer);
|
||||
58
src/images/FolderIcon.h
Normal file
58
src/images/FolderIcon.h
Normal file
@ -0,0 +1,58 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
#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
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user