docs: refresh cache formats and web server workflows (#2233)

## Summary

* **What is the goal of this PR?** Update project documentation to match
the current master implementation for cache formats, i18n, file
transfer/web server workflows, SD-card fonts, and root user-facing docs.
* **What changes are included?** Refreshes `book.bin`/`section.bin` docs
for v6/v25, updates File Transfer/Calibre/WebDAV/API docs, documents 24
UI languages and JSON language persistence, updates root
README/USER_GUIDE cache and network details, and syncs the tracked
CLAUDE skill doc cache-version notes.

## Additional Context

* Docs-only change. Verified with `git diff --check origin/master..HEAD`
and stale-reference greps for old cache versions, removed i18n APIs, old
WiFi screen wording, and raw `Serial.printf` examples. No firmware build
was run.

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**NO**_
This commit is contained in:
Leopoldo Pla Sempere
2026-06-07 15:47:44 -04:00
committed by GitHub
parent 3a1e9f3023
commit 7f01eab62e
12 changed files with 789 additions and 594 deletions
+152 -87
View File
@@ -1,22 +1,26 @@
# File Formats
These formats describe the SD-card cache files under `/.crosspoint/epub_<hash>/`.
All POD fields are written in the ESP32 little-endian representation used by
`Serialization.h`; strings are length-prefixed UTF-8.
## `book.bin`
### Version 5
### Version 7
ImHex Pattern:
`book.bin` stores EPUB metadata plus lookup tables for spine and TOC entries.
The current firmware writes this version from `BookMetadataCache`.
ImHex pattern:
```c++
import std.mem;
import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 5
#define EXPECTED_VERSION 7
#define MAX_STRING_LENGTH 65535
// === String Structure ===
struct String {
u32 length [[hidden, comment("String byte length")]];
if (length > MAX_STRING_LENGTH) {
@@ -29,75 +33,56 @@ fn format_string(String s) {
return s.data;
};
// === Metadata Structure ===
struct Metadata {
String title [[comment("Book title")]];
String author [[comment("Book author")]];
String language [[comment("Book language code")]];
String coverItemHref [[comment("Path to cover image")]];
String textReferenceHref [[comment("Path to guided first text reference")]];
} [[comment("Book metadata information")]];
// === Spine Entry Structure ===
};
struct SpineEntry {
String href [[comment("Resource path")]];
u32 cumulativeSize [[comment("Cumulative size in bytes"), color("FF6B6B")]];
s16 tocIndex [[comment("Index into TOC (-1 if none)"), color("4ECDC4")]];
} [[comment("Spine entry defining reading order")]];
// === TOC Entry Structure ===
u32 cumulativeSize [[comment("Cumulative uncompressed spine size through this entry")]];
s16 tocIndex [[comment("Index into TOC, or inherited/previous TOC index when no direct entry exists")]];
};
struct TocEntry {
String title [[comment("Chapter/section title")]];
String href [[comment("Resource path")]];
String anchor [[comment("Fragment identifier")]];
u8 level [[comment("Nesting level (0-255)"), color("95E1D3")]];
s16 spineIndex [[comment("Index into spine (-1 if none)"), color("F38181")]];
} [[comment("Table of contents entry")]];
// === Book Bin Structure ===
u8 level [[comment("Nesting level")]];
s16 spineIndex [[comment("Index into spine (-1 if none)")]];
};
struct BookBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
u8 version;
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
u32 lutOffset [[comment("Offset to lookup tables"), color("6BCB77")]];
u16 spineCount [[comment("Number of spine entries"), color("4D96FF")]];
u16 tocCount [[comment("Number of TOC entries"), color("FF6B9D")]];
u32 lutOffset [[comment("Offset to lookup tables")]];
u16 spineCount;
u16 tocCount;
// Metadata section
Metadata metadata [[comment("Book metadata")]];
Metadata metadata;
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
}
// Lookup Tables
u32 spineLut[spineCount] [[comment("Spine entry offsets"), color("4D96FF")]];
u32 tocLut[tocCount] [[comment("TOC entry offsets"), color("FF6B9D")]];
u32 spineLut[spineCount] [[comment("Spine entry offsets")]];
u32 tocLut[tocCount] [[comment("TOC entry offsets")]];
// Data Entries
SpineEntry spines[spineCount] [[comment("Spine entries (reading order)")]];
TocEntry toc[tocCount] [[comment("Table of contents entries")]];
SpineEntry spines[spineCount];
TocEntry toc[tocCount];
};
// === File Parsing ===
BookBin book @ 0x00;
// Validate we've consumed the entire file
u32 fileSize = std::mem::size();
u32 parsedSize = $;
if (parsedSize != fileSize) {
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
}
@@ -105,20 +90,33 @@ if (parsedSize != fileSize) {
## `section.bin`
### Version 24
### Version 25
ImHex Pattern:
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 25 includes:
- cache-busting fields for paragraph alignment, hyphenation, embedded CSS,
image rendering mode, and Focus Reading
- page offset LUT
- anchor-to-page map for fragment and footnote navigation
- paragraph and list-item LUTs used by KOReader sync page refinement
- optional per-word Focus Reading split metadata
- per-page footnote entries
ImHex pattern:
```c++
import std.mem;
import std.string;
import std.core;
// === Configuration ===
#define EXPECTED_VERSION 24
#define EXPECTED_VERSION 25
#define MAX_STRING_LENGTH 65535
// === String Structure ===
#define FOOTNOTE_NUMBER_LEN 32
#define FOOTNOTE_HREF_LEN 96
struct String {
u32 length [[hidden, comment("String byte length")]];
@@ -132,44 +130,79 @@ fn format_string(String s) {
return s.data;
};
// === Page Structure ===
enum PageElementTag : u8 {
PageLine = 1,
PageImage = 2,
PageHorizontalRule = 3
TAG_PageLine = 1,
TAG_PageImage = 2,
TAG_PageHorizontalRule = 3
};
enum WordStyle : u8 {
REGULAR = 0,
BOLD = 1,
ITALIC = 2,
BOLD_ITALIC = 3
BOLD_ITALIC = 3,
UNDERLINE = 4,
STRIKETHROUGH = 8,
SUP = 16,
SUB = 32
};
enum BlockStyle : u8 {
enum TextAlign : u8 {
JUSTIFIED = 0,
LEFT_ALIGN = 1,
CENTER_ALIGN = 2,
RIGHT_ALIGN = 3,
NONE = 4
};
struct BlockStyle {
TextAlign alignment;
bool textAlignDefined;
s16 marginTop;
s16 marginBottom;
s16 marginLeft;
s16 marginRight;
s16 paddingTop;
s16 paddingBottom;
s16 paddingLeft;
s16 paddingRight;
s16 textIndent;
bool textIndentDefined;
bool isRtl;
bool directionDefined;
};
struct TextBlock {
u16 wordCount;
String words[wordCount];
s16 wordXPos[wordCount];
WordStyle wordStyle[wordCount];
u8 hasFocus;
if (hasFocus != 0) {
u8 wordFocusBoundary[wordCount] [[comment("UTF-8 byte boundary between bold prefix and suffix")]];
u16 wordFocusSuffixX[wordCount] [[comment("Suffix x offset from word start")]];
}
BlockStyle blockStyle;
};
struct ImageBlock {
String imagePath;
s16 width;
s16 height;
};
struct PageLine {
s16 xPos;
s16 yPos;
u16 wordCount;
String words[wordCount];
u16 wordXPos[wordCount];
WordStyle wordStyle[wordCount];
BlockStyle blockStyle;
s16 xPos;
s16 yPos;
TextBlock block;
};
struct PageImage {
s16 xPos;
s16 yPos;
String imagePath;
s16 width;
s16 height;
ImageBlock image;
};
struct PageHorizontalRule {
@@ -180,63 +213,95 @@ struct PageHorizontalRule {
};
struct PageElement {
u8 pageElementType;
if (pageElementType == 1) {
PageElementTag pageElementType;
if (pageElementType == TAG_PageLine) {
PageLine pageLine [[inline]];
} else if (pageElementType == 2) {
} else if (pageElementType == TAG_PageImage) {
PageImage pageImage [[inline]];
} else if (pageElementType == 3) {
} else if (pageElementType == TAG_PageHorizontalRule) {
PageHorizontalRule horizontalRule [[inline]];
} else {
std::error(std::format("Unknown page element type: {}", pageElementType));
}
};
struct FootnoteEntry {
char number[FOOTNOTE_NUMBER_LEN];
char href[FOOTNOTE_HREF_LEN];
};
struct Page {
u16 elementCount;
PageElement elements[elementCount] [[inline]];
u16 footnoteCount;
FootnoteEntry footnotes[footnoteCount];
};
// === Section Bin Structure ===
struct AnchorEntry {
String anchor;
u16 page;
};
struct AnchorMap {
u16 count;
AnchorEntry entries[count];
};
struct ParagraphLut {
u16 count;
u16 paragraphIndex[count];
};
struct SectionBin {
// Header
u8 version [[comment("Format version"), color("FFD93D")]];
// Version validation
u8 version;
if (version != EXPECTED_VERSION) {
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
}
// Cache busting parameters
s32 fontId;
float lineCompression;
bool extraParagraphSpacing;
u8 paragraphAlignment;
u16 viewportWidth;
u16 vieportHeight;
u16 viewportHeight;
bool hyphenationEnabled;
bool embeddedStyle;
u8 imageRendering;
bool focusReadingEnabled;
u16 pageCount;
u32 lutOffset;
u32 pageLutOffset;
u32 anchorMapOffset;
u32 paragraphLutOffset;
u32 listItemLutOffset;
Page page[pageCount];
Page pages[pageCount];
// Validate LUT offset alignment
u32 currentOffset = $;
if (currentOffset != lutOffset) {
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
if (currentOffset != pageLutOffset) {
std::warning(std::format("Page LUT offset mismatch: expected 0x{:X}, got 0x{:X}", pageLutOffset, currentOffset));
}
// Lookup Tables
u32 lut[pageCount];
u32 pageLut[pageCount] [[comment("Page data offsets")]];
if (anchorMapOffset != 0) {
AnchorMap anchorMap @ anchorMapOffset;
}
if (paragraphLutOffset != 0) {
ParagraphLut paragraphLut @ paragraphLutOffset;
}
if (listItemLutOffset != 0 && paragraphLutOffset != 0) {
u16 listItemIndex[paragraphLut.count] @ listItemLutOffset;
}
};
// === File Parsing ===
SectionBin section @ 0x00;
SectionBin book @ 0x00;
// Validate we've consumed the entire file
u32 fileSize = std::mem::size();
u32 parsedSize = $;
if (parsedSize != fileSize) {
std::warning(std::format("Unparsed data detected: {} bytes remaining at offset 0x{:X}", fileSize - parsedSize, parsedSize));
}