Compare commits

..
Author SHA1 Message Date
Uri Tauber 95a847c721 Merge branch 'develop' 2026-08-07 10:04:31 +03:00
75 changed files with 731 additions and 1510 deletions

No files matched your search

+8 -52
View File
@@ -63,8 +63,7 @@ jobs:
- name: Run cppcheck
run: pio check --fail-on-defect low --fail-on-defect medium --fail-on-defect high
build-default:
name: Build default
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
@@ -91,10 +90,14 @@ jobs:
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
restore-keys: pio-${{ runner.os }}-
- name: Build default firmware
- 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.
run: |
set -euo pipefail
pio run -e default | tee pio.log
pio run -e default -e sticky | tee pio.log
- name: Extract firmware stats
@@ -117,52 +120,6 @@ 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:
@@ -195,8 +152,7 @@ jobs:
test-status:
name: Test Status
needs:
- build-default
- build-sticky
- build
- clang-format
- cppcheck
- unit-tests
+47
View File
@@ -0,0 +1,47 @@
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
-1
View File
@@ -31,4 +31,3 @@ lib/EpdFont/scripts/output/
/dependencies.lock
/sdkconfig.*
sdkconfig.sticky
flake.*
+68 -179
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
/home/urit/Documents/CrossPoint/crosspoint-reader/.skills/SKILL.md
+4 -6
View File
@@ -10,10 +10,9 @@ 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: 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: `.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.
## Setting Up a Dictionary
@@ -36,14 +35,13 @@ 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 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.
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.
### How Lookup Works
1. **Direct match** — the word is found as-is (case-insensitive) in the dictionary index. Surrounding punctuation is ignored.
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.
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.
## The Definition Screen
+3 -10
View File
@@ -90,19 +90,12 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 37
### Version 35
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.
@@ -142,10 +135,10 @@ import std.mem;
import std.string;
import std.core;
#define EXPECTED_VERSION 37
#define EXPECTED_VERSION 35
#define MAX_STRING_LENGTH 65535
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 256
#define FOOTNOTE_HREF_LEN 96
struct String {
u32 length [[hidden, comment("String byte length")]];
+1 -6
View File
@@ -802,12 +802,7 @@ int Epub::getSpineItemsCount() const {
return bookMetadataCache->getSpineCount();
}
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
return 0;
}
return bookMetadataCache->getCumulativeSize(spineIndex);
}
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
-19
View File
@@ -481,30 +481,11 @@ 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");
-9
View File
@@ -7,7 +7,6 @@
#include <deque>
#include <memory>
#include <string>
#include <vector>
class BookMetadataCache {
public:
@@ -63,11 +62,6 @@ 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
@@ -119,9 +113,6 @@ 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; }
+1 -4
View File
@@ -3,10 +3,7 @@
#include <cstring>
#define FOOTNOTE_NUMBER_LEN 32
#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).
#define FOOTNOTE_HREF_LEN 96
struct FootnoteEntry {
char number[FOOTNOTE_NUMBER_LEN];
+1 -6
View File
@@ -31,12 +31,7 @@ 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.
// 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;
constexpr uint8_t SECTION_FILE_VERSION = 36;
// 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
-2
View File
@@ -357,7 +357,6 @@ 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
}
@@ -420,7 +419,6 @@ void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
return;
}
renderer.preserveImagePolarity(x, y, width, height);
LOG_DBG("IMG", "Decode successful");
}
@@ -1,28 +1,16 @@
#include "ImageToFramebufferDecoder.h"
#include <Arduino.h>
#include <Logging.h>
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
// 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));
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);
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,21 +31,9 @@ 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. 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)
// Size validation helpers
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536
bool validateImageDimensions(int width, int height, const std::string& format);
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
@@ -46,8 +46,6 @@ 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*,
@@ -124,8 +122,6 @@ 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);
@@ -498,7 +494,6 @@ 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,8 +38,6 @@ 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*,
@@ -208,8 +206,6 @@ 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;
@@ -436,7 +432,6 @@ 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,18 +793,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
self->currentPageVisibleOffsetSet = false;
}
// 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);
}
// Apply top margin from container block
self->currentPageNextY += imageMarginTop;
// Create ImageBlock and add to page
+16 -65
View File
@@ -1,7 +1,6 @@
#include "GfxRenderer.h"
#include <BidiUtils.h>
#include <BoardConfig.h>
#include <BuildScratch.h>
#include <FontDecompressor.h>
#include <HalGPIO.h>
@@ -1355,12 +1354,6 @@ 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,
@@ -1430,45 +1423,6 @@ 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 {
@@ -2221,33 +2175,30 @@ 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 = vi.top;
*outRight = vi.right;
*outBottom = vi.bottom;
*outLeft = vi.left;
*outTop = VIEWABLE_MARGIN_TOP;
*outRight = VIEWABLE_MARGIN_RIGHT;
*outBottom = VIEWABLE_MARGIN_BOTTOM;
*outLeft = VIEWABLE_MARGIN_LEFT;
break;
case LandscapeClockwise:
*outTop = vi.left;
*outRight = vi.top;
*outBottom = vi.right;
*outLeft = vi.bottom;
*outTop = VIEWABLE_MARGIN_LEFT;
*outRight = VIEWABLE_MARGIN_TOP;
*outBottom = VIEWABLE_MARGIN_RIGHT;
*outLeft = VIEWABLE_MARGIN_BOTTOM;
break;
case PortraitInverted:
*outTop = vi.bottom;
*outRight = vi.left;
*outBottom = vi.top;
*outLeft = vi.right;
*outTop = VIEWABLE_MARGIN_BOTTOM;
*outRight = VIEWABLE_MARGIN_LEFT;
*outBottom = VIEWABLE_MARGIN_TOP;
*outLeft = VIEWABLE_MARGIN_RIGHT;
break;
case LandscapeCounterClockwise:
*outTop = vi.right;
*outRight = vi.bottom;
*outBottom = vi.left;
*outLeft = vi.top;
*outTop = VIEWABLE_MARGIN_RIGHT;
*outRight = VIEWABLE_MARGIN_BOTTOM;
*outBottom = VIEWABLE_MARGIN_LEFT;
*outLeft = VIEWABLE_MARGIN_TOP;
break;
}
}
+5 -3
View File
@@ -109,6 +109,11 @@ 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);
@@ -224,9 +229,6 @@ 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