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
22 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 |
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -339,29 +339,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
const bool hasCssHeight = imgStyle.hasImageHeight();
|
||||
const bool hasCssWidth = imgStyle.hasImageWidth();
|
||||
|
||||
// Compute effective container width for percentage-based image sizes.
|
||||
// If the image is inside a block with horizontal margins/padding (e.g.
|
||||
// <div style="margin: 1em 40%">), percentage widths like width:100%
|
||||
// should resolve against the container width, not the full viewport.
|
||||
int containerWidth = self->viewportWidth;
|
||||
if (self->currentTextBlock) {
|
||||
const int inset = self->currentTextBlock->getBlockStyle().totalHorizontalInset();
|
||||
if (inset > 0 && inset < self->viewportWidth) {
|
||||
containerWidth = self->viewportWidth - inset;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCssHeight && hasCssWidth && dims.width > 0 && dims.height > 0) {
|
||||
// Both CSS height and width set: resolve both, then clamp to viewport preserving requested ratio
|
||||
displayHeight = static_cast<int>(
|
||||
imgStyle.imageHeight.toPixels(emSize, static_cast<float>(self->viewportHeight)) + 0.5f);
|
||||
displayWidth =
|
||||
static_cast<int>(imgStyle.imageWidth.toPixels(emSize, static_cast<float>(containerWidth)) + 0.5f);
|
||||
displayWidth = static_cast<int>(
|
||||
imgStyle.imageWidth.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
|
||||
if (displayHeight < 1) displayHeight = 1;
|
||||
if (displayWidth < 1) displayWidth = 1;
|
||||
if (displayWidth > containerWidth || displayHeight > self->viewportHeight) {
|
||||
float scaleX =
|
||||
(displayWidth > containerWidth) ? static_cast<float>(containerWidth) / displayWidth : 1.0f;
|
||||
if (displayWidth > self->viewportWidth || displayHeight > self->viewportHeight) {
|
||||
float scaleX = (displayWidth > self->viewportWidth)
|
||||
? static_cast<float>(self->viewportWidth) / displayWidth
|
||||
: 1.0f;
|
||||
float scaleY = (displayHeight > self->viewportHeight)
|
||||
? static_cast<float>(self->viewportHeight) / displayHeight
|
||||
: 1.0f;
|
||||
@@ -386,8 +375,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
static_cast<int>(displayHeight * (static_cast<float>(dims.width) / dims.height) + 0.5f);
|
||||
if (displayWidth < 1) displayWidth = 1;
|
||||
}
|
||||
if (displayWidth > containerWidth) {
|
||||
displayWidth = containerWidth;
|
||||
if (displayWidth > self->viewportWidth) {
|
||||
displayWidth = self->viewportWidth;
|
||||
// Rescale height to preserve aspect ratio when width is clamped
|
||||
displayHeight =
|
||||
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
|
||||
@@ -396,10 +385,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
if (displayWidth < 1) displayWidth = 1;
|
||||
LOG_DBG("EHP", "Display size from CSS height: %dx%d", displayWidth, displayHeight);
|
||||
} else if (hasCssWidth && !hasCssHeight && dims.width > 0 && dims.height > 0) {
|
||||
// Use CSS width (resolve % against container width) and derive height from aspect ratio
|
||||
displayWidth =
|
||||
static_cast<int>(imgStyle.imageWidth.toPixels(emSize, static_cast<float>(containerWidth)) + 0.5f);
|
||||
if (displayWidth > containerWidth) displayWidth = containerWidth;
|
||||
// Use CSS width (resolve % against viewport width) and derive height from aspect ratio
|
||||
displayWidth = static_cast<int>(
|
||||
imgStyle.imageWidth.toPixels(emSize, static_cast<float>(self->viewportWidth)) + 0.5f);
|
||||
if (displayWidth > self->viewportWidth) displayWidth = self->viewportWidth;
|
||||
if (displayWidth < 1) displayWidth = 1;
|
||||
displayHeight =
|
||||
static_cast<int>(displayWidth * (static_cast<float>(dims.height) / dims.width) + 0.5f);
|
||||
@@ -413,8 +402,8 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
if (displayHeight < 1) displayHeight = 1;
|
||||
LOG_DBG("EHP", "Display size from CSS width: %dx%d", displayWidth, displayHeight);
|
||||
} else {
|
||||
// Scale to fit container while maintaining aspect ratio
|
||||
int maxWidth = containerWidth;
|
||||
// Scale to fit viewport while maintaining aspect ratio
|
||||
int maxWidth = self->viewportWidth;
|
||||
int maxHeight = self->viewportHeight;
|
||||
float scaleX = (dims.width > maxWidth) ? (float)maxWidth / dims.width : 1.0f;
|
||||
float scaleY = (dims.height > maxHeight) ? (float)maxHeight / dims.height : 1.0f;
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -131,9 +131,9 @@ static void renderCharImpl(const GfxRenderer& renderer, GfxRenderer::RenderMode
|
||||
if (renderMode == GfxRenderer::BW && bmpVal < 3) {
|
||||
// Black (also paints over the grays in BW mode)
|
||||
renderer.drawPixel(screenX, screenY, pixelState);
|
||||
} else if (renderMode == GfxRenderer::GRAYSCALE_MSB && (bmpVal == 1 || (gpio.deviceIsX4() && bmpVal == 2))) {
|
||||
} 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)
|
||||
// X3 AA tuning: keep only the darker antialias level to avoid washed text
|
||||
// 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) {
|
||||
@@ -215,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') {
|
||||
@@ -258,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;
|
||||
}
|
||||
}
|
||||
@@ -683,7 +686,7 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
|
||||
if (renderMode == BW && val < 3) {
|
||||
drawPixel(screenX, screenY);
|
||||
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || (gpio.deviceIsX4() && val == 2))) {
|
||||
} else if (renderMode == GRAYSCALE_MSB && (val == 1 || val == 2)) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
} else if (renderMode == GRAYSCALE_LSB && val == 1) {
|
||||
drawPixel(screenX, screenY, false);
|
||||
@@ -1007,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 {
|
||||
@@ -1068,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;
|
||||
@@ -1094,20 +1104,21 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,4 +157,7 @@ class GfxRenderer {
|
||||
// Low level functions
|
||||
uint8_t* getFrameBuffer() const;
|
||||
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