You've already forked crosspoint-reader
mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-04-29 10:26:52 -07:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c2f64cf87 | |||
| 3d0fcd297d | |||
| 5c12f2f01e | |||
| 8d6b35b8e7 | |||
| 83cd96bc2f | |||
| 14ec53a335 | |||
| 5ba85290ab | |||
| 104f391a29 | |||
| b3b43bb373 | |||
| 5349e81723 | |||
| 825ef56ad8 | |||
| ed0811c898 | |||
| d29b8ee2f9 | |||
| b898d53f7b | |||
| c656673b9a | |||
| 1398aeb1ed | |||
| 6cd19f5619 | |||
| cff3e12a0a | |||
| fa3c7d96a0 | |||
| f429f9035c | |||
| 11984f8fef | |||
| 1c13331189 | |||
| 9b3885135f |
@@ -15,10 +15,10 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t cursorXFP = fp4::fromPixel(startX); // 12.4 fixed-point accumulator
|
||||
int lastBaseX = startX;
|
||||
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
||||
int lastBaseTop = 0;
|
||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
@@ -31,7 +31,9 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
|
||||
const EpdGlyph* glyph = getGlyph(cp);
|
||||
if (!glyph) {
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP); // flush pending advance before resetting
|
||||
prevCp = 0;
|
||||
prevAdvanceFP = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -44,11 +46,11 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
}
|
||||
|
||||
if (!isCombining && prevCp != 0) {
|
||||
cursorXFP += getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
const auto kernFP = getKerning(prevCp, cp); // 4.4 fixed-point kern
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP);
|
||||
}
|
||||
|
||||
const int cursorXPixels = fp4::toPixel(cursorXFP); // snap 12.4 fixed-point to nearest pixel
|
||||
const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : cursorXPixels;
|
||||
const int glyphBaseX = isCombining ? (lastBaseX + fp4::toPixel(lastBaseAdvanceFP / 2)) : lastBaseX;
|
||||
const int glyphBaseY = startY - raiseBy;
|
||||
|
||||
*minX = std::min(*minX, glyphBaseX + glyph->left);
|
||||
@@ -57,10 +59,9 @@ void EpdFont::getTextBounds(const char* string, const int startX, const int star
|
||||
*maxY = std::max(*maxY, glyphBaseY + glyph->top);
|
||||
|
||||
if (!isCombining) {
|
||||
lastBaseX = cursorXPixels;
|
||||
lastBaseAdvanceFP = glyph->advanceX; // 12.4 fixed-point
|
||||
lastBaseTop = glyph->top;
|
||||
cursorXFP += glyph->advanceX; // 12.4 fixed-point advance
|
||||
prevAdvanceFP = lastBaseAdvanceFP;
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,12 @@
|
||||
/// Font metrics use "fixed-point 4" (4 fractional bits, i.e. 1/16-pixel
|
||||
/// resolution). Both the 12.4 glyph advances (uint16_t) and the 4.4 kern
|
||||
/// values (int8_t) share the same 4 fractional bits, so they can be freely
|
||||
/// added into a single int32_t accumulator during text layout. The
|
||||
/// accumulator is snapped to the nearest whole pixel only at render time,
|
||||
/// which avoids the per-character rounding errors that plagued integer-only
|
||||
/// layout.
|
||||
/// added before snapping to whole pixels.
|
||||
///
|
||||
/// Rendering and measurement use "differential rounding": each glyph step
|
||||
/// (previous advance + current kern) is combined in fixed-point and snapped
|
||||
/// to a pixel as one unit. This guarantees identical character pairs always
|
||||
/// produce the same pixel spacing, regardless of position on the line.
|
||||
///
|
||||
/// The helpers below eliminate the raw bit-shifts that would otherwise be
|
||||
/// scattered across every layout / measurement call site.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 18;
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 19;
|
||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
||||
sizeof(uint8_t) + sizeof(uint32_t) + sizeof(uint32_t);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
struct DirectPixelWriter {
|
||||
uint8_t* fb;
|
||||
GfxRenderer::RenderMode mode;
|
||||
uint16_t displayWidthBytes; // Runtime framebuffer stride (X4: 100, X3: 99)
|
||||
|
||||
// Orientation is collapsed into a linear transform:
|
||||
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
|
||||
@@ -29,29 +30,33 @@ struct DirectPixelWriter {
|
||||
void init(GfxRenderer& renderer) {
|
||||
fb = renderer.getFrameBuffer();
|
||||
mode = renderer.getRenderMode();
|
||||
displayWidthBytes = renderer.getDisplayWidthBytes();
|
||||
|
||||
const int phyW = renderer.getDisplayWidth();
|
||||
const int phyH = renderer.getDisplayHeight();
|
||||
|
||||
switch (renderer.getOrientation()) {
|
||||
case GfxRenderer::Portrait:
|
||||
// phyX = y, phyY = (DISPLAY_HEIGHT-1) - x
|
||||
// phyX = y, phyY = (phyH-1) - x
|
||||
phyXBase = 0;
|
||||
phyYBase = HalDisplay::DISPLAY_HEIGHT - 1;
|
||||
phyYBase = phyH - 1;
|
||||
phyXStepX = 0;
|
||||
phyYStepX = -1;
|
||||
phyXStepY = 1;
|
||||
phyYStepY = 0;
|
||||
break;
|
||||
case GfxRenderer::LandscapeClockwise:
|
||||
// phyX = (DISPLAY_WIDTH-1) - x, phyY = (DISPLAY_HEIGHT-1) - y
|
||||
phyXBase = HalDisplay::DISPLAY_WIDTH - 1;
|
||||
phyYBase = HalDisplay::DISPLAY_HEIGHT - 1;
|
||||
// phyX = (phyW-1) - x, phyY = (phyH-1) - y
|
||||
phyXBase = phyW - 1;
|
||||
phyYBase = phyH - 1;
|
||||
phyXStepX = -1;
|
||||
phyYStepX = 0;
|
||||
phyXStepY = 0;
|
||||
phyYStepY = -1;
|
||||
break;
|
||||
case GfxRenderer::PortraitInverted:
|
||||
// phyX = (DISPLAY_WIDTH-1) - y, phyY = x
|
||||
phyXBase = HalDisplay::DISPLAY_WIDTH - 1;
|
||||
// phyX = (phyW-1) - y, phyY = x
|
||||
phyXBase = phyW - 1;
|
||||
phyYBase = 0;
|
||||
phyXStepX = 0;
|
||||
phyYStepX = 1;
|
||||
@@ -115,7 +120,7 @@ struct DirectPixelWriter {
|
||||
const int phyX = rowPhyXBase + logicalX * phyXStepX;
|
||||
const int phyY = rowPhyYBase + logicalX * phyYStepX;
|
||||
|
||||
const uint16_t byteIndex = phyY * HalDisplay::DISPLAY_WIDTH_BYTES + (phyX >> 3);
|
||||
const uint16_t byteIndex = phyY * displayWidthBytes + (phyX >> 3);
|
||||
const uint8_t bitMask = 1 << (7 - (phyX & 7));
|
||||
|
||||
if (state) {
|
||||
|
||||
@@ -37,4 +37,4 @@ class ImageToFramebufferDecoder {
|
||||
|
||||
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -19,38 +19,25 @@ namespace {
|
||||
// The draw callback receives this via pDraw->pUser (set by setUserPointer()).
|
||||
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by jpegOpen()).
|
||||
struct JpegContext {
|
||||
GfxRenderer* renderer;
|
||||
const RenderConfig* config;
|
||||
int screenWidth;
|
||||
int screenHeight;
|
||||
GfxRenderer* renderer{nullptr};
|
||||
const RenderConfig* config{nullptr};
|
||||
int screenWidth{0};
|
||||
int screenHeight{0};
|
||||
|
||||
// Source dimensions after JPEGDEC's built-in scaling
|
||||
int scaledSrcWidth;
|
||||
int scaledSrcHeight;
|
||||
int scaledSrcWidth{0};
|
||||
int scaledSrcHeight{0};
|
||||
|
||||
// Final output dimensions
|
||||
int dstWidth;
|
||||
int dstHeight;
|
||||
int dstWidth{0};
|
||||
int dstHeight{0};
|
||||
|
||||
// Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU)
|
||||
int32_t fineScaleFP; // src -> dst mapping
|
||||
int32_t invScaleFP; // dst -> src mapping
|
||||
int32_t fineScaleFP{1 << 16}; // src -> dst mapping
|
||||
int32_t invScaleFP{1 << 16}; // dst -> src mapping
|
||||
|
||||
PixelCache cache;
|
||||
bool caching;
|
||||
|
||||
JpegContext()
|
||||
: renderer(nullptr),
|
||||
config(nullptr),
|
||||
screenWidth(0),
|
||||
screenHeight(0),
|
||||
scaledSrcWidth(0),
|
||||
scaledSrcHeight(0),
|
||||
dstWidth(0),
|
||||
dstHeight(0),
|
||||
fineScaleFP(1 << 16),
|
||||
invScaleFP(1 << 16),
|
||||
caching(false) {}
|
||||
bool caching{false};
|
||||
};
|
||||
|
||||
// File I/O callbacks use pFile->fHandle to access the FsFile*,
|
||||
|
||||
@@ -19,37 +19,23 @@ namespace {
|
||||
// The draw callback receives this via pDraw->pUser (set by png.decode()).
|
||||
// The file I/O callbacks receive the FsFile* via pFile->fHandle (set by pngOpen()).
|
||||
struct PngContext {
|
||||
GfxRenderer* renderer;
|
||||
const RenderConfig* config;
|
||||
int screenWidth;
|
||||
int screenHeight;
|
||||
GfxRenderer* renderer{nullptr};
|
||||
const RenderConfig* config{nullptr};
|
||||
int screenWidth{0};
|
||||
int screenHeight{0};
|
||||
|
||||
// Scaling state
|
||||
float scale;
|
||||
int srcWidth;
|
||||
int srcHeight;
|
||||
int dstWidth;
|
||||
int dstHeight;
|
||||
int lastDstY; // Track last rendered destination Y to avoid duplicates
|
||||
float scale{1.f};
|
||||
int srcWidth{0};
|
||||
int srcHeight{0};
|
||||
int dstWidth{0};
|
||||
int dstHeight{0};
|
||||
int lastDstY{-1}; // Track last rendered destination Y to avoid duplicates
|
||||
|
||||
PixelCache cache;
|
||||
bool caching;
|
||||
bool caching{false};
|
||||
|
||||
uint8_t* grayLineBuffer;
|
||||
|
||||
PngContext()
|
||||
: renderer(nullptr),
|
||||
config(nullptr),
|
||||
screenWidth(0),
|
||||
screenHeight(0),
|
||||
scale(1.0f),
|
||||
srcWidth(0),
|
||||
srcHeight(0),
|
||||
dstWidth(0),
|
||||
dstHeight(0),
|
||||
lastDstY(-1),
|
||||
caching(false),
|
||||
grayLineBuffer(nullptr) {}
|
||||
uint8_t* grayLineBuffer{nullptr};
|
||||
};
|
||||
|
||||
// File I/O callbacks use pFile->fHandle to access the FsFile*,
|
||||
|
||||
@@ -12,11 +12,24 @@ const LanguageHyphenator* Hyphenator::cachedHyphenator_ = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
// Maps a BCP-47 language tag to a language-specific hyphenator.
|
||||
// Normalize ISO 639-2 (three-letter) codes to ISO 639-1 (two-letter) codes used by the
|
||||
// hyphenation registry. EPUBs may use either form in their dc:language metadata (e.g.
|
||||
// "eng" instead of "en"). Both the bibliographic ("fre"/"ger") and terminological
|
||||
// ("fra"/"deu") ISO 639-2 variants are mapped.
|
||||
struct Iso639Mapping {
|
||||
const char* iso639_2;
|
||||
const char* iso639_1;
|
||||
};
|
||||
static constexpr Iso639Mapping kIso639Mappings[] = {
|
||||
{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"}, {"ger", "de"},
|
||||
{"rus", "ru"}, {"spa", "es"}, {"ita", "it"}, {"ukr", "uk"},
|
||||
};
|
||||
|
||||
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
|
||||
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||
if (langTag.empty()) return nullptr;
|
||||
|
||||
// Extract primary subtag and normalize to lowercase (e.g., "en-US" -> "en").
|
||||
// Extract primary subtag and normalize to lowercase (e.g., "en-US" -> "en", "ENG" -> "en").
|
||||
std::string primary;
|
||||
primary.reserve(langTag.size());
|
||||
for (char c : langTag) {
|
||||
@@ -26,6 +39,14 @@ const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||
}
|
||||
if (primary.empty()) return nullptr;
|
||||
|
||||
// Normalize ISO 639-2 three-letter codes to two-letter equivalents.
|
||||
for (const auto& mapping : kIso639Mappings) {
|
||||
if (primary == mapping.iso639_2) {
|
||||
primary = mapping.iso639_1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return getLanguageHyphenatorForPrimaryTag(primary);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,4 +78,12 @@ bool hasTxtExtension(std::string_view fileName) { return checkFileExtension(file
|
||||
|
||||
bool hasMarkdownExtension(std::string_view fileName) { return checkFileExtension(fileName, ".md"); }
|
||||
|
||||
std::string extractFolderPath(const std::string& filePath) {
|
||||
const auto lastSlash = filePath.find_last_of('/');
|
||||
if (lastSlash == std::string::npos || lastSlash == 0) {
|
||||
return "/";
|
||||
}
|
||||
return filePath.substr(0, lastSlash);
|
||||
}
|
||||
|
||||
} // namespace FsHelpers
|
||||
|
||||
@@ -55,4 +55,6 @@ inline bool hasTxtExtension(const String& fileName) {
|
||||
// Check for .md extension (case-insensitive)
|
||||
bool hasMarkdownExtension(std::string_view fileName);
|
||||
|
||||
std::string extractFolderPath(const std::string& filePath);
|
||||
|
||||
} // namespace FsHelpers
|
||||
|
||||
@@ -108,7 +108,7 @@ uint8_t quantize1bit(int gray, int x, int y) {
|
||||
return (gray >= adjustedThreshold) ? 1 : 0;
|
||||
}
|
||||
|
||||
void createBmpHeader(BmpHeader* bmpHeader, int width, int height) {
|
||||
void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder) {
|
||||
if (!bmpHeader) return;
|
||||
|
||||
// Zero out the memory to ensure no garbage data if called on uninitialized stack memory
|
||||
@@ -126,15 +126,15 @@ void createBmpHeader(BmpHeader* bmpHeader, int width, int height) {
|
||||
|
||||
bmpHeader->infoHeader.biSize = sizeof(bmpHeader->infoHeader);
|
||||
bmpHeader->infoHeader.biWidth = width;
|
||||
bmpHeader->infoHeader.biHeight = height;
|
||||
bmpHeader->infoHeader.biHeight = (rowOrder == BmpRowOrder::TopDown) ? -height : height;
|
||||
bmpHeader->infoHeader.biPlanes = 1;
|
||||
bmpHeader->infoHeader.biBitCount = 1;
|
||||
bmpHeader->infoHeader.biCompression = 0;
|
||||
bmpHeader->infoHeader.biSizeImage = imageSize;
|
||||
bmpHeader->infoHeader.biXPelsPerMeter = 0;
|
||||
bmpHeader->infoHeader.biYPelsPerMeter = 0;
|
||||
bmpHeader->infoHeader.biClrUsed = 0;
|
||||
bmpHeader->infoHeader.biClrImportant = 0;
|
||||
bmpHeader->infoHeader.biXPelsPerMeter = 2835; // 72 DPI
|
||||
bmpHeader->infoHeader.biYPelsPerMeter = 2835; // 72 DPI
|
||||
bmpHeader->infoHeader.biClrUsed = 2;
|
||||
bmpHeader->infoHeader.biClrImportant = 2;
|
||||
|
||||
// Color 0 (black)
|
||||
bmpHeader->colors[0].rgbBlue = 0;
|
||||
|
||||
@@ -11,8 +11,10 @@ uint8_t quantizeSimple(int gray);
|
||||
uint8_t quantize1bit(int gray, int x, int y);
|
||||
int adjustPixel(int gray);
|
||||
|
||||
enum class BmpRowOrder { BottomUp, TopDown };
|
||||
|
||||
// Populates a 1-bit BMP header in the provided memory.
|
||||
void createBmpHeader(BmpHeader* bmpHeader, int width, int height);
|
||||
void createBmpHeader(BmpHeader* bmpHeader, int width, int height, BmpRowOrder rowOrder);
|
||||
|
||||
// 1-bit Atkinson dithering - better quality than noise dithering for thumbnails
|
||||
// Error distribution pattern (same as 2-bit but quantizes to 2 levels):
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GfxRenderer.h"
|
||||
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <Logging.h>
|
||||
#include <Utf8.h>
|
||||
|
||||
@@ -28,6 +29,11 @@ void GfxRenderer::begin() {
|
||||
LOG_ERR("GFX", "!! No framebuffer");
|
||||
assert(false);
|
||||
}
|
||||
panelWidth = display.getDisplayWidth();
|
||||
panelHeight = display.getDisplayHeight();
|
||||
panelWidthBytes = display.getDisplayWidthBytes();
|
||||
frameBufferSize = display.getBufferSize();
|
||||
bwBufferChunks.assign((frameBufferSize + BW_BUFFER_CHUNK_SIZE - 1) / BW_BUFFER_CHUNK_SIZE, nullptr);
|
||||
}
|
||||
|
||||
void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.insert({fontId, font}); }
|
||||
@@ -35,25 +41,25 @@ void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) { fontMap.ins
|
||||
// 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) {
|
||||
int* phyY, const uint16_t panelWidth, const uint16_t panelHeight) {
|
||||
switch (orientation) {
|
||||
case GfxRenderer::Portrait: {
|
||||
// Logical portrait (480x800) → panel (800x480)
|
||||
// Rotation: 90 degrees clockwise
|
||||
*phyX = y;
|
||||
*phyY = HalDisplay::DISPLAY_HEIGHT - 1 - x;
|
||||
*phyY = panelHeight - 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;
|
||||
*phyX = panelWidth - 1 - x;
|
||||
*phyY = panelHeight - 1 - y;
|
||||
break;
|
||||
}
|
||||
case GfxRenderer::PortraitInverted: {
|
||||
// Logical portrait (480x800) → panel (800x480)
|
||||
// Rotation: 90 degrees counter-clockwise
|
||||
*phyX = HalDisplay::DISPLAY_WIDTH - 1 - y;
|
||||
*phyX = panelWidth - 1 - y;
|
||||
*phyY = x;
|
||||
break;
|
||||
}
|
||||
@@ -127,6 +133,7 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
||||
renderer.drawPixel(screenX, screenY, pixelState);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || bmpVal == 2)) {
|
||||
// Light gray (also mark the MSB if it's going to be a dark gray too)
|
||||
// Dedicated X3 gray LUTs now provide proper 4-level gray on both devices
|
||||
// We have to flag pixels in reverse for the gray buffers, as 0 leave alone, 1 update
|
||||
renderer.drawPixel(screenX, screenY, false);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_LSB && bmpVal == 1) {
|
||||
@@ -168,16 +175,16 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const {
|
||||
int phyY = 0;
|
||||
|
||||
// Note: this call should be inlined for better performance
|
||||
rotateCoordinates(orientation, x, y, &phyX, &phyY);
|
||||
rotateCoordinates(orientation, x, y, &phyX, &phyY, panelWidth, panelHeight);
|
||||
|
||||
// Bounds checking against physical panel dimensions
|
||||
if (phyX < 0 || phyX >= HalDisplay::DISPLAY_WIDTH || phyY < 0 || phyY >= HalDisplay::DISPLAY_HEIGHT) {
|
||||
// Bounds checking against runtime panel dimensions
|
||||
if (phyX < 0 || phyX >= panelWidth || phyY < 0 || phyY >= panelHeight) {
|
||||
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 uint32_t byteIndex = static_cast<uint32_t>(phyY) * panelWidthBytes + (phyX / 8);
|
||||
const uint8_t bitPosition = 7 - (phyX % 8); // MSB first
|
||||
|
||||
if (state) {
|
||||
@@ -208,10 +215,10 @@ void GfxRenderer::drawCenteredText(const int fontId, const int y, const char* te
|
||||
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);
|
||||
int32_t xPosFP = fp4::fromPixel(x); // 12.4 fixed-point accumulator
|
||||
int lastBaseX = x;
|
||||
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
||||
int lastBaseTop = 0;
|
||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||
|
||||
// cannot draw a NULL / empty string
|
||||
if (text == nullptr || *text == '\0') {
|
||||
@@ -251,19 +258,22 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
}
|
||||
|
||||
cp = font.applyLigatures(cp, text, style);
|
||||
const int kernFP = (prevCp != 0) ? font.getKerning(prevCp, cp, style) : 0; // 4.4 fixed-point kern
|
||||
xPosFP += kernFP;
|
||||
|
||||
lastBaseX = fp4::toPixel(xPosFP); // snap 12.4 fixed-point to nearest pixel
|
||||
// Differential rounding: snap (previous advance + current kern) as one unit so
|
||||
// identical character pairs always produce the same pixel step regardless of
|
||||
// where they fall on the line.
|
||||
if (prevCp != 0) {
|
||||
const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern
|
||||
lastBaseX += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel
|
||||
}
|
||||
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
|
||||
lastBaseAdvanceFP = glyph ? glyph->advanceX : 0;
|
||||
lastBaseTop = glyph ? glyph->top : 0;
|
||||
prevAdvanceFP = lastBaseAdvanceFP;
|
||||
|
||||
renderCharImpl<TextRotation::None>(*this, renderMode, font, cp, lastBaseX, yPos, black, style);
|
||||
if (glyph) {
|
||||
xPosFP += glyph->advanceX; // 12.4 fixed-point advance
|
||||
}
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
@@ -556,7 +566,7 @@ void GfxRenderer::fillRoundedRect(const int x, const int y, const int width, con
|
||||
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);
|
||||
rotateCoordinates(orientation, x, y, &rotatedX, &rotatedY, panelWidth, panelHeight);
|
||||
// Rotate origin corner
|
||||
switch (orientation) {
|
||||
case Portrait:
|
||||
@@ -596,12 +606,24 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
LOG_DBG("GFX", "Cropping %dx%d by %dx%d pix, is %s", bitmap.getWidth(), bitmap.getHeight(), cropPixX, cropPixY,
|
||||
bitmap.isTopDown() ? "top-down" : "bottom-up");
|
||||
|
||||
if (maxWidth > 0 && (1.0f - cropX) * bitmap.getWidth() > maxWidth) {
|
||||
scale = static_cast<float>(maxWidth) / static_cast<float>((1.0f - cropX) * bitmap.getWidth());
|
||||
isScaled = true;
|
||||
const float croppedWidth = (1.0f - cropX) * static_cast<float>(bitmap.getWidth());
|
||||
const float croppedHeight = (1.0f - cropY) * static_cast<float>(bitmap.getHeight());
|
||||
bool hasTargetBounds = false;
|
||||
float fitScale = 1.0f;
|
||||
|
||||
if (maxWidth > 0 && croppedWidth > 0.0f) {
|
||||
fitScale = static_cast<float>(maxWidth) / croppedWidth;
|
||||
hasTargetBounds = true;
|
||||
}
|
||||
if (maxHeight > 0 && (1.0f - cropY) * bitmap.getHeight() > maxHeight) {
|
||||
scale = std::min(scale, static_cast<float>(maxHeight) / static_cast<float>((1.0f - cropY) * bitmap.getHeight()));
|
||||
|
||||
if (maxHeight > 0 && croppedHeight > 0.0f) {
|
||||
const float heightScale = static_cast<float>(maxHeight) / croppedHeight;
|
||||
fitScale = hasTargetBounds ? std::min(fitScale, heightScale) : heightScale;
|
||||
hasTargetBounds = true;
|
||||
}
|
||||
|
||||
if (hasTargetBounds && fitScale < 1.0f) {
|
||||
scale = fitScale;
|
||||
isScaled = true;
|
||||
}
|
||||
LOG_DBG("GFX", "Scaling by %f - %s", scale, isScaled ? "scaled" : "not scaled");
|
||||
@@ -822,7 +844,7 @@ void GfxRenderer::clearScreen(const uint8_t color) const {
|
||||
}
|
||||
|
||||
void GfxRenderer::invertScreen() const {
|
||||
for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) {
|
||||
for (uint32_t i = 0; i < frameBufferSize; i++) {
|
||||
frameBuffer[i] = ~frameBuffer[i];
|
||||
}
|
||||
}
|
||||
@@ -923,13 +945,13 @@ int GfxRenderer::getScreenWidth() const {
|
||||
case Portrait:
|
||||
case PortraitInverted:
|
||||
// 480px wide in portrait logical coordinates
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
return panelHeight;
|
||||
case LandscapeClockwise:
|
||||
case LandscapeCounterClockwise:
|
||||
// 800px wide in landscape logical coordinates
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
return panelWidth;
|
||||
}
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
return panelHeight;
|
||||
}
|
||||
|
||||
int GfxRenderer::getScreenHeight() const {
|
||||
@@ -937,13 +959,13 @@ int GfxRenderer::getScreenHeight() const {
|
||||
case Portrait:
|
||||
case PortraitInverted:
|
||||
// 800px tall in portrait logical coordinates
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
return panelWidth;
|
||||
case LandscapeClockwise:
|
||||
case LandscapeCounterClockwise:
|
||||
// 480px tall in landscape logical coordinates
|
||||
return HalDisplay::DISPLAY_HEIGHT;
|
||||
return panelHeight;
|
||||
}
|
||||
return HalDisplay::DISPLAY_WIDTH;
|
||||
return panelWidth;
|
||||
}
|
||||
|
||||
int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style style) const {
|
||||
@@ -988,21 +1010,28 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
|
||||
uint32_t cp;
|
||||
uint32_t prevCp = 0;
|
||||
int32_t widthFP = 0; // 12.4 fixed-point accumulator
|
||||
int widthPx = 0;
|
||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||
const auto& font = fontIt->second;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&text)))) {
|
||||
if (utf8IsCombiningMark(cp)) {
|
||||
continue;
|
||||
}
|
||||
cp = font.applyLigatures(cp, text, style);
|
||||
|
||||
// Differential rounding: snap (previous advance + current kern) together,
|
||||
// matching drawText so measurement and rendering agree exactly.
|
||||
if (prevCp != 0) {
|
||||
widthFP += font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern
|
||||
const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern
|
||||
widthPx += fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel
|
||||
}
|
||||
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
if (glyph) widthFP += glyph->advanceX; // 12.4 fixed-point advance
|
||||
prevAdvanceFP = glyph ? glyph->advanceX : 0;
|
||||
prevCp = cp;
|
||||
}
|
||||
return fp4::toPixel(widthFP); // snap 12.4 fixed-point to nearest pixel
|
||||
widthPx += fp4::toPixel(prevAdvanceFP); // final glyph's advance
|
||||
return widthPx;
|
||||
}
|
||||
|
||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
@@ -1049,10 +1078,10 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
|
||||
const auto& font = fontIt->second;
|
||||
|
||||
int32_t yPosFP = fp4::fromPixel(y); // 12.4 fixed-point accumulator
|
||||
int lastBaseY = y;
|
||||
int lastBaseAdvanceFP = 0; // 12.4 fixed-point
|
||||
int lastBaseTop = 0;
|
||||
int32_t prevAdvanceFP = 0; // 12.4 fixed-point: prev glyph's advance + next kern for snap
|
||||
constexpr int MIN_COMBINING_GAP_PX = 1;
|
||||
|
||||
uint32_t cp;
|
||||
@@ -1075,27 +1104,28 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
}
|
||||
|
||||
cp = font.applyLigatures(cp, text, style);
|
||||
|
||||
// Differential rounding: snap (previous advance + current kern) as one unit,
|
||||
// subtracting for the rotated coordinate direction.
|
||||
if (prevCp != 0) {
|
||||
yPosFP -= font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern (subtract for rotated)
|
||||
const auto kernFP = font.getKerning(prevCp, cp, style); // 4.4 fixed-point kern
|
||||
lastBaseY -= fp4::toPixel(prevAdvanceFP + kernFP); // snap 12.4 fixed-point to nearest pixel
|
||||
}
|
||||
|
||||
lastBaseY = fp4::toPixel(yPosFP); // snap 12.4 fixed-point to nearest pixel
|
||||
const EpdGlyph* glyph = font.getGlyph(cp, style);
|
||||
|
||||
lastBaseAdvanceFP = glyph ? glyph->advanceX : 0; // 12.4 fixed-point
|
||||
lastBaseTop = glyph ? glyph->top : 0;
|
||||
prevAdvanceFP = lastBaseAdvanceFP;
|
||||
|
||||
renderCharImpl<TextRotation::Rotated90CW>(*this, renderMode, font, cp, x, lastBaseY, black, style);
|
||||
if (glyph) {
|
||||
yPosFP -= glyph->advanceX; // 12.4 fixed-point advance (subtract for rotated)
|
||||
}
|
||||
prevCp = cp;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t* GfxRenderer::getFrameBuffer() const { return frameBuffer; }
|
||||
|
||||
size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; }
|
||||
size_t GfxRenderer::getBufferSize() const { return frameBufferSize; }
|
||||
|
||||
// unused
|
||||
// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); }
|
||||
@@ -1123,7 +1153,7 @@ void GfxRenderer::freeBwBufferChunks() {
|
||||
*/
|
||||
bool GfxRenderer::storeBwBuffer() {
|
||||
// Allocate and copy each chunk
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
for (size_t i = 0; i < bwBufferChunks.size(); 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);
|
||||
@@ -1132,19 +1162,20 @@ bool GfxRenderer::storeBwBuffer() {
|
||||
}
|
||||
|
||||
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
||||
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(BW_BUFFER_CHUNK_SIZE));
|
||||
const size_t chunkSize = std::min(BW_BUFFER_CHUNK_SIZE, static_cast<size_t>(frameBufferSize - offset));
|
||||
bwBufferChunks[i] = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
|
||||
if (!bwBufferChunks[i]) {
|
||||
LOG_ERR("GFX", "!! Failed to allocate BW buffer chunk %zu (%zu bytes)", i, BW_BUFFER_CHUNK_SIZE);
|
||||
LOG_ERR("GFX", "!! Failed to allocate BW buffer chunk %zu (%zu bytes)", i, chunkSize);
|
||||
// Free previously allocated chunks
|
||||
freeBwBufferChunks();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(bwBufferChunks[i], frameBuffer + offset, BW_BUFFER_CHUNK_SIZE);
|
||||
memcpy(bwBufferChunks[i], frameBuffer + offset, chunkSize);
|
||||
}
|
||||
|
||||
LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", BW_BUFFER_NUM_CHUNKS, BW_BUFFER_CHUNK_SIZE);
|
||||
LOG_DBG("GFX", "Stored BW buffer in %zu chunks (%zu bytes each)", bwBufferChunks.size(), BW_BUFFER_CHUNK_SIZE);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1168,9 +1199,10 @@ void GfxRenderer::restoreBwBuffer() {
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < BW_BUFFER_NUM_CHUNKS; i++) {
|
||||
for (size_t i = 0; i < bwBufferChunks.size(); i++) {
|
||||
const size_t offset = i * BW_BUFFER_CHUNK_SIZE;
|
||||
memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE);
|
||||
const size_t chunkSize = std::min(BW_BUFFER_CHUNK_SIZE, static_cast<size_t>(frameBufferSize - offset));
|
||||
memcpy(frameBuffer + offset, bwBufferChunks[i], chunkSize);
|
||||
}
|
||||
|
||||
display.cleanupGrayscaleBuffers(frameBuffer);
|
||||
|
||||
@@ -30,16 +30,17 @@ class GfxRenderer {
|
||||
|
||||
private:
|
||||
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory
|
||||
static constexpr size_t BW_BUFFER_NUM_CHUNKS = HalDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE;
|
||||
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == HalDisplay::BUFFER_SIZE,
|
||||
"BW buffer chunking does not line up with display buffer size");
|
||||
|
||||
HalDisplay& display;
|
||||
RenderMode renderMode;
|
||||
Orientation orientation;
|
||||
bool fadingFix;
|
||||
uint8_t* frameBuffer = nullptr;
|
||||
uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
|
||||
uint16_t panelWidth = HalDisplay::DISPLAY_WIDTH;
|
||||
uint16_t panelHeight = HalDisplay::DISPLAY_HEIGHT;
|
||||
uint16_t panelWidthBytes = HalDisplay::DISPLAY_WIDTH_BYTES;
|
||||
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
|
||||
std::vector<uint8_t*> bwBufferChunks;
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
|
||||
// Mutable because drawText() is const but needs to delegate scan-mode
|
||||
@@ -155,5 +156,8 @@ class GfxRenderer {
|
||||
|
||||
// Low level functions
|
||||
uint8_t* getFrameBuffer() const;
|
||||
static size_t getBufferSize();
|
||||
size_t getBufferSize() const;
|
||||
uint16_t getDisplayWidth() const { return panelWidth; }
|
||||
uint16_t getDisplayHeight() const { return panelHeight; }
|
||||
uint16_t getDisplayWidthBytes() const { return panelWidthBytes; }
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Адкрыйце гэты адрас у браўзеры"
|
||||
STR_OR_HTTP_PREFIX: "або http://"
|
||||
STR_SCAN_QR_HINT: "або адсканіруйце QR-код:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre па Wi-Fi"
|
||||
STR_CALIBRE_WEB_URL: "Вэб-адрас Calibre"
|
||||
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||
STR_NETWORK_LEGEND: "* = Абаронена | + = Захавана"
|
||||
STR_MAC_ADDRESS: "MAC-адрас:"
|
||||
STR_CHECKING_WIFI: "Праверка Wi-Fi..."
|
||||
|
||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Obriu aquest URL al navegador"
|
||||
STR_OR_HTTP_PREFIX: "o http://"
|
||||
STR_SCAN_QR_HINT: "o escanegeu el codi QR amb el telèfon:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre sense fils"
|
||||
STR_CALIBRE_WEB_URL: "URL web del Calibre"
|
||||
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||
STR_NETWORK_LEGEND: "* = Encriptat | + = Desat"
|
||||
STR_MAC_ADDRESS: "Adreça MAC:"
|
||||
STR_CHECKING_WIFI: "S'està comprovant el WiFi..."
|
||||
|
||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Otevřete tuto URL ve svém prohlížeči"
|
||||
STR_OR_HTTP_PREFIX: "nebo http://"
|
||||
STR_SCAN_QR_HINT: "nebo naskenujte QR kód telefonem:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_CALIBRE_WEB_URL: "URL webu Calibre"
|
||||
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||
STR_NETWORK_LEGEND: "* = Šifrováno | + = Uloženo"
|
||||
STR_MAC_ADDRESS: "MAC adresa:"
|
||||
STR_CHECKING_WIFI: "Kontrola WiFi..."
|
||||
|
||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Åbn denne URL i din browser"
|
||||
STR_OR_HTTP_PREFIX: "eller http://"
|
||||
STR_SCAN_QR_HINT: "eller scan QR-kode med din telefon:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
||||
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||
STR_NETWORK_LEGEND: "* = Krypteret | + = Gemt"
|
||||
STR_MAC_ADDRESS: "MAC-adresse:"
|
||||
STR_CHECKING_WIFI: "Tjekker WiFi..."
|
||||
|
||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open deze URL in je browser"
|
||||
STR_OR_HTTP_PREFIX: "of http://"
|
||||
STR_SCAN_QR_HINT: "of scan de QR-code met je telefoon:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
||||
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||
STR_NETWORK_LEGEND: "* = Beveiligd | + = Opgeslagen"
|
||||
STR_MAC_ADDRESS: "MAC-adres:"
|
||||
STR_CHECKING_WIFI: "Wifi controleren..."
|
||||
|
||||
@@ -45,7 +45,7 @@ STR_OPEN_URL_HINT: "Open this URL in your browser"
|
||||
STR_OR_HTTP_PREFIX: "or http://"
|
||||
STR_SCAN_QR_HINT: "or scan QR code with your phone:"
|
||||
STR_CALIBRE_WIRELESS: "Calibre Wireless"
|
||||
STR_CALIBRE_WEB_URL: "Calibre Web URL"
|
||||
STR_CALIBRE_WEB_URL: "OPDS URL"
|
||||
STR_NETWORK_LEGEND: "* = Encrypted | + = Saved"
|
||||
STR_MAC_ADDRESS: "MAC address:"
|
||||
STR_CHECKING_WIFI: "Checking WiFi..."
|
||||
@@ -175,6 +175,8 @@ STR_UNNAMED: "Unnamed"
|
||||
STR_NO_SERVER_URL: "No server URL configured"
|
||||
STR_FETCH_FEED_FAILED: "Failed to fetch feed"
|
||||
STR_PARSE_FEED_FAILED: "Failed to parse feed"
|
||||
STR_NEXT_PAGE: "Next Page »"
|
||||
STR_PREV_PAGE: "« Previous Page"
|
||||
STR_NETWORK_PREFIX: "Network: "
|
||||
STR_IP_ADDRESS_PREFIX: "IP Address: "
|
||||
STR_ERROR_GENERAL_FAILURE: "Error: General failure"
|
||||
@@ -229,6 +231,7 @@ STR_THEME_LYRA_EXTENDED: "Lyra Extended"
|
||||
STR_SUNLIGHT_FADING_FIX: "Sunlight Fading Fix"
|
||||
STR_REMAP_FRONT_BUTTONS: "Remap Front Buttons"
|
||||
STR_OPDS_BROWSER: "OPDS Browser"
|
||||
STR_SEARCH: "Search"
|
||||
STR_COVER_CUSTOM: "Cover + Custom"
|
||||
STR_MENU_RECENT_BOOKS: "Recent Books"
|
||||
STR_NO_RECENT_BOOKS: "No recent books"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user