mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
60
Commits
1.4.0
...
feat-touch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
757c0b6d9e | ||
|
|
2c8ef01894 | ||
|
|
cbaa498ccc | ||
|
|
ebebc6f202 | ||
|
|
970b2c6ca1 | ||
|
|
b6ce599b20 | ||
|
|
f54eab2725 | ||
|
|
0d4c9ab91b | ||
|
|
0daa9db243 | ||
|
|
a2f2eea79e | ||
|
|
86a9b9c4a2 | ||
|
|
ba44978ac4 | ||
|
|
555f76da88 | ||
|
|
0a57c0a5a7 | ||
|
|
a09aef0889 | ||
|
|
8626d69f46 | ||
|
|
fc89e57e69 | ||
|
|
487613b082 | ||
|
|
8d5b119644 | ||
|
|
7271c00d35 | ||
|
|
1bd718fad9 | ||
|
|
61cb4946d6 | ||
|
|
4e352b9894 | ||
|
|
4e156e1617 | ||
|
|
3e0ba4488b | ||
|
|
e42503f1a4 | ||
|
|
c812091a4a | ||
|
|
beb4e82c9c | ||
|
|
a242397fa0 | ||
|
|
1511ab35d5 | ||
|
|
57c389c0b6 | ||
|
|
06ff5aa44a | ||
|
|
526cf1b6f9 | ||
|
|
6648a980cb | ||
|
|
e567620003 | ||
|
|
0b575d1965 | ||
|
|
4f55444f67 | ||
|
|
4a17d21d80 | ||
|
|
1c63847631 | ||
|
|
4a5b45409f | ||
|
|
e103cdfd11 | ||
|
|
cef0d267b7 | ||
|
|
9f3dddabe6 | ||
|
|
350fc8472a | ||
|
|
79ef9d6a35 | ||
|
|
0036a220be | ||
|
|
8ad538c98b | ||
|
|
f1b5da25b0 | ||
|
|
d739827db2 | ||
|
|
9176e76f5a | ||
|
|
7294610894 | ||
|
|
a51098725d | ||
|
|
6acecd8ea6 | ||
|
|
9b7b9e9094 | ||
|
|
204aff6be6 | ||
|
|
28d8ad565f | ||
|
|
ed6690c0cc | ||
|
|
1bc154da8b | ||
|
|
c308520f9c | ||
|
|
38ad4f9036 |
No files matched your search
+4
-3
@@ -1,3 +1,4 @@
|
||||
[submodule "open-x4-sdk"]
|
||||
path = open-x4-sdk
|
||||
url = https://github.com/crosspoint-reader/community-sdk.git
|
||||
[submodule "freeink-sdk"]
|
||||
path = freeink-sdk
|
||||
url = https://github.com/Free-Ink/freeink-sdk.git
|
||||
branch = main
|
||||
+2
-2
@@ -7,7 +7,7 @@ Mission: Provide a lightweight, high-performance reading experience focused on E
|
||||
* Role: Senior Embedded Systems Engineer (ESP-IDF/Arduino-ESP32 specialized).
|
||||
* Primary Constraint: 380KB RAM is the hard ceiling. Stability is non-negotiable.
|
||||
* Evidence-Based Reasoning: Before proposing a change, you MUST cite the specific file path and line numbers that justify the modification.
|
||||
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the open-x4-sdk or official docs first.
|
||||
* Anti-Hallucination: Do not assume the existence of libraries or ESP-IDF functions. If you are unsure of an API's availability for the ESP32-C3 RISC-V target, check the freeink-sdk source or the FreeInk SDK docs (https://freeink.org/llms.txt for an LLM-readable index) first.
|
||||
* No Unfounded Claims: Do not claim performance gains or memory savings without explaining the technical mechanism (e.g., DRAM vs IRAM usage).
|
||||
* Resource Justification: You must justify any new heap allocation (new, malloc, std::vector) or explain why a stack/static alternative was rejected.
|
||||
* Verification: After suggesting a fix, instruct the user on how to verify it (e.g., monitoring heap via Serial or checking a specific cache file).
|
||||
@@ -127,7 +127,7 @@ These flags in `platformio.ini` fundamentally affect firmware behavior:
|
||||
* lib/hal/: Hardware Abstraction Layer (HalDisplay, HalGPIO, HalStorage)
|
||||
* lib/I18n/: Internationalization (translations in `translations/*.yaml`, generated string tables)
|
||||
* src/activities/: UI logic using the Activity Lifecycle (onEnter, loop, onExit)
|
||||
* open-x4-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
|
||||
* freeink-sdk/: Low-level SDK (EInkDisplay, InputManager, BatteryMonitor, SDCardManager)
|
||||
* .crosspoint/: SD-based binary cache for EPUB metadata and pre-rendered layout sections
|
||||
|
||||
### Hardware Abstraction Layer (HAL)
|
||||
|
||||
+37
-10
@@ -122,19 +122,43 @@ A **Wi-Fi signal strength indicator** (dBm) is displayed on-screen during joined
|
||||
|
||||
CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin.
|
||||
|
||||
1. Install the plugin in Calibre:
|
||||
|
||||
- Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
||||
|
||||
- Download the zip file.
|
||||
|
||||
- Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
||||
#### Installing the Plugin in Calibre
|
||||
|
||||
2. On the device: File Transfer -> Calibre Wireless, then join a network.
|
||||
If you don't already have the plugin installed:
|
||||
|
||||
3. Make sure your computer is on the same Wi-Fi network.
|
||||
1. Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin.
|
||||
2. Download the zip file.
|
||||
3. Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file.
|
||||
4. Restart Calibre.
|
||||
|
||||
4. In Calibre, click "Send to device" to transfer books.
|
||||
#### Configuring the CrossPoint Plugin in Calibre
|
||||
1. In Calibre select Preferences.
|
||||
2. In the Preferences dialog select Plugins.
|
||||
3. In Plugins search for "crosspoint".
|
||||
4. Click on "Customize plugin".
|
||||
5. Update the value for "Host" to match the IP for your device.
|
||||
6. Leave the other settings as they are.
|
||||
7. [optional] Modify the "Upload path" to point to a subfolder other than the root "/" folder. Enter this as a path relative to the root folder. Example: `/mybooks`
|
||||
8. Restart Calibre.
|
||||
|
||||
<img width="420" height="385" alt="Image" src="https://github.com/user-attachments/assets/01fc7e33-a9a7-48ba-9e26-2e68d1f9daec" />
|
||||
|
||||
#### Uploading Books
|
||||
|
||||
To upload a book using the CrossPoint plugin in Calibre:
|
||||
|
||||
1. On the device: File Transfer -> Calibre Wireless, then join a network.
|
||||
2. Select one or more books.
|
||||
3. Right-click on that selection.
|
||||
4. Select "Send to Device" > "Send to main memory"
|
||||
|
||||
The CrossPoint plugin will connect to your device, create a folder for the book's author in the root folder (or the folder you configured for the plugin), then copy the book into that folder.
|
||||
|
||||
<img width="783" height="310" alt="Image" src="https://github.com/user-attachments/assets/741b0909-2e1d-4f16-8af0-2c43fbda5ce6" />
|
||||
|
||||
#### Removing a Book
|
||||
|
||||
Books cannot be removed from your device through Calibre. Use the web interface instead.
|
||||
|
||||
### 3.6 Settings
|
||||
|
||||
@@ -150,6 +174,7 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- "Cover" - The book cover image (Note: this is experimental and may not work as expected)
|
||||
- "None" - A blank screen
|
||||
- "Cover + Custom" - The book cover image while actively reading, falls back to "Custom" behavior otherwise
|
||||
- "Quick resume" - The text of the last page read will be displayed on the sleep screen and a moon icon is shown on the edge of the screen. Waking up the device will return to the same page of the opened book. This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book.
|
||||
|
||||
- **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected:
|
||||
|
||||
@@ -162,6 +187,8 @@ The Settings screen allows you to configure the device's behavior. There are a f
|
||||
- "Contrast" - The image will be displayed as a black & white image without grayscale conversion
|
||||
- "Inverted" - The image will be inverted as in white & black and will be displayed without grayscale conversion
|
||||
|
||||
- **Quick Resume on Timeout**: Whether to enable the "Quick Resume" sleep screen when the device goes to sleep due to inactivity (System > Time to Sleep). This is useful for quickly resuming reading without waiting for the device to fully wake up and load the book. This overwrites the Sleep Screen Cover Mode when enabled.
|
||||
|
||||
- **Status Bar**: Configure the status bar displayed while reading:
|
||||
|
||||
- "None" - No status bar
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
.DESCRIPTION
|
||||
Formats all C/C++ source and header files in the repository, excluding
|
||||
generated, vendored, and build directories (open-x4-sdk, builtinFonts,
|
||||
generated, vendored, and build directories (freeink-sdk, builtinFonts,
|
||||
hyphenation tries, uzlib, .pio, *.generated.h).
|
||||
|
||||
The clang-format binary path is resolved once and cached in
|
||||
@@ -92,7 +92,7 @@ function Resolve-ClangFormat {
|
||||
$clangFormat = Resolve-ClangFormat
|
||||
|
||||
$exclude = @(
|
||||
'open-x4-sdk'
|
||||
'freeink-sdk'
|
||||
'lib\EpdFont\builtinFonts'
|
||||
'lib\Epub\Epub\hyphenation\generated'
|
||||
'lib\uzlib'
|
||||
|
||||
@@ -8,7 +8,7 @@ At a high level, it is firmware that uses an activity-driven application archite
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[open-x4-sdk]
|
||||
A[Hardware: ESP32-C3 + SD + E-ink + Buttons] --> B[freeink-sdk]
|
||||
B --> C[lib/hal wrappers]
|
||||
C --> D[src/main.cpp runtime loop]
|
||||
D --> E[Activities layer]
|
||||
@@ -195,10 +195,10 @@ When editing related source assets, regenerate via normal build steps/scripts.
|
||||
- `src/`: app orchestration, settings/state, and activity implementations
|
||||
- `src/network/`: web server and OTA/update networking
|
||||
- `src/components/`: theming and shared UI components
|
||||
- `lib/hal/`: hardware abstraction wrappers around open-x4-sdk
|
||||
- `lib/hal/`: hardware abstraction wrappers around freeink-sdk
|
||||
- `lib/Epub/`: EPUB parser, layout, CSS handling, and hyphenation
|
||||
- `lib/`: supporting libraries (fonts, text, filesystem helpers, etc.)
|
||||
- `open-x4-sdk/`: hardware SDK submodule (display, input, storage, battery)
|
||||
- `freeink-sdk/`: hardware SDK submodule (display, input, storage, battery). Docs: https://freeink.org/docs
|
||||
- `docs/`: user and technical documentation
|
||||
|
||||
## Embedded constraints that shape design
|
||||
|
||||
Submodule
+1
Submodule freeink-sdk added at 329a4bebef.
@@ -37,3 +37,5 @@
|
||||
#include <builtinFonts/ubuntu_10_regular.h>
|
||||
#include <builtinFonts/ubuntu_12_bold.h>
|
||||
#include <builtinFonts/ubuntu_12_regular.h>
|
||||
#include <builtinFonts/ubuntu_14_bold.h>
|
||||
#include <builtinFonts/ubuntu_14_regular.h>
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -94,6 +94,13 @@ ruby -rdigest -e 'puts [
|
||||
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
||||
))"
|
||||
|
||||
echo "#define UI_14_FONT_ID ($(
|
||||
ruby -rdigest -e 'puts [
|
||||
"./ubuntu_14_regular.h",
|
||||
"./ubuntu_14_bold.h",
|
||||
].map{|f| Digest::SHA256.hexdigest(File.read(f)).to_i(16) }.sum % (2 ** 32) - (2 ** 31)'
|
||||
))"
|
||||
|
||||
echo "#define SMALL_FONT_ID ($(
|
||||
ruby -rdigest -e 'puts [
|
||||
"./notosans_8_regular.h",
|
||||
|
||||
@@ -28,23 +28,42 @@ for size in ${NOTOSANS_FONT_SIZES[@]}; do
|
||||
done
|
||||
done
|
||||
|
||||
# Small UI chrome (button devices, uiScale 1.0). Rendered 1-bit: crisp at these
|
||||
# sizes and half the flash of 2-bit.
|
||||
UI_FONT_SIZES=(10 12)
|
||||
# Larger UI chrome substituted in on touch/high-density boards via the uiScale
|
||||
# remap (see src/main.cpp setupFonts). Rendered 2-bit so it stays smooth when
|
||||
# enlarged. Touch boards use uiScale 1.2, so UI_12 -> 14.4 -> 14 is the size the
|
||||
# remap actually lands on; add larger sizes here if a board adopts a higher scale.
|
||||
UI_FONT_SIZES_LARGE=(14)
|
||||
UI_FONT_STYLES=("Regular" "Bold")
|
||||
|
||||
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
|
||||
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs are
|
||||
# filled from it while every glyph Ubuntu already has stays unchanged (fontstack
|
||||
# is ordered by descending priority). NotoSansHebrew fills U+05D0-U+05EA so the
|
||||
# Hebrew UI translation renders in menus and settings.
|
||||
generate_ui_font() {
|
||||
local size="$1" style="$2" extra_flags="$3"
|
||||
local font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||
local font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
||||
local hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
||||
local viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
||||
local output_path="../builtinFonts/${font_name}.h"
|
||||
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
|
||||
--additional-intervals 0x05D0,0x05EA $extra_flags > $output_path
|
||||
echo "Generated $output_path"
|
||||
}
|
||||
|
||||
for size in ${UI_FONT_SIZES[@]}; do
|
||||
for style in ${UI_FONT_STYLES[@]}; do
|
||||
font_name="ubuntu_${size}_$(echo $style | tr '[:upper:]' '[:lower:]')"
|
||||
font_path="../builtinFonts/source/Ubuntu/Ubuntu-${style}.ttf"
|
||||
hebrew_path="../builtinFonts/source/NotoSansHebrew/NotoSansHebrew-${style}.ttf"
|
||||
# Ubuntu lacks the Latin Extended Additional block (U+1EA0-U+1EF9) used for
|
||||
# Vietnamese tone marks. Append a Vietnamese-only Ubuntu cut so those glyphs
|
||||
# are filled from it while every glyph Ubuntu already has stays unchanged
|
||||
# (fontstack is ordered by descending priority).
|
||||
viet_path="../builtinFonts/source/Ubuntu/Ubuntu-Vietnamese-${style}.ttf"
|
||||
output_path="../builtinFonts/${font_name}.h"
|
||||
python fontconvert.py $font_name $size $font_path $hebrew_path $viet_path \
|
||||
--additional-intervals 0x05D0,0x05EA > $output_path
|
||||
echo "Generated $output_path"
|
||||
generate_ui_font $size $style ""
|
||||
done
|
||||
done
|
||||
|
||||
for size in ${UI_FONT_SIZES_LARGE[@]}; do
|
||||
for style in ${UI_FONT_STYLES[@]}; do
|
||||
generate_ui_font $size $style "--2bit --compress"
|
||||
done
|
||||
done
|
||||
|
||||
|
||||
@@ -14,8 +14,47 @@ constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-com
|
||||
constexpr char bookBinFile[] = "/book.bin";
|
||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||
constexpr uint32_t MAX_CACHE_STRING_LEN = 4096;
|
||||
|
||||
bool readStringBounded(HalFile& file, std::string& out, const uint32_t maxLen = MAX_CACHE_STRING_LEN) {
|
||||
uint32_t len = 0;
|
||||
if (file.read(&len, sizeof(len)) != static_cast<int>(sizeof(len))) {
|
||||
return false;
|
||||
}
|
||||
if (len > maxLen || len > static_cast<uint32_t>(file.available())) {
|
||||
LOG_ERR("BMC", "Invalid cache string length: %lu (max=%lu available=%d)", static_cast<unsigned long>(len),
|
||||
static_cast<unsigned long>(maxLen), file.available());
|
||||
return false;
|
||||
}
|
||||
out.clear();
|
||||
if (len == 0) {
|
||||
return true;
|
||||
}
|
||||
out.resize(len);
|
||||
return file.read(out.data(), len) == static_cast<int>(len);
|
||||
}
|
||||
|
||||
class CacheIoLock {
|
||||
public:
|
||||
explicit CacheIoLock(SemaphoreHandle_t mutex) : mutex(mutex) {
|
||||
if (mutex) xSemaphoreTakeRecursive(mutex, portMAX_DELAY);
|
||||
}
|
||||
~CacheIoLock() {
|
||||
if (mutex) xSemaphoreGiveRecursive(mutex);
|
||||
}
|
||||
|
||||
private:
|
||||
SemaphoreHandle_t mutex;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
BookMetadataCache::~BookMetadataCache() {
|
||||
if (ioMutex) {
|
||||
vSemaphoreDelete(ioMutex);
|
||||
ioMutex = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||
|
||||
bool BookMetadataCache::beginWrite() {
|
||||
@@ -375,6 +414,7 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri
|
||||
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||
|
||||
bool BookMetadataCache::load() {
|
||||
CacheIoLock ioLock(ioMutex);
|
||||
if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
return false;
|
||||
}
|
||||
@@ -392,11 +432,13 @@ bool BookMetadataCache::load() {
|
||||
serialization::readPod(bookFile, spineCount);
|
||||
serialization::readPod(bookFile, tocCount);
|
||||
|
||||
serialization::readString(bookFile, coreMetadata.title);
|
||||
serialization::readString(bookFile, coreMetadata.author);
|
||||
serialization::readString(bookFile, coreMetadata.language);
|
||||
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||
if (!readStringBounded(bookFile, coreMetadata.title) || !readStringBounded(bookFile, coreMetadata.author) ||
|
||||
!readStringBounded(bookFile, coreMetadata.language) || !readStringBounded(bookFile, coreMetadata.coverItemHref) ||
|
||||
!readStringBounded(bookFile, coreMetadata.textReferenceHref)) {
|
||||
LOG_ERR("BMC", "Invalid cache metadata strings");
|
||||
bookFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
||||
@@ -404,6 +446,7 @@ bool BookMetadataCache::load() {
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||
CacheIoLock ioLock(ioMutex);
|
||||
if (!loaded) {
|
||||
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
||||
return {};
|
||||
@@ -418,11 +461,16 @@ BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index)
|
||||
bookFile.seek(lutOffset + sizeof(uint32_t) * index);
|
||||
uint32_t spineEntryPos;
|
||||
serialization::readPod(bookFile, spineEntryPos);
|
||||
if (spineEntryPos >= bookFile.size()) {
|
||||
LOG_ERR("BMC", "Spine entry offset out of range: %lu", static_cast<unsigned long>(spineEntryPos));
|
||||
return {};
|
||||
}
|
||||
bookFile.seek(spineEntryPos);
|
||||
return readSpineEntry(bookFile);
|
||||
}
|
||||
|
||||
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||
CacheIoLock ioLock(ioMutex);
|
||||
if (!loaded) {
|
||||
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
|
||||
return {};
|
||||
@@ -437,24 +485,34 @@ BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||
bookFile.seek(lutOffset + sizeof(uint32_t) * spineCount + sizeof(uint32_t) * index);
|
||||
uint32_t tocEntryPos;
|
||||
serialization::readPod(bookFile, tocEntryPos);
|
||||
if (tocEntryPos >= bookFile.size()) {
|
||||
LOG_ERR("BMC", "TOC entry offset out of range: %lu", static_cast<unsigned long>(tocEntryPos));
|
||||
return {};
|
||||
}
|
||||
bookFile.seek(tocEntryPos);
|
||||
return readTocEntry(bookFile);
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
||||
SpineEntry entry;
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readPod(file, entry.cumulativeSize);
|
||||
serialization::readPod(file, entry.tocIndex);
|
||||
if (!readStringBounded(file, entry.href) ||
|
||||
file.read(&entry.cumulativeSize, sizeof(entry.cumulativeSize)) !=
|
||||
static_cast<int>(sizeof(entry.cumulativeSize)) ||
|
||||
file.read(&entry.tocIndex, sizeof(entry.tocIndex)) != static_cast<int>(sizeof(entry.tocIndex))) {
|
||||
LOG_ERR("BMC", "Invalid spine cache entry");
|
||||
return {};
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
|
||||
TocEntry entry;
|
||||
serialization::readString(file, entry.title);
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readString(file, entry.anchor);
|
||||
serialization::readPod(file, entry.level);
|
||||
serialization::readPod(file, entry.spineIndex);
|
||||
if (!readStringBounded(file, entry.title) || !readStringBounded(file, entry.href) ||
|
||||
!readStringBounded(file, entry.anchor) ||
|
||||
file.read(&entry.level, sizeof(entry.level)) != static_cast<int>(sizeof(entry.level)) ||
|
||||
file.read(&entry.spineIndex, sizeof(entry.spineIndex)) != static_cast<int>(sizeof(entry.spineIndex))) {
|
||||
LOG_ERR("BMC", "Invalid TOC cache entry");
|
||||
return {};
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
@@ -54,6 +55,7 @@ class BookMetadataCache {
|
||||
// Temp file handles during build
|
||||
HalFile spineFile;
|
||||
HalFile tocFile;
|
||||
SemaphoreHandle_t ioMutex;
|
||||
|
||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||
struct SpineHrefIndexEntry {
|
||||
@@ -85,8 +87,14 @@ class BookMetadataCache {
|
||||
BookMetadata coreMetadata;
|
||||
|
||||
explicit BookMetadataCache(std::string cachePath)
|
||||
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
||||
~BookMetadataCache() = default;
|
||||
: cachePath(std::move(cachePath)),
|
||||
lutOffset(0),
|
||||
spineCount(0),
|
||||
tocCount(0),
|
||||
loaded(false),
|
||||
buildMode(false),
|
||||
ioMutex(xSemaphoreCreateRecursiveMutex()) {}
|
||||
~BookMetadataCache();
|
||||
|
||||
// Building phase (stream to disk immediately)
|
||||
bool beginWrite();
|
||||
|
||||
@@ -150,7 +150,6 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||
self->state = IN_GUIDE;
|
||||
// TODO Remove print
|
||||
LOG_DBG("COF", "Entering guide state.");
|
||||
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||
|
||||
+22
-14
@@ -3,6 +3,7 @@
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace FsHelpers {
|
||||
@@ -36,12 +37,14 @@ std::string decodeUriEscapes(const std::string& path) {
|
||||
}
|
||||
|
||||
std::string normalisePath(const std::string& path) {
|
||||
std::vector<std::string> components;
|
||||
std::string component;
|
||||
std::vector<std::string_view> components;
|
||||
components.reserve(8); // Eight nested folders is more than we might expect
|
||||
|
||||
for (const auto c : path) {
|
||||
if (c == '/') {
|
||||
if (!component.empty()) {
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= path.length(); ++i) {
|
||||
if (i == path.length() || path[i] == '/') {
|
||||
if (i > start) {
|
||||
std::string_view component(path.data() + start, i - start);
|
||||
if (component == "..") {
|
||||
if (!components.empty()) {
|
||||
components.pop_back();
|
||||
@@ -49,23 +52,28 @@ std::string normalisePath(const std::string& path) {
|
||||
} else {
|
||||
components.push_back(component);
|
||||
}
|
||||
component.clear();
|
||||
}
|
||||
} else {
|
||||
component += c;
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.empty()) {
|
||||
components.push_back(component);
|
||||
if (components.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
size_t total_len = 0;
|
||||
for (const auto& c : components) {
|
||||
total_len += c.length() + 1;
|
||||
}
|
||||
|
||||
std::string result;
|
||||
for (const auto& c : components) {
|
||||
if (!result.empty()) {
|
||||
result += "/";
|
||||
result.reserve(total_len - 1);
|
||||
|
||||
for (size_t i = 0; i < components.size(); ++i) {
|
||||
if (i > 0) {
|
||||
result += '/';
|
||||
}
|
||||
result += c;
|
||||
result.append(components[i].data(), components[i].length());
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
+281
-25
@@ -3,6 +3,7 @@
|
||||
#include <BidiUtils.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalGPIO.h>
|
||||
#include <Icon.h>
|
||||
#include <Logging.h>
|
||||
#include <SdCardFont.h>
|
||||
#include <Utf8.h>
|
||||
@@ -100,6 +101,23 @@ void GfxRenderer::insertFont(const int fontId, EpdFontFamily font) {
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::setUiFontRemap(const int* from, const int* to, int count) {
|
||||
if (count < 0) count = 0;
|
||||
if (count > MAX_UI_FONT_REMAP) count = MAX_UI_FONT_REMAP;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
uiFontFrom_[i] = from[i];
|
||||
uiFontTo_[i] = to[i];
|
||||
}
|
||||
uiFontRemapCount_ = count;
|
||||
}
|
||||
|
||||
int GfxRenderer::remapUiFont(const int fontId) const {
|
||||
for (int i = 0; i < uiFontRemapCount_; ++i) {
|
||||
if (uiFontFrom_[i] == fontId) return uiFontTo_[i];
|
||||
}
|
||||
return fontId;
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -365,7 +383,7 @@ int GfxRenderer::getTextWidth(const int fontId, const char* text, const EpdFontF
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -407,7 +425,7 @@ void GfxRenderer::drawText(const int fontId, const int x, const int y, const cha
|
||||
return;
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return;
|
||||
@@ -661,8 +679,10 @@ void GfxRenderer::drawRoundedRect(const int x, const int y, const int width, con
|
||||
}
|
||||
|
||||
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);
|
||||
if (state) {
|
||||
fillRectImpl<Color::Black>(x, y, width, height);
|
||||
} else {
|
||||
fillRectImpl<Color::White>(x, y, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -694,26 +714,194 @@ void GfxRenderer::drawPixelDither<Color::DarkGray>(const int x, const int y) con
|
||||
}
|
||||
|
||||
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);
|
||||
switch (color) {
|
||||
case Color::Clear:
|
||||
break;
|
||||
case Color::Black:
|
||||
fillRectImpl<Color::Black>(x, y, width, height);
|
||||
break;
|
||||
case Color::White:
|
||||
fillRectImpl<Color::White>(x, y, width, height);
|
||||
break;
|
||||
case Color::LightGray:
|
||||
fillRectImpl<Color::LightGray>(x, y, width, height);
|
||||
break;
|
||||
case Color::DarkGray:
|
||||
fillRectImpl<Color::DarkGray>(x, y, width, height);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
template <Color C>
|
||||
void GfxRenderer::fillRectImpl(const int x, const int y, const int width, const int height) const {
|
||||
if constexpr (C == Color::Clear) return;
|
||||
if (width <= 0 || height <= 0) return;
|
||||
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
||||
|
||||
// Clip in logical space.
|
||||
const int screenW = getScreenWidth();
|
||||
const int screenH = getScreenHeight();
|
||||
const int lx0 = std::max(0, x);
|
||||
const int ly0 = std::max(0, y);
|
||||
const int lx1 = std::min(screenW, x + width);
|
||||
const int ly1 = std::min(screenH, y + height);
|
||||
if (lx0 >= lx1 || ly0 >= ly1) return;
|
||||
|
||||
// Rotate the two opposing logical corners into physical-framebuffer space.
|
||||
// The bounding rect in physical space is the rect we need to fill — rotation
|
||||
// is rigid (no shear/stretch) so the bbox of the two corners IS the rect.
|
||||
int paX, paY, pbX, pbY;
|
||||
rotateCoordinates(orientation, lx0, ly0, &paX, &paY, panelWidth, panelHeight);
|
||||
rotateCoordinates(orientation, lx1 - 1, ly1 - 1, &pbX, &pbY, panelWidth, panelHeight);
|
||||
|
||||
const int phyX0 = std::min(paX, pbX);
|
||||
const int phyX1 = std::max(paX, pbX); // inclusive
|
||||
int phyY0 = std::min(paY, pbY);
|
||||
int phyY1 = std::max(paY, pbY);
|
||||
|
||||
// Strip mode: clip Y range to the active band and redirect writes.
|
||||
uint8_t* target = getWriteTarget();
|
||||
const int originY = getWriteOriginY();
|
||||
const int writeRows = getWriteRows();
|
||||
phyY0 = std::max(phyY0, originY);
|
||||
phyY1 = std::min(phyY1, originY + writeRows - 1);
|
||||
if (phyY0 > phyY1) return;
|
||||
|
||||
// Bit/byte layout: MSB-first within a byte, so phyX → bit (7 - (phyX & 7)).
|
||||
// Head and tail masks cover only the in-rect bits of the first/last byte.
|
||||
const int byteStart = phyX0 >> 3;
|
||||
const int byteEnd = phyX1 >> 3; // inclusive
|
||||
const uint8_t headMask = static_cast<uint8_t>(0xFFu >> (phyX0 & 7));
|
||||
const uint8_t tailMask = static_cast<uint8_t>(0xFFu << (7 - (phyX1 & 7)));
|
||||
const int32_t panelStride = static_cast<int32_t>(panelWidthBytes);
|
||||
|
||||
if constexpr (C == Color::Black || C == Color::White) {
|
||||
// Solid fill. Framebuffer: 0 = black, 1 = white.
|
||||
const uint8_t fillByte = (C == Color::Black) ? 0x00u : 0xFFu;
|
||||
for (int py = phyY0; py <= phyY1; ++py) {
|
||||
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
|
||||
if (byteStart == byteEnd) {
|
||||
const uint8_t mask = headMask & tailMask;
|
||||
if constexpr (C == Color::Black) {
|
||||
row[byteStart] &= static_cast<uint8_t>(~mask);
|
||||
} else {
|
||||
row[byteStart] |= mask;
|
||||
}
|
||||
} else {
|
||||
if constexpr (C == Color::Black) {
|
||||
row[byteStart] &= static_cast<uint8_t>(~headMask);
|
||||
if (byteEnd > byteStart + 1) {
|
||||
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
|
||||
}
|
||||
row[byteEnd] &= static_cast<uint8_t>(~tailMask);
|
||||
} else {
|
||||
row[byteStart] |= headMask;
|
||||
if (byteEnd > byteStart + 1) {
|
||||
memset(row + byteStart + 1, fillByte, byteEnd - byteStart - 1);
|
||||
}
|
||||
row[byteEnd] |= tailMask;
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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);
|
||||
} else {
|
||||
// Dither (LightGray / DarkGray). Both patterns have period 2 in logical
|
||||
// (x, y), so per physical row we precompute one byte that represents the
|
||||
// pattern across an 8-pixel stretch — every full byte in the row uses
|
||||
// that same value.
|
||||
//
|
||||
// dlxPerPhyX / dlyPerPhyX: how logical (x, y) change as phyX increments
|
||||
// along a physical row. Derived from inverting rotateCoordinates.
|
||||
int dlxPerPhyX = 0, dlyPerPhyX = 0;
|
||||
switch (orientation) {
|
||||
case Portrait:
|
||||
dlxPerPhyX = 0;
|
||||
dlyPerPhyX = 1;
|
||||
break;
|
||||
case PortraitInverted:
|
||||
dlxPerPhyX = 0;
|
||||
dlyPerPhyX = -1;
|
||||
break;
|
||||
case LandscapeClockwise:
|
||||
dlxPerPhyX = -1;
|
||||
dlyPerPhyX = 0;
|
||||
break;
|
||||
case LandscapeCounterClockwise:
|
||||
dlxPerPhyX = 1;
|
||||
dlyPerPhyX = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// The dither pattern has period 2 in logical space, and each orientation
|
||||
// maps py to logical coords with a fixed parity relationship. The
|
||||
// blackMask byte therefore repeats with period 2 in py. Precompute both
|
||||
// variants outside the row loop to eliminate the per-row switch + 8-bit
|
||||
// construction loop.
|
||||
uint8_t blackMasks[2];
|
||||
for (int parityIdx = 0; parityIdx < 2; ++parityIdx) {
|
||||
const int samplePy = phyY0 + parityIdx;
|
||||
int lxBase = 0, lyBase = 0;
|
||||
switch (orientation) {
|
||||
case Portrait:
|
||||
lxBase = panelHeight - 1 - samplePy;
|
||||
lyBase = byteStart * 8;
|
||||
break;
|
||||
case PortraitInverted:
|
||||
lxBase = samplePy;
|
||||
lyBase = panelWidth - 1 - byteStart * 8;
|
||||
break;
|
||||
case LandscapeClockwise:
|
||||
lxBase = panelWidth - 1 - byteStart * 8;
|
||||
lyBase = panelHeight - 1 - samplePy;
|
||||
break;
|
||||
case LandscapeCounterClockwise:
|
||||
lxBase = byteStart * 8;
|
||||
lyBase = samplePy;
|
||||
break;
|
||||
}
|
||||
uint8_t mask = 0;
|
||||
for (int b = 0; b < 8; ++b) {
|
||||
const int lx = lxBase + b * dlxPerPhyX;
|
||||
const int ly = lyBase + b * dlyPerPhyX;
|
||||
bool isBlack;
|
||||
if constexpr (C == Color::LightGray) {
|
||||
isBlack = ((lx & 1) == 0) && ((ly & 1) == 0);
|
||||
} else { // DarkGray
|
||||
isBlack = (((lx + ly) & 1) == 0);
|
||||
}
|
||||
if (isBlack) mask |= static_cast<uint8_t>(1u << (7 - b));
|
||||
}
|
||||
blackMasks[samplePy & 1] = mask;
|
||||
}
|
||||
|
||||
for (int py = phyY0; py <= phyY1; ++py) {
|
||||
const uint8_t blackMask = blackMasks[py & 1];
|
||||
const uint8_t whiteMask = static_cast<uint8_t>(~blackMask);
|
||||
|
||||
// Dither writes BOTH inks (the slow path called drawPixel for every
|
||||
// pixel — setting or clearing — so we must do the same). Inside the
|
||||
// rect mask: write whiteMask (1s where white, 0s where black). Outside
|
||||
// the rect mask: leave the framebuffer untouched.
|
||||
uint8_t* row = target + static_cast<int32_t>(py - originY) * panelStride;
|
||||
if (byteStart == byteEnd) {
|
||||
const uint8_t rectMask = headMask & tailMask;
|
||||
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~rectMask) | (rectMask & whiteMask));
|
||||
} else {
|
||||
row[byteStart] = static_cast<uint8_t>((row[byteStart] & ~headMask) | (headMask & whiteMask));
|
||||
if (byteEnd > byteStart + 1) {
|
||||
// Period 2, so every full byte in this row is exactly whiteMask.
|
||||
memset(row + byteStart + 1, whiteMask, byteEnd - byteStart - 1);
|
||||
}
|
||||
row[byteEnd] = static_cast<uint8_t>((row[byteEnd] & ~tailMask) | (tailMask & whiteMask));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template void GfxRenderer::fillRectImpl<Color::Black>(int, int, int, int) const;
|
||||
template void GfxRenderer::fillRectImpl<Color::White>(int, int, int, int) const;
|
||||
template void GfxRenderer::fillRectImpl<Color::LightGray>(int, int, int, int) const;
|
||||
template void GfxRenderer::fillRectImpl<Color::DarkGray>(int, int, int, int) const;
|
||||
|
||||
void GfxRenderer::maskRoundedRectOutsideCorners(const int x, const int y, const int width, const int height,
|
||||
const int radius, const Color color) const {
|
||||
if (radius <= 0 || color == Color::Clear) {
|
||||
@@ -888,6 +1076,21 @@ void GfxRenderer::drawIcon(const uint8_t bitmap[], const int x, const int y, con
|
||||
display.drawImageTransparent(bitmap, y, getScreenWidth() - width - x, height, width);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawIcon(const freeink::Icon& icon, const int x, const int y, const bool black) const {
|
||||
// Bits are un-rotated, so each drawn pixel goes through drawPixel (which applies
|
||||
// the orientation transform) — correct in every orientation, unlike the legacy
|
||||
// uint8_t* overload that pre-rotates for one orientation.
|
||||
const int rowBytes = (icon.w + 7) / 8;
|
||||
for (int row = 0; row < icon.h; ++row) {
|
||||
const uint8_t* r = icon.bits + static_cast<int>(row) * rowBytes;
|
||||
for (int col = 0; col < icon.w; ++col) {
|
||||
if (((r[col >> 3] >> (7 - (col & 7))) & 1) == 0) { // 0 = drawn
|
||||
drawPixel(x + col, y + row, black);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if (fontCacheManager_ && fontCacheManager_->isScanning()) return;
|
||||
@@ -1296,6 +1499,38 @@ int GfxRenderer::getScreenHeight() const {
|
||||
return panelWidth;
|
||||
}
|
||||
|
||||
void GfxRenderer::tapToLogical(float nx, float ny, int& outX, int& outY) const {
|
||||
// Native panel pixel of the tap (wasTouchTap normalizes over the native panel).
|
||||
int phyX = static_cast<int>(nx * panelWidth);
|
||||
int phyY = static_cast<int>(ny * panelHeight);
|
||||
if (phyX < 0) phyX = 0;
|
||||
if (phyX > panelWidth - 1) phyX = panelWidth - 1;
|
||||
if (phyY < 0) phyY = 0;
|
||||
if (phyY > panelHeight - 1) phyY = panelHeight - 1;
|
||||
|
||||
// Inverse of rotateCoordinates() (see the forward transform above): map a
|
||||
// physical/native point back into the current orientation's logical frame.
|
||||
switch (orientation) {
|
||||
case Portrait: // forward: phyX=logY, phyY=panelHeight-1-logX
|
||||
outX = panelHeight - 1 - phyY;
|
||||
outY = phyX;
|
||||
break;
|
||||
case PortraitInverted: // forward: phyX=panelWidth-1-logY, phyY=logX
|
||||
outX = phyY;
|
||||
outY = panelWidth - 1 - phyX;
|
||||
break;
|
||||
case LandscapeClockwise: // forward: phyX=panelWidth-1-logX, phyY=panelHeight-1-logY
|
||||
outX = panelWidth - 1 - phyX;
|
||||
outY = panelHeight - 1 - phyY;
|
||||
break;
|
||||
case LandscapeCounterClockwise: // forward: identity
|
||||
default:
|
||||
outX = phyX;
|
||||
outY = phyY;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Translate a logical rect through rotateCoordinates and take the bounding
|
||||
// box of its four corners on the physical panel. Output coords are inclusive
|
||||
// and clamped. Returns false if the rect ends up fully off-panel.
|
||||
@@ -1387,7 +1622,7 @@ int GfxRenderer::getSpaceWidth(const int fontId, const EpdFontFamily::Style styl
|
||||
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1408,7 +1643,7 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
|
||||
return fp4::toPixel(sdIt->second->getAdvance(' ', resolvedStyle));
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
const auto& font = fontIt->second;
|
||||
const EpdGlyph* spaceGlyph = font.getGlyph(' ', style);
|
||||
@@ -1422,7 +1657,7 @@ int GfxRenderer::getSpaceAdvance(const int fontId, const uint32_t leftCp, const
|
||||
|
||||
int GfxRenderer::getKerning(const int fontId, const uint32_t leftCp, const uint32_t rightCp,
|
||||
const EpdFontFamily::Style style) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
const int kernFP = fontIt->second.getKerning(leftCp, rightCp, style); // 4.4 fixed-point
|
||||
return fp4::toPixel(kernFP); // snap 4.4 fixed-point to nearest pixel
|
||||
@@ -1454,7 +1689,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
return fp4::toPixel(widthFP);
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1490,7 +1725,7 @@ int GfxRenderer::getTextAdvanceX(const int fontId, const char* text, EpdFontFami
|
||||
}
|
||||
|
||||
int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1500,7 +1735,7 @@ int GfxRenderer::getFontAscenderSize(const int fontId) const {
|
||||
}
|
||||
|
||||
int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1510,7 +1745,7 @@ int GfxRenderer::getLineHeight(const int fontId) const {
|
||||
}
|
||||
|
||||
int GfxRenderer::getTextHeight(const int fontId) const {
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return 0;
|
||||
@@ -1518,6 +1753,27 @@ int GfxRenderer::getTextHeight(const int fontId) const {
|
||||
return fontIt->second.getData(EpdFontFamily::REGULAR)->ascender;
|
||||
}
|
||||
|
||||
// Height of a reference glyph above the baseline (glyph->top): 'H' gives the real
|
||||
// cap height, the actual visible font extent for vertical centering.
|
||||
int GfxRenderer::glyphTop(const int fontId, const uint32_t cp) const {
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) return 0;
|
||||
const EpdGlyph* g = fontIt->second.getGlyph(cp, EpdFontFamily::REGULAR);
|
||||
return g ? g->top : 0;
|
||||
}
|
||||
|
||||
int GfxRenderer::getFontCapHeight(const int fontId) const { return glyphTop(fontId, 'H'); }
|
||||
|
||||
int GfxRenderer::getTextVisualCenterOffset(const int fontId) const {
|
||||
// Cap-height middle: baseline is at top + ascender, caps reach capHeight above it,
|
||||
// so the optical center is ascender - capHeight/2 below the top. Cap (not x) height
|
||||
// because UI labels are capital-led. Falls back to ascender*0.65 without an 'H'.
|
||||
const int ascender = getFontAscenderSize(fontId);
|
||||
const int capHeight = getFontCapHeight(fontId);
|
||||
if (capHeight <= 0) return (ascender * 65) / 100;
|
||||
return ascender - capHeight / 2;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1525,7 +1781,7 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y
|
||||
return;
|
||||
}
|
||||
|
||||
const auto fontIt = fontMap.find(fontId);
|
||||
const auto fontIt = fontMap.find(remapUiFont(fontId));
|
||||
if (fontIt == fontMap.end()) {
|
||||
LOG_ERR("GFX", "Font %d not found", fontId);
|
||||
return;
|
||||
|
||||
@@ -13,6 +13,9 @@ enum class BidiBaseDir : signed char { AUTO = -1, LTR = 0, RTL = 1 };
|
||||
|
||||
class FontCacheManager;
|
||||
class SdCardFont;
|
||||
namespace freeink {
|
||||
struct Icon;
|
||||
}
|
||||
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
@@ -51,6 +54,13 @@ class GfxRenderer {
|
||||
uint32_t frameBufferSize = HalDisplay::BUFFER_SIZE;
|
||||
std::vector<uint8_t*> bwBufferChunks;
|
||||
std::map<int, EpdFontFamily> fontMap;
|
||||
// UI chrome font remap table (see setUiFontRemap). Empty = identity.
|
||||
static constexpr int MAX_UI_FONT_REMAP = 8;
|
||||
int uiFontFrom_[MAX_UI_FONT_REMAP] = {0};
|
||||
int uiFontTo_[MAX_UI_FONT_REMAP] = {0};
|
||||
int uiFontRemapCount_ = 0;
|
||||
int remapUiFont(int fontId) const;
|
||||
int glyphTop(int fontId, uint32_t cp) const;
|
||||
// Mutable because ensureSdCardFontReady() is const (called from layout code
|
||||
// that holds a const GfxRenderer&) but triggers SD card reads and heap
|
||||
// allocation inside the SdCardFont objects. Same pragmatic compromise as
|
||||
@@ -81,6 +91,12 @@ class GfxRenderer {
|
||||
void drawPixelDither(int x, int y) const;
|
||||
template <Color color>
|
||||
void fillArc(int maxRadius, int cx, int cy, int xDir, int yDir) const;
|
||||
// Byte-aligned, orientation-specialized rectangle fill. Rotates the rect's
|
||||
// two opposing corners into physical-framebuffer space once, then walks each
|
||||
// physical row with head-mask / middle memset / tail-mask byte writes — no
|
||||
// per-pixel rotation, no per-pixel RMW.
|
||||
template <Color color>
|
||||
void fillRectImpl(int x, int y, int width, int height) const;
|
||||
|
||||
public:
|
||||
explicit GfxRenderer(HalDisplay& halDisplay)
|
||||
@@ -95,6 +111,14 @@ class GfxRenderer {
|
||||
// Setup
|
||||
void begin(); // must be called right after display.begin()
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
// Orientation-correct icon blit for the freeink::Icon format (un-rotated bits +
|
||||
// optical-center metadata). Prefer this over the legacy raw-bitmap drawIcon.
|
||||
void drawIcon(const freeink::Icon& icon, int x, int y, bool black = true) const;
|
||||
// UI chrome font scaling: firmware supplies a small remap table (from the board
|
||||
// uiScale) that substitutes a larger font for each scaled UI font id at lookup
|
||||
// time, so layout and drawing stay consistent with no call-site changes. Reader
|
||||
// body fonts aren't in the table, so book text is unaffected. count <= 8.
|
||||
void setUiFontRemap(const int* from, const int* to, int count);
|
||||
// Clears both the flash-font map and any SD-font registration for fontId.
|
||||
// Coupled to avoid dangling SdCardFont* in sdCardFonts_ when callers free
|
||||
// the underlying SdCardFont and forget the SD-side unregister.
|
||||
@@ -135,6 +159,11 @@ class GfxRenderer {
|
||||
void clearScreen(uint8_t color = 0xFF) const;
|
||||
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const;
|
||||
|
||||
// Map a touch tap (normalized 0..1 in panel-native orientation, from
|
||||
// InputManager::wasTouchTap) to logical screen coordinates matching the Rects the
|
||||
// UI draws in. Inverse of rotateCoordinates() for the current orientation.
|
||||
void tapToLogical(float nx, float ny, int& outX, int& outY) const;
|
||||
|
||||
// Tiled grayscale strip target. While active, drawPixel() and clearScreen()
|
||||
// operate on `scratch` (panelWidthBytes * stripRows bytes, holding physical
|
||||
// rows [stripY0, stripY0 + stripRows)) instead of the framebuffer; pixels
|
||||
@@ -202,6 +231,12 @@ class GfxRenderer {
|
||||
int getTextAdvanceX(int fontId, const char* text, EpdFontFamily::Style style) const;
|
||||
int getFontAscenderSize(int fontId) const;
|
||||
int getLineHeight(int fontId) const;
|
||||
// Cap height (from the 'H' glyph), the real visible font extent for vertical
|
||||
// alignment that follows the font instead of a guessed ascender fraction.
|
||||
int getFontCapHeight(int fontId) const;
|
||||
// Offset from text top (drawText's y) to the text's optical center; align an
|
||||
// element to a line via centerY = textTop + getTextVisualCenterOffset(fontId).
|
||||
int getTextVisualCenterOffset(int fontId) const;
|
||||
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
/// Word-wrap \p text into at most \p maxLines lines, each no wider than
|
||||
|
||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Згладжванне тэксту"
|
||||
STR_SHORT_PWR_BTN: "Кароткае націсканне PWR"
|
||||
STR_ORIENTATION: "Арыентацыя чытання"
|
||||
STR_SIDE_BTN_LAYOUT: "Бакавыя кнопкі"
|
||||
STR_TOUCH_READER_CONTROLS: "Сэнсарнае кіраванне чытаннем"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Арыентаваць пярэднія кнопкі"
|
||||
STR_LONG_PRESS_SKIP: "Доўгае націсканне - змена раздзела"
|
||||
STR_FONT_PREVIEW_TEXT: "У Іўі худы жвавы чорт у зялёнай камізэльцы пабег пад'есці фаршу з юшкай"
|
||||
@@ -126,6 +127,7 @@ STR_PAGE_TURN: "Перагортванне"
|
||||
STR_PORTRAIT: "Партрэт"
|
||||
STR_LANDSCAPE_CW: "Ландшафт (CW)"
|
||||
STR_INVERTED: "Інверсія"
|
||||
STR_ORIENTATION_INVERTED: "Партрэт 180°"
|
||||
STR_LANDSCAPE_CCW: "Ландшафт (CCW)"
|
||||
STR_PREV_NEXT: "Назад/Наперад"
|
||||
STR_NEXT_PREV: "Наперад/Назад"
|
||||
|
||||
@@ -72,6 +72,7 @@ STR_IMAGES_SUPPRESS: "Suprimir"
|
||||
STR_SHORT_PWR_BTN: "Clic curt del botó d'engegada"
|
||||
STR_ORIENTATION: "Orientació de lectura"
|
||||
STR_SIDE_BTN_LAYOUT: "Disposició botons laterals"
|
||||
STR_TOUCH_READER_CONTROLS: "Controls tàctils del lector"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientar botons frontals"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Comportament de prémer llargament el botó"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "Desactivat"
|
||||
@@ -136,6 +137,7 @@ STR_PAGE_TURN: "Canvi de pàgina"
|
||||
STR_PORTRAIT: "Vertical"
|
||||
STR_LANDSCAPE_CW: "Horitzontal horari"
|
||||
STR_INVERTED: "Invertit"
|
||||
STR_ORIENTATION_INVERTED: "Vertical 180°"
|
||||
STR_LANDSCAPE_CCW: "Horitzontal antihorari"
|
||||
STR_PREV_NEXT: "Anterior/Següent"
|
||||
STR_NEXT_PREV: "Següent/Anterior"
|
||||
|
||||
@@ -68,6 +68,7 @@ STR_TEXT_AA: "Vyhlazování textu"
|
||||
STR_SHORT_PWR_BTN: "Krátké stisknutí tlačítka napájení"
|
||||
STR_ORIENTATION: "Orientace čtení"
|
||||
STR_SIDE_BTN_LAYOUT: "Rozvržení bočních tlačítek (čtečka)"
|
||||
STR_TOUCH_READER_CONTROLS: "Dotykové ovládání čtečky"
|
||||
STR_FRONT_BTN_FOLLOW_ORIENTATION: "Orientovat přední tlačítka"
|
||||
STR_LONG_PRESS_BEHAVIOR: "Chování při dlouhém stisknutí tlačítka"
|
||||
STR_LONG_PRESS_BEHAVIOR_OFF: "VYP"
|
||||
@@ -131,6 +132,7 @@ STR_PAGE_TURN: "Otáčení stránek"
|
||||
STR_PORTRAIT: "Na výšku"
|
||||
STR_LANDSCAPE_CW: "Na šířku po směru hod. ručiček"
|
||||
STR_INVERTED: "Invertovaný"
|
||||
STR_ORIENTATION_INVERTED: "Na výšku 180°"
|
||||
STR_LANDSCAPE_CCW: "Na šířku proti směru hod. ručiček"
|
||||
STR_PREV_NEXT: "Předchozí/Další"
|
||||
STR_NEXT_PREV: "Další/Předchozí"
|
||||
|
||||
Loaded 20 of 142 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user