Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
2e2ea6a
feat: Page turn button orientation change (#1069)
mchuck May 4, 2026
a5fac32
refactor: Simplify sort in GfxRenderer::fillPolygon (#1817)
znelson May 4, 2026
6c4ae7c
refactor: Avoid vector for page turn rates list (#1818)
znelson May 4, 2026
a1007a4
fix: Track block style stack for nested styles (#1582)
daveallie May 4, 2026
e026bcb
fix: Track block style stack for nested styles (#1582)
daveallie May 4, 2026
333286a
refactor: Use std::size instead of sizeof/sizeof (#1819)
znelson May 4, 2026
b8a6b58
docs: expand first use of OPDS acronym and provide a wikipedia link (…
sizezero May 4, 2026
5717374
feat(update): SD-card firmware update + X3 bootloader compatibility (…
eunchurn May 4, 2026
adcd796
feat: self-heal from transient WiFi loss, add dBm indicator during We…
jeremydk May 5, 2026
78625af
chore: Added RAM to firmware_size_history.py script (#1830)
znelson May 5, 2026
40af426
refactor: change ukrainian translation to adaptation and add missing …
KymAndriy May 5, 2026
f44722a
fix: swedish translation (#1829)
steka May 5, 2026
efa4f71
feat: Set sleep cover from BMP viewer (#1104)
el May 5, 2026
3e7d63d
style: align action buttons vertically with page title (#1795)
fain182 May 5, 2026
6bad014
Merge branch 'uxjulia:main' into devFusion
wildfire070 May 6, 2026
939014d
fix: incorrect y-axis scale factor in jpeg nearest-neighbor downscale…
WuTofu May 6, 2026
4ea938b
chore: Update SDK to fork in CrossPoint org (#1836)
znelson May 6, 2026
cfe3a94
refactor: Simplify XtcReaderActivity with detectPageTurn (#1837)
znelson May 6, 2026
395e68e
refactor: Simplify isReaderActivity bookkeeping (#1838)
znelson May 6, 2026
66202a4
Merge branch 'uxjulia:main' into devFusion
wildfire070 May 6, 2026
144baf2
Merge branch 'uxjulia:main' into devFusion
wildfire070 May 7, 2026
11bc36e
refactor: Use fixed-size integers for BookMetadataCache data (#1844)
znelson May 7, 2026
dadce51
fix: display empty lines in txt reader (#1841)
Uri-Tauber May 7, 2026
c15b5b9
fix: short-press power action triggered after screenshot combo releas…
pablohc May 7, 2026
83f0cee
fix: Roundraff theme home menu offset with no recent books (#1845)
znelson May 7, 2026
6709e5c
chore: merge upstream master into main
uxjulia May 7, 2026
5e59a80
Merge branch 'main' into devFusion
wildfire070 May 7, 2026
bf3e691
Fix: hidden item check in WebDAVHandler
wildfire070 May 7, 2026
e1c04c4
Fix: hidden item check in WebDAVHandler
wildfire070 May 7, 2026
87ae8ce
Update WebDAVHandler.cpp
wildfire070 May 7, 2026
8369714
WebDAVHandler.cpp clang-fix
wildfire070 May 7, 2026
1570e12
Fix WebDAVHandler.cpp
wildfire070 May 8, 2026
35e3ba5
Merge pull request #8 from wildfire070/devFusion
wildfire070 May 8, 2026
bc0b877
Merge branch 'CapInkFusion' into main
wildfire070 May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
[submodule "open-x4-sdk"]
path = open-x4-sdk
url = https://github.com/open-x4-epaper/community-sdk.git
url = https://github.com/crosspoint-reader/community-sdk.git
2 changes: 1 addition & 1 deletion USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ The Settings screen allows you to configure the device's behavior. There are a f

- **WiFi Networks**: Connect to WiFi networks for file transfers and firmware updates.
- **KOReader Sync**: Options for setting up KOReader for syncing book progress.
- **OPDS Servers**: Manage one or more OPDS libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
- **OPDS Servers**: Manage one or more OPDS [(Open Publication Distribution System)](https://en.wikipedia.org/wiki/Open_Publication_Distribution_System) libraries for browsing and downloading books. See [OPDS Servers (Multiple Libraries)](#365-opds-servers-multiple-libraries) below.
- **Clear Reading Cache**: Clear the internal SD card cache.
- **Check for updates**: Check for CrossInk firmware updates over WiFi.
- **Language**: Set the system language (see **[Supported Languages](#supported-languages)** for more information).
Expand Down
1 change: 1 addition & 0 deletions docs/translators.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ If you'd like to add your name to this list, please open a PR adding yourself an

## Ukrainian
- [mirus-ua](https://github.com/mirus-ua)
- [KymAndriy](https://github.com/KymAndriy)

## Belarusian
- [Dexif](https://github.com/dexif)
Expand Down
2 changes: 1 addition & 1 deletion lib/Epub/Epub/Section.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
#include "parsers/ChapterHtmlSlimParser.h"

namespace {
constexpr uint8_t SECTION_FILE_VERSION = 30;
constexpr uint8_t SECTION_FILE_VERSION = 31;
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(bool) +
sizeof(uint8_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) +
sizeof(bool) + sizeof(bool) + sizeof(uint8_t) + sizeof(bool) + sizeof(bool) +
Expand Down
79 changes: 48 additions & 31 deletions lib/Epub/Epub/blocks/BlockStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,45 +34,62 @@ struct BlockStyle {
// NOT propagated through getCombinedBlockStyle so it can't leak into sibling blocks.
bool fromBrElement = false;

// Combined horizontal insets (margin + padding)
// Combined insets (margin + padding)
[[nodiscard]] int16_t leftInset() const { return marginLeft + paddingLeft; }
[[nodiscard]] int16_t rightInset() const { return marginRight + paddingRight; }
[[nodiscard]] int16_t totalHorizontalInset() const { return leftInset() + rightInset(); }
[[nodiscard]] int16_t topInset() const { return marginTop + paddingTop; }
[[nodiscard]] int16_t bottomInset() const { return marginBottom + paddingBottom; }

// Combine with another block style. Useful for parent -> child styles, where the child style should be
// applied on top of the parent's style to get the combined style.
BlockStyle getCombinedBlockStyle(const BlockStyle& child) const {
BlockStyle combinedBlockStyle;
// Return a copy with bottom margins/padding zeroed out.
[[nodiscard]] BlockStyle withoutBottom() const {
BlockStyle result = *this;
result.marginBottom = 0;
result.paddingBottom = 0;
return result;
}

combinedBlockStyle.marginTop = static_cast<int16_t>(child.marginTop + marginTop);
combinedBlockStyle.marginBottom = static_cast<int16_t>(child.marginBottom + marginBottom);
combinedBlockStyle.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
combinedBlockStyle.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
// Return a copy with bottom margins/padding collapsed (max) with the source's.
// Uses CSS margin collapsing: adjacent parent-child margins resolve to the larger value.
[[nodiscard]] BlockStyle addBottom(const BlockStyle& source) const {
BlockStyle result = *this;
result.marginBottom = std::max(marginBottom, source.marginBottom);
result.paddingBottom = static_cast<int16_t>(paddingBottom + source.paddingBottom);
return result;
}

combinedBlockStyle.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
combinedBlockStyle.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
combinedBlockStyle.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
combinedBlockStyle.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
// Text indent: use child's if defined
if (child.textIndentDefined) {
combinedBlockStyle.textIndent = child.textIndent;
combinedBlockStyle.textIndentDefined = true;
} else {
combinedBlockStyle.textIndent = textIndent;
combinedBlockStyle.textIndentDefined = textIndentDefined;
}
// Text align: use child's if defined
if (child.textAlignDefined) {
combinedBlockStyle.alignment = child.alignment;
combinedBlockStyle.textAlignDefined = true;
enum class CombineAxis : uint8_t {
Horizontal = 1, // margins left/right, padding left/right, text-align, text-indent
Vertical = 2, // margins top/bottom, padding top/bottom
};

// Combine this style's properties with a child style along the specified axis.
// Properties on the other axis are kept from the child unchanged.
[[nodiscard]] BlockStyle getCombinedBlockStyle(const BlockStyle& child, CombineAxis axis) const {
BlockStyle result = child;

if (axis == CombineAxis::Horizontal) {
result.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
result.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
result.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
result.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
if (!child.textIndentDefined && textIndentDefined) {
result.textIndent = textIndent;
result.textIndentDefined = true;
}
if (!child.textAlignDefined && textAlignDefined) {
result.alignment = alignment;
result.textAlignDefined = true;
}
} else {
combinedBlockStyle.alignment = alignment;
combinedBlockStyle.textAlignDefined = textAlignDefined;
result.marginTop = std::max(child.marginTop, marginTop);
result.marginBottom = std::max(child.marginBottom, marginBottom);
result.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
result.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
}
// fromBrElement is never propagated — it is consumed by startNewTextBlock
// when the empty <br> block is merged with the following paragraph.
combinedBlockStyle.fromBrElement = false;
return combinedBlockStyle;
// fromBrElement is consumed by startNewTextBlock and should not leak through ancestor style merging.
result.fromBrElement = false;
return result;
}

// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
Expand Down
60 changes: 39 additions & 21 deletions lib/Epub/Epub/converters/JpegToFramebufferConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,15 @@ struct JpegContext {
int dstWidth{0};
int dstHeight{0};

// Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU)
int32_t fineScaleFP{1 << 16}; // src -> dst mapping
int32_t invScaleFP{1 << 16}; // dst -> src mapping
// Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU).
// X and Y axes use separate scale factors: the aspect ratio of the output (dstWidth/dstHeight)
// may differ from the source (srcWidth/srcHeight) due to integer rounding of displayHeight.
// Using a single (X-based) scale for both axes causes the wrong srcRow to be skipped
// during nearest-neighbor downscaling, potentially losing critical image content.
int32_t fineScaleFPX{1 << 16}; // X: src -> dst column mapping
int32_t invScaleFPX{1 << 16}; // X: dst -> src column mapping
int32_t fineScaleFPY{1 << 16}; // Y: src -> dst row mapping
int32_t invScaleFPY{1 << 16}; // Y: dst -> src row mapping

PixelCache cache;
bool caching{false};
Expand Down Expand Up @@ -125,8 +131,10 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {

const bool useDithering = ctx->config->useDithering;
const bool caching = ctx->caching;
const int32_t fineScaleFP = ctx->fineScaleFP;
const int32_t invScaleFP = ctx->invScaleFP;
const int32_t fineScaleFPX = ctx->fineScaleFPX;
const int32_t invScaleFPX = ctx->invScaleFPX;
const int32_t fineScaleFPY = ctx->fineScaleFPY;
const int32_t invScaleFPY = ctx->invScaleFPY;
GfxRenderer& renderer = *ctx->renderer;
const int cfgX = ctx->config->x;
const int cfgY = ctx->config->y;
Expand All @@ -137,10 +145,10 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
const int srcYEnd = blockY + blockH;
const int srcXEnd = blockX + validW;

int dstYStart = (int)((int64_t)blockY * fineScaleFP >> FP_SHIFT);
int dstYEnd = (srcYEnd >= ctx->scaledSrcHeight) ? ctx->dstHeight : (int)((int64_t)srcYEnd * fineScaleFP >> FP_SHIFT);
int dstXStart = (int)((int64_t)blockX * fineScaleFP >> FP_SHIFT);
int dstXEnd = (srcXEnd >= ctx->scaledSrcWidth) ? ctx->dstWidth : (int)((int64_t)srcXEnd * fineScaleFP >> FP_SHIFT);
int dstYStart = (int)((int64_t)blockY * fineScaleFPY >> FP_SHIFT);
int dstYEnd = (srcYEnd >= ctx->scaledSrcHeight) ? ctx->dstHeight : (int)((int64_t)srcYEnd * fineScaleFPY >> FP_SHIFT);
int dstXStart = (int)((int64_t)blockX * fineScaleFPX >> FP_SHIFT);
int dstXEnd = (srcXEnd >= ctx->scaledSrcWidth) ? ctx->dstWidth : (int)((int64_t)srcXEnd * fineScaleFPX >> FP_SHIFT);

// Pre-clamp destination ranges to screen bounds (eliminates per-pixel screen checks)
int clampYMax = ctx->dstHeight;
Expand All @@ -165,7 +173,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
}

// === 1:1 fast path: no scaling math ===
if (fineScaleFP == FP_ONE) {
if (fineScaleFPX == FP_ONE && fineScaleFPY == FP_ONE) {
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
Expand All @@ -191,11 +199,11 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
// === Bilinear interpolation (upscale: fineScale > 1.0) ===
// Smooths block boundaries that would otherwise create visible banding
// on progressive JPEG DC-only decode (1/8 resolution upscaled to target).
if (fineScaleFP > FP_ONE) {
if (fineScaleFPX > FP_ONE && fineScaleFPY > FP_ONE) {
// Pre-compute safe X range where lx0 and lx0+1 are both in [0, validW-1].
// Only the left/right edge pixels (typically 0-2 and 1-8 respectively) need clamping.
int safeXStart = (int)(((int64_t)blockX * fineScaleFP + FP_MASK) >> FP_SHIFT);
int safeXEnd = (int)((int64_t)(blockX + validW - 1) * fineScaleFP >> FP_SHIFT);
int safeXStart = (int)(((int64_t)blockX * fineScaleFPX + FP_MASK) >> FP_SHIFT);
int safeXEnd = (int)((int64_t)(blockX + validW - 1) * fineScaleFPX >> FP_SHIFT);
if (safeXStart < dstXStart) safeXStart = dstXStart;
if (safeXEnd > dstXEnd) safeXEnd = dstXEnd;
if (safeXStart > safeXEnd) safeXEnd = safeXStart;
Expand All @@ -204,7 +212,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP;
const int32_t srcFyFP = dstY * invScaleFPY;
const int32_t fy = srcFyFP & FP_MASK;
const int32_t fyInv = FP_ONE - fy;
int ly0 = (srcFyFP >> FP_SHIFT) - blockY;
Expand All @@ -219,7 +227,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
// Left edge (with X boundary clamping)
for (int dstX = dstXStart; dstX < safeXStart; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t srcFxFP = dstX * invScaleFPX;
const int32_t fx = srcFxFP & FP_MASK;
const int32_t fxInv = FP_ONE - fx;
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
Expand Down Expand Up @@ -247,7 +255,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
// Interior (no X boundary checks — lx0 and lx0+1 guaranteed in bounds)
for (int dstX = safeXStart; dstX < safeXEnd; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t srcFxFP = dstX * invScaleFPX;
const int32_t fx = srcFxFP & FP_MASK;
const int32_t fxInv = FP_ONE - fx;
const int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
Expand All @@ -270,7 +278,7 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
// Right edge (with X boundary clamping)
for (int dstX = safeXEnd; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t srcFxFP = dstX * invScaleFPX;
const int32_t fx = srcFxFP & FP_MASK;
const int32_t fxInv = FP_ONE - fx;
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
Expand Down Expand Up @@ -301,15 +309,15 @@ int jpegDrawCallback(JPEGDRAW* pDraw) {
const int outY = cfgY + dstY;
pw.beginRow(outY);
if (caching) cw.beginRow(outY, ctx->config->y);
const int32_t srcFyFP = dstY * invScaleFP;
const int32_t srcFyFP = dstY * invScaleFPY;
int ly = (srcFyFP >> FP_SHIFT) - blockY;
if (ly < 0) ly = 0;
if (ly >= blockH) ly = blockH - 1;
const uint8_t* row = &pixels[ly * stride];

for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
const int outX = cfgX + dstX;
const int32_t srcFxFP = dstX * invScaleFP;
const int32_t srcFxFP = dstX * invScaleFPX;
int lx = (srcFxFP >> FP_SHIFT) - blockX;
if (lx < 0) lx = 0;
if (lx >= validW) lx = validW - 1;
Expand Down Expand Up @@ -442,12 +450,22 @@ bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePat
jpegScaleDenom = chooseJpegScale(targetScale, jpegScaleOption);
}

if (destWidth <= 0 || destHeight <= 0) {
LOG_ERR("JPG", "Degenerate output dimensions %dx%d for %s, skipping render", destWidth, destHeight,
imagePath.c_str());
jpeg->close();
delete jpeg;
return false;
}

ctx.scaledSrcWidth = (srcWidth + jpegScaleDenom - 1) / jpegScaleDenom;
ctx.scaledSrcHeight = (srcHeight + jpegScaleDenom - 1) / jpegScaleDenom;
ctx.dstWidth = destWidth;
ctx.dstHeight = destHeight;
ctx.fineScaleFP = (int32_t)((int64_t)destWidth * FP_ONE / ctx.scaledSrcWidth);
ctx.invScaleFP = (int32_t)((int64_t)ctx.scaledSrcWidth * FP_ONE / destWidth);
ctx.fineScaleFPX = (int32_t)((int64_t)destWidth * FP_ONE / ctx.scaledSrcWidth);
ctx.invScaleFPX = (int32_t)((int64_t)ctx.scaledSrcWidth * FP_ONE / destWidth);
ctx.fineScaleFPY = (int32_t)((int64_t)destHeight * FP_ONE / ctx.scaledSrcHeight);
ctx.invScaleFPY = (int32_t)((int64_t)ctx.scaledSrcHeight * FP_ONE / destHeight);

LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f, jpegScale 1/%d, fineScale %.2f)%s", srcWidth, srcHeight, destWidth,
destHeight, targetScale, jpegScaleDenom, (float)destWidth / ctx.scaledSrcWidth,
Expand Down
7 changes: 3 additions & 4 deletions lib/Epub/Epub/htmlEntities.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "htmlEntities.h"

#include <cstring>
#include <iterator>

struct EntityPair {
const char* key;
Expand Down Expand Up @@ -62,8 +63,6 @@ static constexpr EntityPair ENTITY_LOOKUP[] = {
{"&yen;", "¥"}, {"&yuml;", "ÿ"}, {"&zeta;", "ζ"}, {"&zwj;", "\u200D"}, {"&zwnj;", "\u200C"},
};

static const size_t ENTITY_LOOKUP_COUNT = sizeof(ENTITY_LOOKUP) / sizeof(ENTITY_LOOKUP[0]);

// Verify the table is sorted at compile time.
static constexpr int constexprStrcmp(const char* a, const char* b) {
for (size_t i = 0;; i++) {
Expand All @@ -73,7 +72,7 @@ static constexpr int constexprStrcmp(const char* a, const char* b) {
}

static constexpr bool isTableSorted() {
for (size_t i = 1; i < ENTITY_LOOKUP_COUNT; i++) {
for (size_t i = 1; i < std::size(ENTITY_LOOKUP); i++) {
if (constexprStrcmp(ENTITY_LOOKUP[i - 1].key, ENTITY_LOOKUP[i].key) >= 0) return false;
}
return true;
Expand All @@ -85,7 +84,7 @@ const char* lookupHtmlEntity(const char* entity, size_t len) {
if (entity == nullptr || len == 0) return nullptr;

size_t lo = 0;
size_t hi = ENTITY_LOOKUP_COUNT;
size_t hi = std::size(ENTITY_LOOKUP);

while (lo < hi) {
const size_t mid = lo + (hi - lo) / 2;
Expand Down
Loading
Loading