311 lines
7.6 KiB
Markdown
311 lines
7.6 KiB
Markdown
# 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 7
|
|
|
|
`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;
|
|
|
|
#define EXPECTED_VERSION 7
|
|
#define MAX_STRING_LENGTH 65535
|
|
|
|
struct String {
|
|
u32 length [[hidden, comment("String byte length")]];
|
|
if (length > MAX_STRING_LENGTH) {
|
|
std::warning(std::format("Unusually large string length: {} bytes", length));
|
|
}
|
|
char data[length] [[comment("UTF-8 string data")]];
|
|
} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]];
|
|
|
|
fn format_string(String s) {
|
|
return s.data;
|
|
};
|
|
|
|
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")]];
|
|
};
|
|
|
|
struct SpineEntry {
|
|
String href [[comment("Resource path")]];
|
|
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")]];
|
|
s16 spineIndex [[comment("Index into spine (-1 if none)")]];
|
|
};
|
|
|
|
struct BookBin {
|
|
u8 version;
|
|
if (version != EXPECTED_VERSION) {
|
|
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
|
}
|
|
|
|
u32 lutOffset [[comment("Offset to lookup tables")]];
|
|
u16 spineCount;
|
|
u16 tocCount;
|
|
|
|
Metadata metadata;
|
|
|
|
u32 currentOffset = $;
|
|
if (currentOffset != lutOffset) {
|
|
std::warning(std::format("LUT offset mismatch: expected 0x{:X}, got 0x{:X}", lutOffset, currentOffset));
|
|
}
|
|
|
|
u32 spineLut[spineCount] [[comment("Spine entry offsets")]];
|
|
u32 tocLut[tocCount] [[comment("TOC entry offsets")]];
|
|
|
|
SpineEntry spines[spineCount];
|
|
TocEntry toc[tocCount];
|
|
};
|
|
|
|
BookBin book @ 0x00;
|
|
|
|
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));
|
|
}
|
|
```
|
|
|
|
## `section.bin`
|
|
|
|
### Version 28
|
|
|
|
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 28 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
|
|
- serialized word style bits for underline, strikethrough, superscript, and
|
|
subscript
|
|
|
|
ImHex pattern:
|
|
|
|
```c++
|
|
import std.mem;
|
|
import std.string;
|
|
import std.core;
|
|
|
|
#define EXPECTED_VERSION 28
|
|
#define MAX_STRING_LENGTH 65535
|
|
#define FOOTNOTE_NUMBER_LEN 32
|
|
#define FOOTNOTE_HREF_LEN 96
|
|
|
|
struct String {
|
|
u32 length [[hidden, comment("String byte length")]];
|
|
if (length > MAX_STRING_LENGTH) {
|
|
std::warning(std::format("Unusually large string length: {} bytes", length));
|
|
}
|
|
char data[length] [[comment("UTF-8 string data")]];
|
|
} [[sealed, format("format_string"), comment("Length-prefixed UTF-8 string")]];
|
|
|
|
fn format_string(String s) {
|
|
return s.data;
|
|
};
|
|
|
|
enum PageElementTag : u8 {
|
|
TAG_PageLine = 1,
|
|
TAG_PageImage = 2,
|
|
TAG_PageHorizontalRule = 3
|
|
};
|
|
|
|
enum WordStyle : u8 {
|
|
REGULAR = 0,
|
|
BOLD = 1,
|
|
ITALIC = 2,
|
|
BOLD_ITALIC = 3,
|
|
UNDERLINE = 4,
|
|
STRIKETHROUGH = 8,
|
|
SUP = 16,
|
|
SUB = 32
|
|
};
|
|
|
|
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;
|
|
TextBlock block;
|
|
};
|
|
|
|
struct PageImage {
|
|
s16 xPos;
|
|
s16 yPos;
|
|
ImageBlock image;
|
|
};
|
|
|
|
struct PageHorizontalRule {
|
|
s16 xPos;
|
|
s16 yPos;
|
|
u16 width;
|
|
u8 thickness;
|
|
};
|
|
|
|
struct PageElement {
|
|
PageElementTag pageElementType;
|
|
if (pageElementType == TAG_PageLine) {
|
|
PageLine pageLine [[inline]];
|
|
} else if (pageElementType == TAG_PageImage) {
|
|
PageImage pageImage [[inline]];
|
|
} 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];
|
|
};
|
|
|
|
struct AnchorEntry {
|
|
String anchor;
|
|
u16 page;
|
|
};
|
|
|
|
struct AnchorMap {
|
|
u16 count;
|
|
AnchorEntry entries[count];
|
|
};
|
|
|
|
struct ParagraphLut {
|
|
u16 count;
|
|
u16 paragraphIndex[count];
|
|
};
|
|
|
|
struct SectionBin {
|
|
u8 version;
|
|
if (version != EXPECTED_VERSION) {
|
|
std::error(std::format("Unsupported version: {} (expected {})", version, EXPECTED_VERSION));
|
|
}
|
|
|
|
s32 fontId;
|
|
float lineCompression;
|
|
bool extraParagraphSpacing;
|
|
u8 paragraphAlignment;
|
|
u16 viewportWidth;
|
|
u16 viewportHeight;
|
|
bool hyphenationEnabled;
|
|
bool embeddedStyle;
|
|
u8 imageRendering;
|
|
bool focusReadingEnabled;
|
|
|
|
u16 pageCount;
|
|
u32 pageLutOffset;
|
|
u32 anchorMapOffset;
|
|
u32 paragraphLutOffset;
|
|
u32 listItemLutOffset;
|
|
|
|
Page pages[pageCount];
|
|
|
|
u32 currentOffset = $;
|
|
if (currentOffset != pageLutOffset) {
|
|
std::warning(std::format("Page LUT offset mismatch: expected 0x{:X}, got 0x{:X}", pageLutOffset, currentOffset));
|
|
}
|
|
|
|
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;
|
|
}
|
|
};
|
|
|
|
SectionBin section @ 0x00;
|
|
|
|
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));
|
|
}
|
|
```
|