## Summary **What is the goal of this PR?** This change fixes an issue I noticed while reading where occasionally, especially in italics, some words would have too much space between them. The problem was that word width calculations were including any negative X overhang, and combined with a space before the word, that can lead to an inconsistently large space. ## Additional Context Screenshots of some problematic text: | In CrossPoint 1.0 | With this change | | -- | -- | | <img src="https://github.com/user-attachments/assets/87bf0e4b-341f-4ba9-b3ea-38c13bd26363" width="400" /> | <img src="https://github.com/user-attachments/assets/bf11ba20-c297-4ce1-aa07-43477ef86fc2" width="400" /> | --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_
1221 lines
41 KiB
C++
1221 lines
41 KiB
C++
#include "GfxRenderer.h"
|
|
|
|
#include <Logging.h>
|
|
#include <Utf8.h>
|
|
|
|
void GfxRenderer::begin() {
|
|
frameBuffer = display.getFrameBuffer();
|
|
if (!frameBuffer) {
|
|
LOG_ERR("GFX", "!! No framebuffer");
|
|
assert(false);
|
|
}
|
|
}
|
|
|
|
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, std::move(font)}); }
|
|
|
|
// Translate logical (x,y) coordinates to physical panel coordinates based on current orientation
|
|
// This should always be inlined for better performance
|
|
static inline void rotateCoordinates(const GfxRenderer::Orientation orientation, const int x, const int y, int* phyX,
|
|
int* phyY) {
|
|
switch (orientation) {
|
|
case GfxRenderer::Portrait: {
|
|
// Logical portrait (480x800) → panel (800x480)
|
|
// Rotation: 90 degrees clockwise
|
|
*phyX = y;
|
|
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
|
|
break;
|
|
}
|
|
case GfxRenderer::LandscapeClockwise: {
|
|
// Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right)
|
|
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - x;
|
|
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - y;
|
|
break;
|
|
}
|
|
case GfxRenderer::PortraitInverted: {
|
|
// Logical portrait (480x800) → panel (800x480)
|
|
// Rotation: 90 degrees counter-clockwise
|
|
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - y;
|
|
*phyY = x;
|
|
break;
|
|
}
|
|
case GfxRenderer::LandscapeCounterClockwise: {
|
|
// Logical landscape (800x480) aligned with panel orientation
|
|
*phyX = x;
|
|
*phyY = y;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// IMPORTANT: This function is in critical rendering path and is called for every pixel. Please keep it as simple and
|
|
// efficient as possible.
|
|
void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
|
int phyX = 0;
|
|
int phyY = 0;
|
|
|
|
// Note: this call should be inlined for better performance
|
|
rotateCoordinates(orientation, x, y, &phyX, &phyY);
|
|
|
|
// Bounds checking against physical panel dimensions
|
|
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
|
LOG_ERR("GFX", "!! Outside range (%d, %d) -> (%d, %d)", x, y, phyX, phyY);
|
|
return;
|
|
}
|
|
|
|
// Calculate byte position and bit position
|
|
const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX / 8);
|
|
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first
|
|
|
|
if (state) {
|
|
frameBuffer[byteIndex] &= ~(1 << bitPosition); // Clear bit
|
|
} else {
|
|
frameBuffer[byteIndex] |= 1 << bitPosition; // Set bit
|
|
}
|
|
}
|
|
|
|
void GfxRenderer::drawPixelGray(const int x, const int y, const uint8_t val2bit) const {
|
|
if (renderMode == BW && val2bit < 3) {
|
|
drawPixel(x, y);
|
|
} else if (renderMode == GRAYSCALE_MSB && (val2bit == 1 || val2bit == 2)) {
|
|
drawPixel(x, y, false);
|
|
} else if (renderMode == GRAYSCALE_LSB && val2bit == 1) {
|
|
drawPixel(x, y, false);
|
|
}
|
|
}
|
|
|
|
int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
|
const auto fontIt = fontMap.find(fontId);
|
|
if (fontIt == fontMap.end()) {
|
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
|
return 0;
|
|
}
|
|
|
|
int w = 0, h = 0;
|
|
fontIt->second.getTextDimensions(text, &w, &h, style);
|
|
return w;
|
|
}
|
|
|
|
void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* text, const bool black,
|
|
const EpdFontFamily::Style style) const {
|
|
const int x = (getScreenWidth() - getTextWidth(fontId, text, style)) / 2;
|
|
drawText(fontId, x, y, text, black, style);
|
|
}
|
|
|
|
void GfxRenderer::drawText(const int fontId, const int x, const int y, const char* text, const bool black,
|
|
const EpdFontFamily::Style style) const {
|
|
const int yPos = y + getFontAscenderSize(fontId);
|
|
int xpos = x;
|
|
|
|
// cannot draw a NULL / empty string
|
|
if (text == nullptr || *text == '\0') {
|
|
return;
|
|
}
|
|
|
|
const auto fontIt = fontMap.find(fontId);
|
|
if (fontIt == fontMap.end()) {
|
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
|
return;
|
|
}
|
|
const auto& font = fontIt->second;
|
|
|
|
uint32_t cp;
|
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
|
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) {
|
|
std::swap(y1, y2);
|
|
}
|
|
for (int y = y1; y <= y2; y++) {
|
|
drawPixel(x1, y, state);
|
|
}
|
|
} else if (y1 == y2) {
|
|
if (x2 < x1) {
|
|
std::swap(x1, x2);
|
|
}
|
|
for (int x = x1; x <= x2; x++) {
|
|
drawPixel(x, y1, state);
|
|
}
|
|
} else {
|
|
// Bresenham's line algorithm — integer arithmetic only
|
|
int dx = x2 - x1;
|
|
int dy = y2 - y1;
|
|
int sx = (dx > 0) ? 1 : -1;
|
|
int sy = (dy > 0) ? 1 : -1;
|
|
dx = sx * dx; // abs
|
|
dy = sy * dy; // abs
|
|
|
|
int err = dx - dy;
|
|
while (true) {
|
|
drawPixel(x1, y1, state);
|
|
if (x1 == x2 && y1 == y2) break;
|
|
int e2 = 2 * err;
|
|
if (e2 > -dy) {
|
|
err -= dy;
|
|
x1 += sx;
|
|
}
|
|
if (e2 < dx) {
|
|
err += dx;
|
|
y1 += sy;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
drawLine(x + width - 1, y + height - 1, x, y + height - 1, state);
|
|
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, bool state) const {
|
|
drawRoundedRect(x, y, width, height, lineWidth, cornerRadius, true, true, true, true, 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, bool roundTopLeft, bool roundTopRight, bool roundBottomLeft,
|
|
bool roundBottomRight, 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) {
|
|
if (roundTopLeft || roundTopRight) {
|
|
fillRect(x + maxRadius, y, horizontalWidth, stroke, state);
|
|
}
|
|
if (roundBottomLeft || roundBottomRight) {
|
|
fillRect(x + maxRadius, bottom - stroke + 1, horizontalWidth, stroke, state);
|
|
}
|
|
}
|
|
|
|
const int verticalHeight = height - 2 * maxRadius;
|
|
if (verticalHeight > 0) {
|
|
if (roundTopLeft || roundBottomLeft) {
|
|
fillRect(x, y + maxRadius, stroke, verticalHeight, state);
|
|
}
|
|
if (roundTopRight || roundBottomRight) {
|
|
fillRect(right - stroke + 1, y + maxRadius, stroke, verticalHeight, state);
|
|
}
|
|
}
|
|
|
|
if (roundTopLeft) {
|
|
drawArc(maxRadius, x + maxRadius, y + maxRadius, -1, -1, lineWidth, state);
|
|
}
|
|
if (roundTopRight) {
|
|
drawArc(maxRadius, right - maxRadius, y + maxRadius, 1, -1, lineWidth, state);
|
|
}
|
|
if (roundBottomRight) {
|
|
drawArc(maxRadius, right - maxRadius, bottom - maxRadius, 1, 1, lineWidth, state);
|
|
}
|
|
if (roundBottomLeft) {
|
|
drawArc(maxRadius, x + maxRadius, bottom - maxRadius, -1, 1, lineWidth, state);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
// NOTE: Those are in critical path, and need to be templated to avoid runtime checks for every pixel.
|
|
// Any branching must be done outside the loops to avoid performance degradation.
|
|
template <>
|
|
void GfxRenderer::drawPixelDither<Color::Clear>(const int x, const int y) const {
|
|
// Do nothing
|
|
}
|
|
|
|
template <>
|
|
void GfxRenderer::drawPixelDither<Color::Black>(const int x, const int y) const {
|
|
drawPixel(x, y, true);
|
|
}
|
|
|
|
template <>
|
|
void GfxRenderer::drawPixelDither<Color::White>(const int x, const int y) const {
|
|
drawPixel(x, y, false);
|
|
}
|
|
|
|
template <>
|
|
void GfxRenderer::drawPixelDither<Color::LightGray>(const int x, const int y) const {
|
|
drawPixel(x, y, x % 2 == 0 && y % 2 == 0);
|
|
}
|
|
|
|
template <>
|
|
void GfxRenderer::drawPixelDither<Color::DarkGray>(const int x, const int y) const {
|
|
drawPixel(x, y, (x + y) % 2 == 0);
|
|
}
|
|
|
|
void GfxRenderer::fillRectDither(const int x, const int y, const int width, const int height, Color color) const {
|
|
if (color == Color::Clear) {
|
|
} else if (color == Color::Black) {
|
|
fillRect(x, y, width, height, true);
|
|
} else if (color == Color::White) {
|
|
fillRect(x, y, width, height, false);
|
|
} else if (color == Color::LightGray) {
|
|
for (int fillY = y; fillY < y + height; fillY++) {
|
|
for (int fillX = x; fillX < x + width; fillX++) {
|
|
drawPixelDither<Color::LightGray>(fillX, fillY);
|
|
}
|
|
}
|
|
} else if (color == Color::DarkGray) {
|
|
for (int fillY = y; fillY < y + height; fillY++) {
|
|
for (int fillX = x; fillX < x + width; fillX++) {
|
|
drawPixelDither<Color::DarkGray>(fillX, fillY);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
template <Color color>
|
|
void GfxRenderer::fillArc(const int maxRadius, const int cx, const int cy, const int xDir, const int yDir) 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) {
|
|
drawPixelDither<color>(px, py);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, const int height, const int cornerRadius,
|
|
const Color color) const {
|
|
fillRoundedRect(x, y, width, height, cornerRadius, true, true, true, true, color);
|
|
}
|
|
|
|
void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, const int height, const int cornerRadius,
|
|
bool roundTopLeft, bool roundTopRight, bool roundBottomLeft, bool roundBottomRight,
|
|
const Color color) const {
|
|
if (width <= 0 || height <= 0) {
|
|
return;
|
|
}
|
|
|
|
// Assume if we're not rounding all corners then we are only rounding one side
|
|
const int roundedSides = (!roundTopLeft || !roundTopRight || !roundBottomLeft || !roundBottomRight) ? 1 : 2;
|
|
const int maxRadius = std::min({cornerRadius, width / roundedSides, height / roundedSides});
|
|
if (maxRadius <= 0) {
|
|
fillRectDither(x, y, width, height, color);
|
|
return;
|
|
}
|
|
|
|
const int horizontalWidth = width - 2 * maxRadius;
|
|
if (horizontalWidth > 0) {
|
|
fillRectDither(x + maxRadius + 1, y, horizontalWidth - 2, height, color);
|
|
}
|
|
|
|
const int leftFillTop = y + (roundTopLeft ? (maxRadius + 1) : 0);
|
|
const int leftFillBottom = y + height - 1 - (roundBottomLeft ? (maxRadius + 1) : 0);
|
|
if (leftFillBottom >= leftFillTop) {
|
|
fillRectDither(x, leftFillTop, maxRadius + 1, leftFillBottom - leftFillTop + 1, color);
|
|
}
|
|
|
|
const int rightFillTop = y + (roundTopRight ? (maxRadius + 1) : 0);
|
|
const int rightFillBottom = y + height - 1 - (roundBottomRight ? (maxRadius + 1) : 0);
|
|
if (rightFillBottom >= rightFillTop) {
|
|
fillRectDither(x + width - maxRadius - 1, rightFillTop, maxRadius + 1, rightFillBottom - rightFillTop + 1, color);
|
|
}
|
|
|
|
auto fillArcTemplated = [this](int maxRadius, int cx, int cy, int xDir, int yDir, Color color) {
|
|
switch (color) {
|
|
case Color::Clear:
|
|
break;
|
|
case Color::Black:
|
|
fillArc<Color::Black>(maxRadius, cx, cy, xDir, yDir);
|
|
break;
|
|
case Color::White:
|
|
fillArc<Color::White>(maxRadius, cx, cy, xDir, yDir);
|
|
break;
|
|
case Color::LightGray:
|
|
fillArc<Color::LightGray>(maxRadius, cx, cy, xDir, yDir);
|
|
break;
|
|
case Color::DarkGray:
|
|
fillArc<Color::DarkGray>(maxRadius, cx, cy, xDir, yDir);
|
|
break;
|
|
}
|
|
};
|
|
|
|
if (roundTopLeft) {
|
|
fillArcTemplated(maxRadius, x + maxRadius, y + maxRadius, -1, -1, color);
|
|
}
|
|
|
|
if (roundTopRight) {
|
|
fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + maxRadius, 1, -1, color);
|
|
}
|
|
|
|
if (roundBottomRight) {
|
|
fillArcTemplated(maxRadius, x + width - maxRadius - 1, y + height - maxRadius - 1, 1, 1, color);
|
|
}
|
|
|
|
if (roundBottomLeft) {
|
|
fillArcTemplated(maxRadius, x + maxRadius, y + height - maxRadius - 1, -1, 1, color);
|
|
}
|
|
}
|
|
|
|
void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
|
int rotatedX = 0;
|
|
int rotatedY = 0;
|
|
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY);
|
|
// Rotate origin corner
|
|
switch (orientation) {
|
|
case Portrait:
|
|
rotatedY = rotatedY - height;
|
|
break;
|
|
case PortraitInverted:
|
|
rotatedX = rotatedX - width;
|
|
break;
|
|
case LandscapeClockwise:
|
|
rotatedY = rotatedY - height;
|
|
rotatedX = rotatedX - width;
|
|
break;
|
|
case LandscapeCounterClockwise:
|
|
break;
|
|
}
|
|
// TODO: Rotate bits
|
|
display.drawImage(bitmap, rotatedX, rotatedY, width, height);
|
|
}
|
|
|
|
void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const {
|
|
display.drawImageTransparent(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 cropX, const float cropY) const {
|
|
// For 1-bit bitmaps, use optimized 1-bit rendering path (no crop support for 1-bit)
|
|
if (bitmap.is1Bit() && cropX == 0.0f && cropY == 0.0f) {
|
|
drawBitmap1Bit(bitmap, x, y, maxWidth, maxHeight);
|
|
return;
|
|
}
|
|
|
|
float scale = 1.0f;
|
|
bool isScaled = false;
|
|
int cropPixX = std::floor(bitmap.getWidth() * cropX / 2.0f);
|
|
int cropPixY = std::floor(bitmap.getHeight() * cropY / 2.0f);
|
|
LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY,
|
|
bitmap.isTopDown() ? "top-down" : "bottom-up");
|
|
|
|
const float effectiveWidth = (1.0f - cropX) * bitmap.getWidth();
|
|
const float effectiveHeight = (1.0f - cropY) * bitmap.getHeight();
|
|
|
|
// Calculate scale factor: supports both downscaling and upscaling when both constraints are provided
|
|
if (maxWidth > 0 && maxHeight > 0) {
|
|
const float scaleX = static_cast<float>(maxWidth) / effectiveWidth;
|
|
const float scaleY = static_cast<float>(maxHeight) / effectiveHeight;
|
|
scale = std::min(scaleX, scaleY);
|
|
isScaled = (scale < 0.999f || scale > 1.001f);
|
|
} else if (maxWidth > 0 && effectiveWidth > static_cast<float>(maxWidth)) {
|
|
scale = static_cast<float>(maxWidth) / effectiveWidth;
|
|
isScaled = true;
|
|
} else if (maxHeight > 0 && effectiveHeight > static_cast<float>(maxHeight)) {
|
|
scale = static_cast<float>(maxHeight) / effectiveHeight;
|
|
isScaled = true;
|
|
}
|
|
LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled");
|
|
|
|
// Calculate output row size (2 bits per pixel, packed into bytes)
|
|
// IMPORTANT: Use int, not uint8_t, to avoid overflow for images > 1020 pixels wide
|
|
const int outputRowSize = (bitmap.getWidth() + 3) / 4;
|
|
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
|
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
|
|
|
if (!outputRow || !rowBytes) {
|
|
LOG_ERR("GFX", "!! Failed to allocate BMP row buffers");
|
|
free(outputRow);
|
|
free(rowBytes);
|
|
return;
|
|
}
|
|
|
|
for (int bmpY = 0; bmpY < (bitmap.getHeight() - cropPixY); bmpY++) {
|
|
// The BMP's (0, 0) is the bottom-left corner (if the height is positive, top-left if negative).
|
|
// Screen's (0, 0) is the top-left corner.
|
|
const int logicalY = -cropPixY + (bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY);
|
|
int screenYStart, screenYEnd;
|
|
if (isScaled) {
|
|
screenYStart = static_cast<int>(std::floor(logicalY * scale)) + y;
|
|
screenYEnd = static_cast<int>(std::floor((logicalY + 1) * scale)) + y;
|
|
} else {
|
|
screenYStart = logicalY + y;
|
|
screenYEnd = screenYStart + 1;
|
|
}
|
|
|
|
if (screenYStart >= getScreenHeight()) {
|
|
break;
|
|
}
|
|
|
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
|
LOG_ERR("GFX", "Failed to read row %d from bitmap", bmpY);
|
|
free(outputRow);
|
|
free(rowBytes);
|
|
return;
|
|
}
|
|
|
|
if (screenYEnd <= 0) {
|
|
continue;
|
|
}
|
|
|
|
if (bmpY < cropPixY) {
|
|
// Skip the row if it's outside the crop area
|
|
continue;
|
|
}
|
|
|
|
const int syStart = std::max(screenYStart, 0);
|
|
const int syEnd = std::min(screenYEnd, getScreenHeight());
|
|
|
|
for (int bmpX = cropPixX; bmpX < bitmap.getWidth() - cropPixX; bmpX++) {
|
|
const int outX = bmpX - cropPixX;
|
|
int screenXStart, screenXEnd;
|
|
if (isScaled) {
|
|
screenXStart = static_cast<int>(std::floor(outX * scale)) + x;
|
|
screenXEnd = static_cast<int>(std::floor((outX + 1) * scale)) + x;
|
|
} else {
|
|
screenXStart = outX + x;
|
|
screenXEnd = screenXStart + 1;
|
|
}
|
|
|
|
if (screenXStart >= getScreenWidth()) {
|
|
break;
|
|
}
|
|
if (screenXEnd <= 0) {
|
|
continue;
|
|
}
|
|
|
|
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
|
|
|
const int sxStart = std::max(screenXStart, 0);
|
|
const int sxEnd = std::min(screenXEnd, getScreenWidth());
|
|
|
|
for (int sy = syStart; sy < syEnd; sy++) {
|
|
for (int sx = sxStart; sx < sxEnd; sx++) {
|
|
if (renderMode == BW && val < 3) {
|
|
drawPixel(sx, sy);
|
|
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
|
|
drawPixel(sx, sy, false);
|
|
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
|
drawPixel(sx, sy, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
free(outputRow);
|
|
free(rowBytes);
|
|
}
|
|
|
|
void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y, const int maxWidth,
|
|
const int maxHeight) const {
|
|
float scale = 1.0f;
|
|
bool isScaled = false;
|
|
// Calculate scale factor: supports both downscaling and upscaling when both constraints are provided
|
|
if (maxWidth > 0 && maxHeight > 0) {
|
|
const float scaleX = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
|
const float scaleY = static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight());
|
|
scale = std::min(scaleX, scaleY);
|
|
isScaled = (scale < 0.999f || scale > 1.001f);
|
|
} else if (maxWidth > 0 && bitmap.getWidth() > maxWidth) {
|
|
scale = static_cast<float>(maxWidth) / static_cast<float>(bitmap.getWidth());
|
|
isScaled = true;
|
|
} else if (maxHeight > 0 && bitmap.getHeight() > maxHeight) {
|
|
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>(bitmap.getHeight()));
|
|
isScaled = true;
|
|
}
|
|
|
|
// For 1-bit BMP, output is still 2-bit packed (for consistency with readNextRow)
|
|
const int outputRowSize = (bitmap.getWidth() + 3) / 4;
|
|
auto* outputRow = static_cast<uint8_t*>(malloc(outputRowSize));
|
|
auto* rowBytes = static_cast<uint8_t*>(malloc(bitmap.getRowBytes()));
|
|
|
|
if (!outputRow || !rowBytes) {
|
|
LOG_ERR("GFX", "!! Failed to allocate 1-bit BMP row buffers");
|
|
free(outputRow);
|
|
free(rowBytes);
|
|
return;
|
|
}
|
|
|
|
for (int bmpY = 0; bmpY < bitmap.getHeight(); bmpY++) {
|
|
// Read rows sequentially using readNextRow
|
|
if (bitmap.readNextRow(outputRow, rowBytes) != BmpReaderError::Ok) {
|
|
LOG_ERR("GFX", "Failed to read row %d from 1-bit bitmap", bmpY);
|
|
free(outputRow);
|
|
free(rowBytes);
|
|
return;
|
|
}
|
|
|
|
// Calculate screen Y based on whether BMP is top-down or bottom-up
|
|
const int bmpYOffset = bitmap.isTopDown() ? bmpY : bitmap.getHeight() - 1 - bmpY;
|
|
int screenYStart, screenYEnd;
|
|
if (isScaled) {
|
|
screenYStart = static_cast<int>(std::floor(bmpYOffset * scale)) + y;
|
|
screenYEnd = static_cast<int>(std::floor((bmpYOffset + 1) * scale)) + y;
|
|
} else {
|
|
screenYStart = bmpYOffset + y;
|
|
screenYEnd = screenYStart + 1;
|
|
}
|
|
if (screenYStart >= getScreenHeight()) {
|
|
continue; // Continue reading to keep row counter in sync
|
|
}
|
|
if (screenYEnd <= 0) {
|
|
continue;
|
|
}
|
|
|
|
const int syStart = std::max(screenYStart, 0);
|
|
const int syEnd = std::min(screenYEnd, getScreenHeight());
|
|
|
|
for (int bmpX = 0; bmpX < bitmap.getWidth(); bmpX++) {
|
|
int screenXStart, screenXEnd;
|
|
if (isScaled) {
|
|
screenXStart = static_cast<int>(std::floor(bmpX * scale)) + x;
|
|
screenXEnd = static_cast<int>(std::floor((bmpX + 1) * scale)) + x;
|
|
} else {
|
|
screenXStart = bmpX + x;
|
|
screenXEnd = screenXStart + 1;
|
|
}
|
|
if (screenXStart >= getScreenWidth()) {
|
|
break;
|
|
}
|
|
if (screenXEnd <= 0) {
|
|
continue;
|
|
}
|
|
|
|
// Get 2-bit value (result of readNextRow quantization)
|
|
const uint8_t val = outputRow[bmpX / 4] >> (6 - ((bmpX * 2) % 8)) & 0x3;
|
|
|
|
// For 1-bit source: 0 or 1 -> map to black (0,1,2) or white (3)
|
|
// val < 3 means black pixel (draw it)
|
|
if (val < 3) {
|
|
const int sxStart = std::max(screenXStart, 0);
|
|
const int sxEnd = std::min(screenXEnd, getScreenWidth());
|
|
for (int sy = syStart; sy < syEnd; sy++) {
|
|
for (int sx = sxStart; sx < sxEnd; sx++) {
|
|
drawPixel(sx, sy, true);
|
|
}
|
|
}
|
|
}
|
|
// White pixels (val == 3) are not drawn (leave background)
|
|
}
|
|
}
|
|
|
|
free(outputRow);
|
|
free(rowBytes);
|
|
}
|
|
|
|
void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state) const {
|
|
if (numPoints < 3) return;
|
|
|
|
// Find bounding box
|
|
int minY = yPoints[0], maxY = yPoints[0];
|
|
for (int i = 1; i < numPoints; i++) {
|
|
if (yPoints[i] < minY) minY = yPoints[i];
|
|
if (yPoints[i] > maxY) maxY = yPoints[i];
|
|
}
|
|
|
|
// Clip to screen
|
|
if (minY < 0) minY = 0;
|
|
if (maxY >= getScreenHeight()) maxY = getScreenHeight() - 1;
|
|
|
|
// Allocate node buffer for scanline algorithm
|
|
auto* nodeX = static_cast<int*>(malloc(numPoints * sizeof(int)));
|
|
if (!nodeX) {
|
|
LOG_ERR("GFX", "!! Failed to allocate polygon node buffer");
|
|
return;
|
|
}
|
|
|
|
// Scanline fill algorithm
|
|
for (int scanY = minY; scanY <= maxY; scanY++) {
|
|
int nodes = 0;
|
|
|
|
// Find all intersection points with edges
|
|
int j = numPoints - 1;
|
|
for (int i = 0; i < numPoints; i++) {
|
|
if ((yPoints[i] < scanY && yPoints[j] >= scanY) || (yPoints[j] < scanY && yPoints[i] >= scanY)) {
|
|
// Calculate X intersection using fixed-point to avoid float
|
|
int dy = yPoints[j] - yPoints[i];
|
|
if (dy != 0) {
|
|
nodeX[nodes++] = xPoints[i] + (scanY - yPoints[i]) * (xPoints[j] - xPoints[i]) / dy;
|
|
}
|
|
}
|
|
j = i;
|
|
}
|
|
|
|
// Sort nodes by X (simple bubble sort, numPoints is small)
|
|
for (int i = 0; i < nodes - 1; i++) {
|
|
for (int k = i + 1; k < nodes; k++) {
|
|
if (nodeX[i] > nodeX[k]) {
|
|
int temp = nodeX[i];
|
|
nodeX[i] = nodeX[k];
|
|
nodeX[k] = temp;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fill between pairs of nodes
|
|
for (int i = 0; i < nodes - 1; i += 2) {
|
|
int startX = nodeX[i];
|
|
int endX = nodeX[i + 1];
|
|
|
|
// Clip to screen
|
|
if (startX < 0) startX = 0;
|
|
if (endX >= getScreenWidth()) endX = getScreenWidth() - 1;
|
|
|
|
// Draw horizontal line
|
|
for (int x = startX; x <= endX; x++) {
|
|
drawPixel(x, scanY, state);
|
|
}
|
|
}
|
|
}
|
|
|
|
free(nodeX);
|
|
}
|
|
|
|
// For performance measurement (using static to allow "const" methods)
|
|
static unsigned long start_ms = 0;
|
|
|
|
void GfxRenderer::clearScreen(const uint8_t color) const {
|
|
start_ms = millis();
|
|
display.clearScreen(color);
|
|
}
|
|
|
|
void GfxRenderer::invertScreen() const {
|
|
for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
|
|
frameBuffer[i] = ~frameBuffer[i];
|
|
}
|
|
}
|
|
|
|
void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const {
|
|
auto elapsed = millis() - start_ms;
|
|
LOG_DBG("GFX", "Time = %lu ms from clearScreen to displayBuffer", elapsed);
|
|
display.displayBuffer(refreshMode, fadingFix);
|
|
}
|
|
|
|
// EXPERIMENTAL: Display only a rectangular region with specified refresh mode
|
|
void GfxRenderer::displayWindow(int x, int y, int width, int height,
|
|
HalDisplay::RefreshMode mode) const {
|
|
LOG_DBG("GFX", "Displaying window at (%d,%d) size (%dx%d) with mode %d", x, y, width, height,
|
|
static_cast<int>(mode));
|
|
|
|
// Validate bounds
|
|
if (x < 0 || y < 0 || x + width > getScreenWidth() || y + height > getScreenHeight()) {
|
|
LOG_ERR("GFX", "Window bounds exceed display dimensions!");
|
|
return;
|
|
}
|
|
|
|
display.displayWindow(static_cast<uint16_t>(x), static_cast<uint16_t>(y),
|
|
static_cast<uint16_t>(width), static_cast<uint16_t>(height), mode,
|
|
fadingFix);
|
|
}
|
|
|
|
std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth,
|
|
const EpdFontFamily::Style style) const {
|
|
if (!text || maxWidth <= 0) return "";
|
|
|
|
std::string item = text;
|
|
const char* ellipsis = "...";
|
|
int textWidth = getTextWidth(fontId, item.c_str(), style);
|
|
if (textWidth <= maxWidth) {
|
|
// Text fits, return as is
|
|
return item;
|
|
}
|
|
|
|
while (!item.empty() && getTextWidth(fontId, (item + ellipsis).c_str(), style) >= maxWidth) {
|
|
utf8RemoveLastChar(item);
|
|
}
|
|
|
|
return item.empty() ? ellipsis : item + ellipsis;
|
|
}
|
|
|
|
// Note: Internal driver treats screen in command orientation; this library exposes a logical orientation
|
|
int GfxRenderer::getScreenWidth() const {
|
|
switch (orientation) {
|
|
case Portrait:
|
|
case PortraitInverted:
|
|
// 480px wide in portrait logical coordinates
|
|
return HalDisplay::DISPLAY_HEIGHT;
|
|
case LandscapeClockwise:
|
|
case LandscapeCounterClockwise:
|
|
// 800px wide in landscape logical coordinates
|
|
return HalDisplay::DISPLAY_WIDTH;
|
|
}
|
|
return HalDisplay::DISPLAY_HEIGHT;
|
|
}
|
|
|
|
int GfxRenderer::getScreenHeight() const {
|
|
switch (orientation) {
|
|
case Portrait:
|
|
case PortraitInverted:
|
|
// 800px tall in portrait logical coordinates
|
|
return HalDisplay::DISPLAY_WIDTH;
|
|
case LandscapeClockwise:
|
|
case LandscapeCounterClockwise:
|
|
// 480px tall in landscape logical coordinates
|
|
return HalDisplay::DISPLAY_HEIGHT;
|
|
}
|
|
return HalDisplay::DISPLAY_WIDTH;
|
|
}
|
|
|
|
int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style style) const {
|
|
const auto fontIt = fontMap.find(fontId);
|
|
if (fontIt == fontMap.end()) {
|
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
|
return 0;
|
|
}
|
|
|
|
return fontIt->second.getGlyph(' ', style)->advanceX;
|
|
}
|
|
|
|
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, const EpdFontFamily::Style style) const {
|
|
const auto fontIt = fontMap.find(fontId);
|
|
if (fontIt == fontMap.end()) {
|
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
|
return 0;
|
|
}
|
|
|
|
uint32_t cp;
|
|
int width = 0;
|
|
const auto& font = fontIt->second;
|
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
|
width += font.getGlyph(cp, style)->advanceX;
|
|
}
|
|
return width;
|
|
}
|
|
|
|
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
|
const auto fontIt = fontMap.find(fontId);
|
|
if (fontIt == fontMap.end()) {
|
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
|
return 0;
|
|
}
|
|
|
|
return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
|
|
}
|
|
|
|
int GfxRenderer::getLineHeight(const int fontId) const {
|
|
const auto fontIt = fontMap.find(fontId);
|
|
if (fontIt == fontMap.end()) {
|
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
|
return 0;
|
|
}
|
|
|
|
return fontIt->second.getData(EpdFontFamily::REGULAR)->advanceY;
|
|
}
|
|
|
|
int GfxRenderer::getTextHeight(const int fontId) const {
|
|
const auto fontIt = fontMap.find(fontId);
|
|
if (fontIt == fontMap.end()) {
|
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
|
return 0;
|
|
}
|
|
return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
|
|
}
|
|
|
|
void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y, const char* text, const bool black,
|
|
const EpdFontFamily::Style style) const {
|
|
// Cannot draw a NULL / empty string
|
|
if (text == nullptr || *text == '\0') {
|
|
return;
|
|
}
|
|
|
|
const auto fontIt = fontMap.find(fontId);
|
|
if (fontIt == fontMap.end()) {
|
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
|
return;
|
|
}
|
|
|
|
const auto& font = fontIt->second;
|
|
|
|
// For 90° clockwise rotation:
|
|
// Original (glyphX, glyphY) -> Rotated (glyphY, -glyphX)
|
|
// Text reads from bottom to top
|
|
|
|
int yPos = y; // Current Y position (decreases as we draw characters)
|
|
|
|
uint32_t cp;
|
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
|
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
|
if (!glyph) {
|
|
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
|
}
|
|
if (!glyph) {
|
|
continue;
|
|
}
|
|
|
|
const int is2Bit = font.getData(style)->is2Bit;
|
|
const uint32_t offset = glyph->dataOffset;
|
|
const uint8_t width = glyph->width;
|
|
const uint8_t height = glyph->height;
|
|
const int left = glyph->left;
|
|
const int top = glyph->top;
|
|
|
|
const uint8_t* bitmap = &font.getData(style)->bitmap[offset];
|
|
|
|
if (bitmap != nullptr) {
|
|
for (int glyphY = 0; glyphY < height; glyphY++) {
|
|
for (int glyphX = 0; glyphX < width; glyphX++) {
|
|
const int pixelPosition = glyphY * width + glyphX;
|
|
|
|
// 90° clockwise rotation transformation:
|
|
// screenX = x + (ascender - top + glyphY)
|
|
// screenY = yPos - (left + glyphX)
|
|
const int screenX = x + (font.getData(style)->ascender - top + glyphY);
|
|
const int screenY = yPos - left - glyphX;
|
|
|
|
if (is2Bit) {
|
|
const uint8_t byte = bitmap[pixelPosition / 4];
|
|
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
|
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
|
|
|
|
if (renderMode == BW && bmpVal < 3) {
|
|
drawPixel(screenX, screenY, black);
|
|
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
|
drawPixel(screenX, screenY, false);
|
|
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
|
|
drawPixel(screenX, screenY, false);
|
|
}
|
|
} else {
|
|
const uint8_t byte = bitmap[pixelPosition / 8];
|
|
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
|
|
|
if ((byte >> bit_index) & 1) {
|
|
drawPixel(screenX, screenY, black);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move to next character position (going up, so decrease Y)
|
|
yPos -= glyph->advanceX;
|
|
}
|
|
}
|
|
|
|
void GfxRenderer::drawTextRotated90CCW(const int fontId, const int x, const int y, const char* text, const bool black,
|
|
const EpdFontFamily::Style style) const {
|
|
// Cannot draw a NULL / empty string
|
|
if (text == nullptr || *text == '\0') {
|
|
return;
|
|
}
|
|
|
|
if (fontMap.count(fontId) == 0) {
|
|
LOG_ERR("GFX", "Font %d not found", fontId);
|
|
return;
|
|
}
|
|
const auto font = fontMap.at(fontId);
|
|
|
|
// For 90° counter-clockwise rotation:
|
|
// Mirror of CW: glyphY maps to -X direction, glyphX maps to +Y direction
|
|
// Text reads from top to bottom
|
|
|
|
const int advanceY = font.getData(style)->advanceY;
|
|
const int ascender = font.getData(style)->ascender;
|
|
|
|
int yPos = y; // Current Y position (increases as we draw characters)
|
|
|
|
uint32_t cp;
|
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
|
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
|
if (!glyph) {
|
|
glyph = font.getGlyph(REPLACEMENT_GLYPH, style);
|
|
}
|
|
if (!glyph) {
|
|
continue;
|
|
}
|
|
|
|
const int is2Bit = font.getData(style)->is2Bit;
|
|
const uint32_t offset = glyph->dataOffset;
|
|
const uint8_t width = glyph->width;
|
|
const uint8_t height = glyph->height;
|
|
const int left = glyph->left;
|
|
const int top = glyph->top;
|
|
|
|
const uint8_t* bitmap = &font.getData(style)->bitmap[offset];
|
|
|
|
if (bitmap != nullptr) {
|
|
for (int glyphY = 0; glyphY < height; glyphY++) {
|
|
for (int glyphX = 0; glyphX < width; glyphX++) {
|
|
const int pixelPosition = glyphY * width + glyphX;
|
|
|
|
// 90° counter-clockwise rotation transformation:
|
|
// screenX = mirrored CW X (right-to-left within advanceY span)
|
|
// screenY = yPos + (left + glyphX) (downward)
|
|
const int screenX = x + advanceY - 1 - (ascender - top + glyphY);
|
|
const int screenY = yPos + left + glyphX;
|
|
|
|
if (is2Bit) {
|
|
const uint8_t byte = bitmap[pixelPosition / 4];
|
|
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
|
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
|
|
|
|
if (renderMode == BW && bmpVal < 3) {
|
|
drawPixel(screenX, screenY, black);
|
|
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
|
drawPixel(screenX, screenY, false);
|
|
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
|
|
drawPixel(screenX, screenY, false);
|
|
}
|
|
} else {
|
|
const uint8_t byte = bitmap[pixelPosition / 8];
|
|
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
|
|
|
if ((byte >> bit_index) & 1) {
|
|
drawPixel(screenX, screenY, black);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move to next character position (going down, so increase Y)
|
|
yPos += glyph->advanceX;
|
|
}
|
|
}
|
|
|
|
uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; }
|
|
|
|
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
|
|
|
|
// unused
|
|
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
|
|
|
|
void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(frameBuffer); }
|
|
|
|
void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(frameBuffer); }
|
|
|
|
void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(fadingFix); }
|
|
|
|
void GfxRenderer::freeBwBufferChunks() {
|
|
for (auto& bwBufferChunk : bwBufferChunks) {
|
|
if (bwBufferChunk) {
|
|
free(bwBufferChunk);
|
|
bwBufferChunk = nullptr;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This should be called before grayscale buffers are populated.
|
|
* A `restoreBwBuffer` call should always follow the grayscale render if this method was called.
|
|
* Uses chunked allocation to avoid needing 48KB of contiguous memory.
|
|
* Returns true if buffer was stored successfully, false if allocation failed.
|
|
*/
|
|
bool GfxRenderer::storeBwBuffer() {
|
|
// Allocate and copy each chunk
|
|
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
|
// Check if any chunks are already allocated
|
|
if (bwBufferChunks[i]) {
|
|
LOG_ERR("GFX", "!! BW buffer chunk %zu already stored - this is likely a bug, freeing chunk", i);
|
|
free(bwBufferChunks[i]);
|
|
bwBufferChunks[i] = nullptr;
|
|
}
|
|
|
|
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
|
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
|
|
|
|
if (!bwBufferChunks[i]) {
|
|
LOG_ERR("GFX", "!! Failed to allocate BW buffer chunk %zu (%zu bytes)", i, BW_BUFFER_CHUNK_SIZE);
|
|
// Free previously allocated chunks
|
|
freeBwBufferChunks();
|
|
return false;
|
|
}
|
|
|
|
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
|
}
|
|
|
|
LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", BW_BUFFER_NUM_CHUNKS, BW_BUFFER_CHUNK_SIZE);
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* This can only be called if `storeBwBuffer` was called prior to the grayscale render.
|
|
* It should be called to restore the BW buffer state after grayscale rendering is complete.
|
|
* Uses chunked restoration to match chunked storage.
|
|
*/
|
|
void GfxRenderer::restoreBwBuffer() {
|
|
// Check if any all chunks are allocated
|
|
bool missingChunks = false;
|
|
for (const auto& bwBufferChunk : bwBufferChunks) {
|
|
if (!bwBufferChunk) {
|
|
missingChunks = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (missingChunks) {
|
|
freeBwBufferChunks();
|
|
return;
|
|
}
|
|
|
|
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
|
// Check if chunk is missing
|
|
if (!bwBufferChunks[i]) {
|
|
LOG_ERR("GFX", "!! BW buffer chunks not stored - this is likely a bug");
|
|
freeBwBufferChunks();
|
|
return;
|
|
}
|
|
|
|
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
|
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
|
|
}
|
|
|
|
display.cleanupGrayscaleBuffers(frameBuffer);
|
|
|
|
freeBwBufferChunks();
|
|
LOG_DBG("GFX", "Restored and freed BW buffer chunks");
|
|
}
|
|
|
|
/**
|
|
* Cleanup grayscale buffers using the current frame buffer.
|
|
* Use this when BW buffer was re-rendered instead of stored/restored.
|
|
*/
|
|
void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
|
|
if (frameBuffer) {
|
|
display.cleanupGrayscaleBuffers(frameBuffer);
|
|
}
|
|
}
|
|
|
|
void GfxRenderer::renderChar(const EpdFontFamily& fontFamily, const uint32_t cp, int* x, const int* y,
|
|
const bool pixelState, const EpdFontFamily::Style style) const {
|
|
const EpdGlyph* glyph = fontFamily.getGlyph(cp, style);
|
|
if (!glyph) {
|
|
glyph = fontFamily.getGlyph(REPLACEMENT_GLYPH, style);
|
|
}
|
|
|
|
// no glyph?
|
|
if (!glyph) {
|
|
LOG_ERR("GFX", "No glyph for codepoint %d", cp);
|
|
return;
|
|
}
|
|
|
|
const int is2Bit = fontFamily.getData(style)->is2Bit;
|
|
const uint32_t offset = glyph->dataOffset;
|
|
const uint8_t width = glyph->width;
|
|
const uint8_t height = glyph->height;
|
|
const int left = glyph->left;
|
|
|
|
const uint8_t* bitmap = nullptr;
|
|
bitmap = &fontFamily.getData(style)->bitmap[offset];
|
|
|
|
if (bitmap != nullptr) {
|
|
for (int glyphY = 0; glyphY < height; glyphY++) {
|
|
const int screenY = *y - glyph->top + glyphY;
|
|
for (int glyphX = 0; glyphX < width; glyphX++) {
|
|
const int pixelPosition = glyphY * width + glyphX;
|
|
const int screenX = *x + left + glyphX;
|
|
|
|
if (is2Bit) {
|
|
const uint8_t byte = bitmap[pixelPosition / 4];
|
|
const uint8_t bit_index = (3 - pixelPosition % 4) * 2;
|
|
// the direct bit from the font is 0 -> white, 1 -> light gray, 2 -> dark gray, 3 -> black
|
|
// we swap this to better match the way images and screen think about colors:
|
|
// 0 -> black, 1 -> dark grey, 2 -> light grey, 3 -> white
|
|
const uint8_t bmpVal = 3 - (byte >> bit_index) & 0x3;
|
|
|
|
if (renderMode == BW && bmpVal < 3) {
|
|
// Black (also paints over the grays in BW mode)
|
|
drawPixel(screenX, screenY, pixelState);
|
|
} else if (renderMode == GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
|
// Light gray (also mark the MSB if it's going to be a dark gray too)
|
|
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
|
|
drawPixel(screenX, screenY, false);
|
|
} else if (renderMode == GRAYSCALE_LSB && bmpVal == 1) {
|
|
// Dark gray
|
|
drawPixel(screenX, screenY, false);
|
|
}
|
|
} else {
|
|
const uint8_t byte = bitmap[pixelPosition / 8];
|
|
const uint8_t bit_index = 7 - (pixelPosition % 8);
|
|
|
|
if ((byte >> bit_index) & 1) {
|
|
drawPixel(screenX, screenY, pixelState);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
*x += glyph->advanceX;
|
|
}
|
|
|
|
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
|
|
switch (orientation) {
|
|
case Portrait:
|
|
*outTop = VIEWABLE_MARGIN_TOP;
|
|
*outRight = VIEWABLE_MARGIN_RIGHT;
|
|
*outBottom = VIEWABLE_MARGIN_BOTTOM;
|
|
*outLeft = VIEWABLE_MARGIN_LEFT;
|
|
break;
|
|
case LandscapeClockwise:
|
|
*outTop = VIEWABLE_MARGIN_LEFT;
|
|
*outRight = VIEWABLE_MARGIN_TOP;
|
|
*outBottom = VIEWABLE_MARGIN_RIGHT;
|
|
*outLeft = VIEWABLE_MARGIN_BOTTOM;
|
|
break;
|
|
case PortraitInverted:
|
|
*outTop = VIEWABLE_MARGIN_BOTTOM;
|
|
*outRight = VIEWABLE_MARGIN_LEFT;
|
|
*outBottom = VIEWABLE_MARGIN_TOP;
|
|
*outLeft = VIEWABLE_MARGIN_RIGHT;
|
|
break;
|
|
case LandscapeCounterClockwise:
|
|
*outTop = VIEWABLE_MARGIN_RIGHT;
|
|
*outRight = VIEWABLE_MARGIN_BOTTOM;
|
|
*outBottom = VIEWABLE_MARGIN_LEFT;
|
|
*outLeft = VIEWABLE_MARGIN_TOP;
|
|
break;
|
|
}
|
|
}
|