mirror of
https://github.com/crosspoint-reader/crosspoint-reader.git
synced 2026-08-13 14:21:57 -07:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bf9fea5923 | ||
|
|
e9c3a166f5 | ||
|
|
d720891b2f | ||
|
|
e4aae7baef | ||
|
|
7f8b005ae1 | ||
|
|
81028d5849 | ||
|
|
0423276d2c | ||
|
|
596f0d3f71 | ||
|
|
6af9a04934 | ||
|
|
1582e70ef4 | ||
|
|
d4733bfc65 |
No files matched your search
@@ -63,7 +63,8 @@ jobs:
|
||||
- name: Run cppcheck
|
||||
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
|
||||
|
||||
build:
|
||||
build-default:
|
||||
name: Build default
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -90,14 +91,10 @@ jobs:
|
||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
||||
restore-keys: pio-${{ runner.os }}-
|
||||
|
||||
- name: Build CrossPoint
|
||||
# Both envs in ONE pio invocation: a second `pio run` can wipe the whole
|
||||
# .pio/build tree (project-checksum mismatch after the S3 toolchain
|
||||
# installs into the restored cache), deleting the default firmware.bin
|
||||
# before the artifact upload.
|
||||
- name: Build default firmware
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pio run -e default -e sticky | tee pio.log
|
||||
pio run -e default | tee pio.log
|
||||
|
||||
- name: Extract firmware stats
|
||||
|
||||
@@ -120,6 +117,52 @@ jobs:
|
||||
path: .pio/build/default/firmware.bin
|
||||
if-no-files-found: error
|
||||
|
||||
build-sticky:
|
||||
name: Build sticky
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
|
||||
- name: Cache PlatformIO packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
||||
restore-keys: pio-${{ runner.os }}-
|
||||
|
||||
- name: Build sticky firmware
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pio run -e sticky | tee pio.log
|
||||
|
||||
- name: Extract firmware stats
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ram_line="$(grep -E "RAM:\\s" -m1 pio.log || true)"
|
||||
flash_line="$(grep -E "Flash:\\s" -m1 pio.log || true)"
|
||||
echo "ram_line=${ram_line}" >> "$GITHUB_OUTPUT"
|
||||
echo "flash_line=${flash_line}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "## Sticky firmware build stats"
|
||||
if [ -n "$ram_line" ]; then echo "- ${ram_line}"; else echo "- RAM: not found"; fi
|
||||
if [ -n "$flash_line" ]; then echo "- ${flash_line}"; else echo "- Flash: not found"; fi
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
unit-tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -152,7 +195,8 @@ jobs:
|
||||
test-status:
|
||||
name: Test Status
|
||||
needs:
|
||||
- build
|
||||
- build-default
|
||||
- build-sticky
|
||||
- clang-format
|
||||
- cppcheck
|
||||
- unit-tests
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
name: Compile Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
version: "latest"
|
||||
enable-cache: false
|
||||
|
||||
- name: Install PlatformIO Core
|
||||
run: uv pip install --system -U https://github.com/pioarduino/platformio-core/archive/refs/tags/v6.1.19.zip
|
||||
|
||||
- name: Cache PlatformIO packages
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
|
||||
restore-keys: pio-${{ runner.os }}-
|
||||
|
||||
- name: Build CrossPoint
|
||||
run: pio run -e gh_release
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: CrossPoint-${{ github.ref_name }}
|
||||
path: |
|
||||
.pio/build/gh_release/bootloader.bin
|
||||
.pio/build/gh_release/firmware.bin
|
||||
.pio/build/gh_release/firmware.elf
|
||||
.pio/build/gh_release/firmware.map
|
||||
.pio/build/gh_release/partitions.bin
|
||||
@@ -31,3 +31,4 @@ lib/EpdFont/scripts/output/
|
||||
/dependencies.lock
|
||||
/sdkconfig.*
|
||||
sdkconfig.sticky
|
||||
flake.*
|
||||
+179
-68
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1 @@
|
||||
/home/urit/Documents/CrossPoint/crosspoint-reader/.skills/SKILL.md
|
||||
+6
-4
@@ -10,9 +10,10 @@ A dictionary folder must contain:
|
||||
|
||||
- `.idx` — word index (required, **must be uncompressed** — a `.idx.gz` will not work; decompress it on your computer with `gzip -d` first)
|
||||
- `.dict` or `.dict.dz` — definition data (`.dict.dz` is supported as-is; entries are decompressed on the fly during lookup)
|
||||
- `.syn` — synonym index (optional; maps alternate spellings and irregular forms to their headword)
|
||||
- `.ifo` — metadata (optional)
|
||||
|
||||
Not supported: `.syn` synonym files (ignored), dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
|
||||
Not supported: dictionaries with 64-bit index offsets (`idxoffsetbits=64` in the `.ifo` — rare, and rejected with an error), and HTML-formatted definitions render as raw markup rather than styled text.
|
||||
|
||||
## Setting Up a Dictionary
|
||||
|
||||
@@ -35,13 +36,14 @@ One word on the page becomes highlighted:
|
||||
2. Press **Confirm** to look up the highlighted word.
|
||||
3. Press **Back** to return to the reader.
|
||||
|
||||
On the very first lookup with a dictionary (and again if the dictionary file changes), the reader shows *"Indexing dictionary…"* while it builds a small `.qidx` sidecar file next to the `.idx`. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecar can be deleted safely at any time — it will simply be rebuilt.
|
||||
On the very first lookup with a dictionary (and again whenever the `.idx` or `.syn` source file changes), the reader shows *"Indexing dictionary…"* while it builds small sidecar files next to them — a `.qidx` for the word index, and a `.sidx` when a `.syn` synonym file is present. Each sidecar is rebuilt independently, only when its own source changes. This takes a few seconds for large dictionaries and makes all subsequent lookups fast. The sidecars can be deleted safely at any time — they will simply be rebuilt.
|
||||
|
||||
### How Lookup Works
|
||||
|
||||
1. **Direct match** — the word is found as-is (case-insensitive) in the dictionary index. Surrounding punctuation is ignored.
|
||||
2. **Stemming** — on a miss, common English word forms are retried automatically: possessives and plurals (`dogs` → `dog`, `stories` → `story`) and verb endings (`walked` → `walk`, `running` → `run`, `making` → `make`).
|
||||
3. **Not found** — a short popup appears and you return to word selection.
|
||||
2. **Synonyms** — on a miss, if the dictionary ships a `.syn` file, alternate spellings and irregular forms recorded there are resolved to their headword (e.g. `oxen` → `ox`, `colour` → `color`). This step is skipped if the `.sidx` sidecar could not be built (e.g. transient low memory during indexing); the dictionary otherwise stays usable, and the build is retried the next time it is opened.
|
||||
3. **Stemming** — still no match: common English word forms are retried automatically: possessives and plurals (`dogs` → `dog`, `stories` → `story`) and verb endings (`walked` → `walk`, `running` → `run`, `making` → `make`).
|
||||
4. **Not found** — a short popup appears and you return to word selection.
|
||||
|
||||
## The Definition Screen
|
||||
|
||||
|
||||
+10
-3
@@ -90,12 +90,19 @@ if (parsedSize != fileSize) {
|
||||
|
||||
## `section.bin`
|
||||
|
||||
### Version 35
|
||||
### Version 37
|
||||
|
||||
Each file in `sections/*.bin` stores one laid-out spine section. The header is
|
||||
also the cache-busting key: if any layout-affecting setting differs from the
|
||||
current reader settings, the section is discarded and rebuilt.
|
||||
|
||||
Version 37 increases the fixed-size footnote href field from 96 to 256 bytes.
|
||||
This changes each serialized footnote record from 128 to 288 bytes, so older
|
||||
section caches must be discarded and rebuilt.
|
||||
|
||||
Version 36 invalidates cached word positions after ruby and CJK justification
|
||||
layout changes.
|
||||
|
||||
Version 35 adds a header offset and a `uint32_t` entry per page for the
|
||||
visible-text offset LUT. The other section LUTs remain unchanged.
|
||||
|
||||
@@ -135,10 +142,10 @@ import std.mem;
|
||||
import std.string;
|
||||
import std.core;
|
||||
|
||||
#define EXPECTED_VERSION 35
|
||||
#define EXPECTED_VERSION 37
|
||||
#define MAX_STRING_LENGTH 65535
|
||||
#define FOOTNOTE_NUMBER_LEN 32
|
||||
#define FOOTNOTE_HREF_LEN 96
|
||||
#define FOOTNOTE_HREF_LEN 256
|
||||
|
||||
struct String {
|
||||
u32 length [[hidden, comment("String byte length")]];
|
||||
|
||||
+6
-1
@@ -802,7 +802,12 @@ int Epub::getSpineItemsCount() const {
|
||||
return bookMetadataCache->getSpineCount();
|
||||
}
|
||||
|
||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
|
||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
return 0;
|
||||
}
|
||||
return bookMetadataCache->getCumulativeSize(spineIndex);
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
|
||||
@@ -481,11 +481,30 @@ bool BookMetadataCache::load() {
|
||||
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||
|
||||
// Cache cumulative spine sizes in RAM. The progress bar (every render) and percent
|
||||
// jumps otherwise pay 2 seeks + a heap-allocating SpineEntry read per access. Spine
|
||||
// entries are stored contiguously in index order immediately after the LUTs, so read
|
||||
// them in a single sequential pass.
|
||||
cumulativeSizes.clear();
|
||||
cumulativeSizes.reserve(spineCount);
|
||||
const uint32_t lutSize = (static_cast<uint32_t>(spineCount) + tocCount) * sizeof(uint32_t);
|
||||
bookFile.seek(lutOffset + lutSize);
|
||||
for (uint16_t i = 0; i < spineCount; i++) {
|
||||
cumulativeSizes.push_back(readSpineEntry(bookFile).cumulativeSize);
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t BookMetadataCache::getCumulativeSize(const int index) const {
|
||||
if (index < 0 || index >= static_cast<int>(cumulativeSizes.size())) {
|
||||
return 0;
|
||||
}
|
||||
return cumulativeSizes[index];
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||
if (!loaded) {
|
||||
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class BookMetadataCache {
|
||||
public:
|
||||
@@ -62,6 +63,11 @@ class BookMetadataCache {
|
||||
// wrapper serves whichever pass is active (spine, then toc).
|
||||
std::unique_ptr<serialization::BufferedFileWriter> passOut;
|
||||
|
||||
// Cumulative spine sizes, cached in RAM at load() so progress/percent lookups are
|
||||
// O(1) instead of 2 seeks + a heap-allocating SpineEntry read per access (4 bytes
|
||||
// per spine item; <1KB for typical books).
|
||||
std::vector<uint32_t> cumulativeSizes;
|
||||
|
||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||
struct SpineHrefIndexEntry {
|
||||
uint64_t hrefHash; // FNV-1a 64-bit hash
|
||||
@@ -113,6 +119,9 @@ class BookMetadataCache {
|
||||
bool load();
|
||||
SpineEntry getSpineEntry(int index);
|
||||
TocEntry getTocEntry(int index);
|
||||
// Cumulative byte size up to and including the given spine item (0 if out of range
|
||||
// or not loaded). Backed by the in-RAM cumulativeSizes cache populated in load().
|
||||
uint32_t getCumulativeSize(int index) const;
|
||||
int getSpineCount() const { return spineCount; }
|
||||
int getTocCount() const { return tocCount; }
|
||||
bool isLoaded() const { return loaded; }
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
#include <cstring>
|
||||
|
||||
#define FOOTNOTE_NUMBER_LEN 32
|
||||
#define FOOTNOTE_HREF_LEN 96
|
||||
#define FOOTNOTE_HREF_LEN 256
|
||||
// ponytail: bumped from 96 to 256; calibre-generated EPUBs with long filenames
|
||||
// and URL-encoded characters routinely exceed 96 chars (e.g.
|
||||
// "Author-Title_split_NNN.html#_ftnN" encoded is ~150 chars).
|
||||
|
||||
struct FootnoteEntry {
|
||||
char number[FOOTNOTE_NUMBER_LEN];
|
||||
|
||||
@@ -31,7 +31,12 @@ namespace {
|
||||
// match. Keeps <br>-per-paragraph books (common CJK formatting) from
|
||||
// re-adding container spacing at every paragraph.
|
||||
// v35: Persist a uint32_t visible-text start offset for every page.
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 36;
|
||||
// v36: Ruby and CJK justification layout changes invalidate cached word positions.
|
||||
// v37: Footnote href records grew from 96 to 256 bytes.
|
||||
// v38: Image top margin is clamped so a full-viewport-height image cannot
|
||||
// overflow the page bottom; older caches can hold placements that panels
|
||||
// with no bottom inset refuse to draw.
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 38;
|
||||
// Written into the version field while a build is in progress; patched to
|
||||
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
|
||||
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
|
||||
|
||||
@@ -357,6 +357,7 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
// Try to render from cache first
|
||||
std::string cachePath = getCachePath(imagePath);
|
||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||
renderer.preserveImagePolarity(x, y, width, height);
|
||||
return; // Successfully rendered from cache
|
||||
}
|
||||
|
||||
@@ -419,6 +420,7 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.preserveImagePolarity(x, y, width, height);
|
||||
LOG_DBG("IMG", "Decode successful");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,16 +1,28 @@
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Logging.h>
|
||||
|
||||
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||
if (width * height > MAX_SOURCE_PIXELS) {
|
||||
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height,
|
||||
format.c_str(), MAX_SOURCE_PIXELS);
|
||||
// 64-bit multiply: a malformed header (e.g. 40000x60000) overflows a plain
|
||||
// int product before the comparison can reject it.
|
||||
const int64_t pixels = static_cast<int64_t>(width) * static_cast<int64_t>(height);
|
||||
if (pixels > MAX_SOURCE_PIXELS) {
|
||||
LOG_ERR("IMG", "Image too large (%dx%d = %lld pixels %s), max supported: %lld pixels", width, height,
|
||||
static_cast<long long>(pixels), format.c_str(), static_cast<long long>(MAX_SOURCE_PIXELS));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImageToFramebufferDecoder::yieldDuringDecode(uint32_t& lastYieldMs) {
|
||||
const uint32_t now = millis();
|
||||
if (now - lastYieldMs >= 250) {
|
||||
lastYieldMs = now;
|
||||
vTaskDelay(1);
|
||||
}
|
||||
}
|
||||
|
||||
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
||||
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
|
||||
imagePath.c_str());
|
||||
|
||||
@@ -31,9 +31,21 @@ class ImageToFramebufferDecoder {
|
||||
|
||||
virtual const char* getFormatName() const = 0;
|
||||
|
||||
// Call from per-row/per-MCU decode callbacks (free functions, hence public):
|
||||
// yields one tick at most every 250 ms so multi-second decodes keep the idle
|
||||
// task (and its watchdog) fed. `lastYieldMs` is caller-held state,
|
||||
// initialized to the decode start time.
|
||||
static void yieldDuringDecode(uint32_t& lastYieldMs);
|
||||
|
||||
protected:
|
||||
// Size validation helpers
|
||||
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536
|
||||
// Size validation helpers. The cap bounds decode TIME, not memory: both decoders
|
||||
// stream (JPEG in MCU bands at 1/2..1/8 coarse scale, PNG scanline-by-scanline
|
||||
// with its own width-based row-buffer guard), so RAM never scales with source
|
||||
// area. 8 MP admits real-world ebook covers (KDP recommends 1600x2560 and
|
||||
// 2000x3000) while keeping a worst-case single decode in single-digit seconds;
|
||||
// the row callbacks yield periodically so a long decode cannot starve the idle
|
||||
// task's watchdog.
|
||||
static constexpr int64_t MAX_SOURCE_PIXELS = 8388608; // 8 MP (e.g. 2048 * 4096)
|
||||
|
||||
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||
|
||||
@@ -46,6 +46,8 @@ struct JpegContext {
|
||||
|
||||
PixelCache cache;
|
||||
bool caching{false};
|
||||
|
||||
uint32_t lastYieldMs{0}; // throttle state for yieldDuringDecode()
|
||||
};
|
||||
|
||||
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
||||
@@ -122,6 +124,8 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
JpegContext* ctx = reinterpret_cast<JpegContext*>(pDraw->pUser);
|
||||
if (!ctx || !ctx->config || !ctx->renderer) return 0;
|
||||
|
||||
ImageToFramebufferDecoder::yieldDuringDecode(ctx->lastYieldMs);
|
||||
|
||||
// In EIGHT_BIT_GRAYSCALE mode, pPixels contains 8-bit grayscale values
|
||||
// Buffer is densely packed: stride = pDraw->iWidth, valid columns = pDraw->iWidthUsed
|
||||
uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels);
|
||||
@@ -494,6 +498,7 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
|
||||
}
|
||||
|
||||
unsigned long decodeStart = millis();
|
||||
ctx.lastYieldMs = decodeStart;
|
||||
rc = jpeg->decode(0, 0, jpegScaleOption);
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ struct PngContext {
|
||||
bool caching{false};
|
||||
|
||||
uint8_t* grayLineBuffer{nullptr};
|
||||
|
||||
uint32_t lastYieldMs{0}; // throttle state for yieldDuringDecode()
|
||||
};
|
||||
|
||||
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
||||
@@ -206,6 +208,8 @@ int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
|
||||
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
|
||||
|
||||
ImageToFramebufferDecoder::yieldDuringDecode(ctx->lastYieldMs);
|
||||
|
||||
int srcY = pDraw->y;
|
||||
int srcWidth = ctx->srcWidth;
|
||||
|
||||
@@ -432,6 +436,7 @@ bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath
|
||||
}
|
||||
|
||||
unsigned long decodeStart = millis();
|
||||
ctx.lastYieldMs = decodeStart;
|
||||
rc = png->decode(&ctx, 0);
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
|
||||
@@ -793,7 +793,18 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
self->currentPageVisibleOffsetSet = false;
|
||||
}
|
||||
|
||||
// Apply top margin from container block
|
||||
// Apply top margin from container block. Clamp it so the image never
|
||||
// overflows the page bottom: a full-viewport-height image leaves no room
|
||||
// for the margin, and the break above only fires on non-empty pages, so a
|
||||
// fresh page would otherwise place the image at y=marginTop and run
|
||||
// marginTop pixels past viewportHeight. A large bottom reserve (status
|
||||
// bar / big screen margin) absorbs that overflow silently, but with a
|
||||
// thin reserve it crosses the physical screen edge and fails
|
||||
// ImageBlock::render's bounds check, dropping the image entirely.
|
||||
if (self->currentPageNextY + imageMarginTop + displayHeight > self->viewportHeight) {
|
||||
const int room = self->viewportHeight - displayHeight - self->currentPageNextY;
|
||||
imageMarginTop = static_cast<int16_t>(room > 0 ? room : 0);
|
||||
}
|
||||
self->currentPageNextY += imageMarginTop;
|
||||
|
||||
// Create ImageBlock and add to page
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "GfxRenderer.h"
|
||||
|
||||
#include <BidiUtils.h>
|
||||
#include <BoardConfig.h>
|
||||
#include <BuildScratch.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <HalGPIO.h>
|
||||
@@ -1354,6 +1355,12 @@ void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, con
|
||||
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
|
||||
const int sourceWidth = bitmap.getWidth() - cropPixX * 2;
|
||||
const int sourceHeight = bitmap.getHeight() - cropPixY * 2;
|
||||
const int renderedWidth = isScaled ? static_cast<int>(std::floor((sourceWidth - 1) * scale)) + 1 : sourceWidth;
|
||||
const int renderedHeight = isScaled ? static_cast<int>(std::floor((sourceHeight - 1) * scale)) + 1 : sourceHeight;
|
||||
preserveImagePolarity(x, y, renderedWidth, renderedHeight);
|
||||
}
|
||||
|
||||
void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y, const int maxWidth,
|
||||
@@ -1423,6 +1430,45 @@ void GfxRenderer::drawBitmap1Bit(const Bitmap& bitmap, const int x, const int y,
|
||||
|
||||
free(outputRow);
|
||||
free(rowBytes);
|
||||
|
||||
const int renderedWidth =
|
||||
isScaled ? static_cast<int>(std::floor((bitmap.getWidth() - 1) * scale)) + 1 : bitmap.getWidth();
|
||||
const int renderedHeight =
|
||||
isScaled ? static_cast<int>(std::floor((bitmap.getHeight() - 1) * scale)) + 1 : bitmap.getHeight();
|
||||
preserveImagePolarity(x, y, renderedWidth, renderedHeight);
|
||||
}
|
||||
|
||||
void GfxRenderer::preserveImagePolarity(const int x, const int y, const int width, const int height) const {
|
||||
if (renderMode != BW || !display.isInverted() || _stripActive || !frameBuffer || width <= 0 || height <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int ax, ay, bx, by;
|
||||
rotateCoordinates(orientation, x, y, &ax, &ay, panelWidth, panelHeight);
|
||||
rotateCoordinates(orientation, x + width - 1, y + height - 1, &bx, &by, panelWidth, panelHeight);
|
||||
|
||||
int left = std::max(0, std::min(ax, bx));
|
||||
int right = std::min(static_cast<int>(panelWidth) - 1, std::max(ax, bx));
|
||||
int top = std::max(0, std::min(ay, by));
|
||||
int bottom = std::min(static_cast<int>(panelHeight) - 1, std::max(ay, by));
|
||||
if (left > right || top > bottom) return;
|
||||
|
||||
for (int row = top; row <= bottom; row++) {
|
||||
uint8_t* rowData = frameBuffer + static_cast<uint32_t>(row) * panelWidthBytes;
|
||||
int col = left;
|
||||
while (col <= right && (col & 7) != 0) {
|
||||
rowData[col >> 3] ^= static_cast<uint8_t>(0x80U >> (col & 7));
|
||||
col++;
|
||||
}
|
||||
while (col + 7 <= right) {
|
||||
rowData[col >> 3] ^= 0xFF;
|
||||
col += 8;
|
||||
}
|
||||
while (col <= right) {
|
||||
rowData[col >> 3] ^= static_cast<uint8_t>(0x80U >> (col & 7));
|
||||
col++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state) const {
|
||||
@@ -2175,30 +2221,33 @@ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const {
|
||||
}
|
||||
|
||||
void GfxRenderer::getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const {
|
||||
// Board truth: the bezel insets live in the active profile (panel-native
|
||||
// portrait frame); this only rotates them into the current orientation.
|
||||
const BoardConfig::ViewableInsets& vi = BoardConfig::ACTIVE.viewableInsets;
|
||||
switch (orientation) {
|
||||
case Portrait:
|
||||
*outTop = VIEWABLE_MARGIN_TOP;
|
||||
*outRight = VIEWABLE_MARGIN_RIGHT;
|
||||
*outBottom = VIEWABLE_MARGIN_BOTTOM;
|
||||
*outLeft = VIEWABLE_MARGIN_LEFT;
|
||||
*outTop = vi.top;
|
||||
*outRight = vi.right;
|
||||
*outBottom = vi.bottom;
|
||||
*outLeft = vi.left;
|
||||
break;
|
||||
case LandscapeClockwise:
|
||||
*outTop = VIEWABLE_MARGIN_LEFT;
|
||||
*outRight = VIEWABLE_MARGIN_TOP;
|
||||
*outBottom = VIEWABLE_MARGIN_RIGHT;
|
||||
*outLeft = VIEWABLE_MARGIN_BOTTOM;
|
||||
*outTop = vi.left;
|
||||
*outRight = vi.top;
|
||||
*outBottom = vi.right;
|
||||
*outLeft = vi.bottom;
|
||||
break;
|
||||
case PortraitInverted:
|
||||
*outTop = VIEWABLE_MARGIN_BOTTOM;
|
||||
*outRight = VIEWABLE_MARGIN_LEFT;
|
||||
*outBottom = VIEWABLE_MARGIN_TOP;
|
||||
*outLeft = VIEWABLE_MARGIN_RIGHT;
|
||||
*outTop = vi.bottom;
|
||||
*outRight = vi.left;
|
||||
*outBottom = vi.top;
|
||||
*outLeft = vi.right;
|
||||
break;
|
||||
case LandscapeCounterClockwise:
|
||||
*outTop = VIEWABLE_MARGIN_RIGHT;
|
||||
*outRight = VIEWABLE_MARGIN_BOTTOM;
|
||||
*outBottom = VIEWABLE_MARGIN_LEFT;
|
||||
*outLeft = VIEWABLE_MARGIN_TOP;
|
||||
*outTop = vi.right;
|
||||
*outRight = vi.bottom;
|
||||
*outBottom = vi.left;
|
||||
*outLeft = vi.top;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -109,11 +109,6 @@ class GfxRenderer {
|
||||
: display(halDisplay), renderMode(BW), orientation(Portrait), fadingFix(false) {}
|
||||
~GfxRenderer() { freeBwBufferChunks(); }
|
||||
|
||||
static constexpr int VIEWABLE_MARGIN_TOP = 9;
|
||||
static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
|
||||
static constexpr int VIEWABLE_MARGIN_BOTTOM = 3;
|
||||
static constexpr int VIEWABLE_MARGIN_LEFT = 3;
|
||||
|
||||
// Setup
|
||||
void begin(); // must be called right after display.begin()
|
||||
void insertFont(int fontId, EpdFontFamily font);
|
||||
@@ -229,6 +224,9 @@ class GfxRenderer {
|
||||
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0,
|
||||
float cropY = 0) const;
|
||||
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const;
|
||||
// Counter-invert content images in the logical framebuffer so output-level
|
||||
// dark mode leaves their original polarity unchanged.
|
||||
void preserveImagePolarity(int x, int y, int width, int height) const;
|
||||
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const;
|
||||
|
||||
// Snapshot / restore a screen-coordinate framebuffer region (byte-aligned in
|
||||
|
||||
Loaded 20 of 75 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user