mirror of
https://github.com/daveallie/crosspoint-reader.git
synced 2026-02-04 14:47:37 +03:00
Compare commits
13 Commits
9115fb43bc
...
2f8a40d09c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f8a40d09c | ||
|
|
9e1356bb92 | ||
|
|
cd61b263e5 | ||
|
|
6115bf3cd2 | ||
|
|
a7ffc02c34 | ||
|
|
d564173949 | ||
|
|
f0f6618246 | ||
|
|
0fa4bb4100 | ||
|
|
53931b3693 | ||
|
|
9d58952ba7 | ||
|
|
78d6e5931c | ||
|
|
dac11c3fdd | ||
|
|
d403044f76 |
@ -102,13 +102,18 @@ After flashing the new features, it’s recommended to capture detailed logs fro
|
||||
First, make sure all required Python packages are installed:
|
||||
|
||||
```python
|
||||
python3 -m pip install serial colorama matplotlib
|
||||
python3 -m pip install pyserial colorama matplotlib
|
||||
```
|
||||
after that run the script:
|
||||
```sh
|
||||
# For Linux
|
||||
# This was tested on Debian and should work on most Linux systems.
|
||||
python3 scripts/debugging_monitor.py
|
||||
|
||||
# For macOS
|
||||
python3 scripts/debugging_monitor.py /dev/cu.usbmodem2101
|
||||
```
|
||||
This was tested on Debian and should work on most Linux systems. Minor adjustments may be required for Windows or macOS.
|
||||
Minor adjustments may be required for Windows.
|
||||
|
||||
## Internals
|
||||
|
||||
|
||||
@ -1,23 +1,19 @@
|
||||
#include "EpdFontFamily.h"
|
||||
|
||||
const EpdFont* EpdFontFamily::getFont(const Style style) const {
|
||||
if (style == BOLD && bold) {
|
||||
// Extract font style bits (ignore UNDERLINE bit for font selection)
|
||||
const bool hasBold = (style & BOLD) != 0;
|
||||
const bool hasItalic = (style & ITALIC) != 0;
|
||||
|
||||
if (hasBold && hasItalic) {
|
||||
if (boldItalic) return boldItalic;
|
||||
if (bold) return bold;
|
||||
if (italic) return italic;
|
||||
} else if (hasBold && bold) {
|
||||
return bold;
|
||||
}
|
||||
if (style == ITALIC && italic) {
|
||||
} else if (hasItalic && italic) {
|
||||
return italic;
|
||||
}
|
||||
if (style == BOLD_ITALIC) {
|
||||
if (boldItalic) {
|
||||
return boldItalic;
|
||||
}
|
||||
if (bold) {
|
||||
return bold;
|
||||
}
|
||||
if (italic) {
|
||||
return italic;
|
||||
}
|
||||
}
|
||||
|
||||
return regular;
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
|
||||
class EpdFontFamily {
|
||||
public:
|
||||
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3 };
|
||||
enum Style : uint8_t { REGULAR = 0, BOLD = 1, ITALIC = 2, BOLD_ITALIC = 3, UNDERLINE = 4 };
|
||||
|
||||
explicit EpdFontFamily(const EpdFont* regular, const EpdFont* bold = nullptr, const EpdFont* italic = nullptr,
|
||||
const EpdFont* boldItalic = nullptr)
|
||||
|
||||
@ -86,8 +86,9 @@ bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata) {
|
||||
tocNavItem = opfParser.tocNavPath;
|
||||
}
|
||||
|
||||
// Copy CSS files to metadata
|
||||
bookMetadata.cssFiles = opfParser.cssFiles;
|
||||
if (!opfParser.cssFiles.empty()) {
|
||||
cssFiles = opfParser.cssFiles;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Successfully parsed content.opf\n", millis());
|
||||
return true;
|
||||
@ -207,21 +208,30 @@ bool Epub::parseTocNavFile() const {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseCssFiles() {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
Serial.printf("[%lu] [EBP] Cannot parse CSS, cache not loaded\n", millis());
|
||||
std::string Epub::getCssRulesCache() const { return cachePath + "/css_rules.cache"; }
|
||||
|
||||
bool Epub::loadCssRulesFromCache() const {
|
||||
FsFile cssCacheFile;
|
||||
if (SdMan.openFileForRead("EBP", getCssRulesCache(), cssCacheFile)) {
|
||||
if (cssParser->loadFromCache(cssCacheFile)) {
|
||||
cssCacheFile.close();
|
||||
Serial.printf("[%lu] [EBP] Loaded CSS rules from cache\n", millis());
|
||||
return true;
|
||||
}
|
||||
cssCacheFile.close();
|
||||
Serial.printf("[%lu] [EBP] CSS cache invalid, reparsing\n", millis());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always create CssParser - needed for inline style parsing even without CSS files
|
||||
cssParser.reset(new CssParser());
|
||||
|
||||
const auto& cssFiles = bookMetadataCache->coreMetadata.cssFiles;
|
||||
void Epub::parseCssFiles() const {
|
||||
if (cssFiles.empty()) {
|
||||
Serial.printf("[%lu] [EBP] No CSS files to parse, but CssParser created for inline styles\n", millis());
|
||||
return true;
|
||||
}
|
||||
|
||||
// Try to load from CSS cache first
|
||||
if (!loadCssRulesFromCache()) {
|
||||
// Cache miss - parse CSS files
|
||||
for (const auto& cssPath : cssFiles) {
|
||||
Serial.printf("[%lu] [EBP] Parsing CSS file: %s\n", millis(), cssPath.c_str());
|
||||
|
||||
@ -251,22 +261,38 @@ bool Epub::parseCssFiles() {
|
||||
SdMan.remove(tmpCssPath.c_str());
|
||||
}
|
||||
|
||||
// Save to cache for next time
|
||||
FsFile cssCacheFile;
|
||||
if (SdMan.openFileForWrite("EBP", getCssRulesCache(), cssCacheFile)) {
|
||||
cssParser->saveToCache(cssCacheFile);
|
||||
cssCacheFile.close();
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded %zu CSS style rules from %zu files\n", millis(), cssParser->ruleCount(),
|
||||
cssFiles.size());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// load in the meta data for the epub file
|
||||
bool Epub::load(const bool buildIfMissing) {
|
||||
bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
Serial.printf("[%lu] [EBP] Loading ePub: %s\n", millis(), filepath.c_str());
|
||||
|
||||
// Initialize spine/TOC cache
|
||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||
// Always create CssParser - needed for inline style parsing even without CSS files
|
||||
cssParser.reset(new CssParser());
|
||||
|
||||
// Try to load existing cache first
|
||||
if (bookMetadataCache->load()) {
|
||||
// Parse CSS files from loaded cache
|
||||
if (!skipLoadingCss && !loadCssRulesFromCache()) {
|
||||
Serial.printf("[%lu] [EBP] Warning: CSS rules cache not found, attempting to parse CSS files\n", millis());
|
||||
// to get CSS file list
|
||||
if (!parseContentOpf(bookMetadataCache->coreMetadata)) {
|
||||
Serial.printf("[%lu] [EBP] Could not parse content.opf from cached bookMetadata for CSS files\n", millis());
|
||||
// continue anyway - book will work without CSS and we'll still load any inline style CSS
|
||||
}
|
||||
parseCssFiles();
|
||||
}
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
@ -363,8 +389,10 @@ bool Epub::load(const bool buildIfMissing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!skipLoadingCss) {
|
||||
// Parse CSS files after cache reload
|
||||
parseCssFiles();
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [EBP] Loaded ePub: %s\n", millis(), filepath.c_str());
|
||||
return true;
|
||||
|
||||
@ -27,12 +27,16 @@ class Epub {
|
||||
std::unique_ptr<BookMetadataCache> bookMetadataCache;
|
||||
// CSS parser for styling
|
||||
std::unique_ptr<CssParser> cssParser;
|
||||
// CSS files
|
||||
std::vector<std::string> cssFiles;
|
||||
|
||||
bool findContentOpfFile(std::string* contentOpfFile) const;
|
||||
bool parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata);
|
||||
bool parseTocNcxFile() const;
|
||||
bool parseTocNavFile() const;
|
||||
bool parseCssFiles();
|
||||
void parseCssFiles() const;
|
||||
std::string getCssRulesCache() const;
|
||||
bool loadCssRulesFromCache() const;
|
||||
|
||||
public:
|
||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||
@ -41,7 +45,7 @@ class Epub {
|
||||
}
|
||||
~Epub() = default;
|
||||
std::string& getBasePath() { return contentBasePath; }
|
||||
bool load(bool buildIfMissing = true);
|
||||
bool load(bool buildIfMissing = true, bool skipLoadingCss = false);
|
||||
bool clearCache() const;
|
||||
void setupCacheDir() const;
|
||||
const std::string& getCachePath() const;
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
#include "FsHelpers.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t BOOK_CACHE_VERSION = 6;
|
||||
constexpr uint8_t BOOK_CACHE_VERSION = 5;
|
||||
constexpr char bookBinFile[] = "/book.bin";
|
||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||
@ -115,14 +115,9 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
|
||||
constexpr uint32_t headerASize =
|
||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||
// Calculate CSS files size: count + each string (length + data)
|
||||
uint32_t cssFilesSize = sizeof(uint16_t); // count
|
||||
for (const auto& css : metadata.cssFiles) {
|
||||
cssFilesSize += sizeof(uint32_t) + css.size();
|
||||
}
|
||||
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
|
||||
sizeof(uint32_t) * 5 + cssFilesSize;
|
||||
sizeof(uint32_t) * 5;
|
||||
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
||||
const uint32_t lutOffset = headerASize + metadataSize;
|
||||
|
||||
@ -137,11 +132,6 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta
|
||||
serialization::writeString(bookFile, metadata.language);
|
||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||
serialization::writeString(bookFile, metadata.textReferenceHref);
|
||||
// CSS files
|
||||
serialization::writePod(bookFile, static_cast<uint16_t>(metadata.cssFiles.size()));
|
||||
for (const auto& css : metadata.cssFiles) {
|
||||
serialization::writeString(bookFile, css);
|
||||
}
|
||||
|
||||
// Loop through spine entries, writing LUT positions
|
||||
spineFile.seek(0);
|
||||
@ -395,16 +385,6 @@ bool BookMetadataCache::load() {
|
||||
serialization::readString(bookFile, coreMetadata.language);
|
||||
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||
// CSS files
|
||||
uint16_t cssCount;
|
||||
serialization::readPod(bookFile, cssCount);
|
||||
coreMetadata.cssFiles.clear();
|
||||
coreMetadata.cssFiles.reserve(cssCount);
|
||||
for (uint16_t i = 0; i < cssCount; i++) {
|
||||
std::string cssPath;
|
||||
serialization::readString(bookFile, cssPath);
|
||||
coreMetadata.cssFiles.push_back(std::move(cssPath));
|
||||
}
|
||||
|
||||
loaded = true;
|
||||
Serial.printf("[%lu] [BMC] Loaded cache data: %d spine, %d TOC entries\n", millis(), spineCount, tocCount);
|
||||
|
||||
@ -14,7 +14,6 @@ class BookMetadataCache {
|
||||
std::string language;
|
||||
std::string coverItemHref;
|
||||
std::string textReferenceHref;
|
||||
std::vector<std::string> cssFiles;
|
||||
};
|
||||
|
||||
struct SpineEntry {
|
||||
|
||||
@ -19,23 +19,6 @@ namespace {
|
||||
constexpr char SOFT_HYPHEN_UTF8[] = "\xC2\xAD";
|
||||
constexpr size_t SOFT_HYPHEN_BYTES = 2;
|
||||
|
||||
// Check if a character is punctuation that should attach to the previous word
|
||||
// (no space before it). Includes sentence punctuation and closing quotes.
|
||||
// Excludes brackets/parens to avoid false positives with decorative patterns like "[ 1 ]".
|
||||
bool isAttachingPunctuation(const char c) {
|
||||
return c == '.' || c == ',' || c == '!' || c == '?' || c == ';' || c == ':' || c == '"' || c == '\'';
|
||||
}
|
||||
|
||||
// Check if a word consists entirely of punctuation that should attach to the previous word
|
||||
bool isAttachingPunctuationWord(const std::string& word) {
|
||||
if (word.empty()) return false;
|
||||
// Check if word starts with attaching punctuation and is short (to avoid false positives)
|
||||
if (isAttachingPunctuation(word[0]) && word.size() <= 3) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool containsSoftHyphen(const std::string& word) { return word.find(SOFT_HYPHEN_UTF8) != std::string::npos; }
|
||||
|
||||
// Removes every soft hyphen in-place so rendered glyphs match measured widths.
|
||||
@ -66,12 +49,15 @@ uint16_t measureWordWidth(const GfxRenderer& renderer, const int fontId, const s
|
||||
|
||||
} // namespace
|
||||
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style fontStyle, const bool underline) {
|
||||
void ParsedText::addWord(std::string word, const EpdFontFamily::Style style, const bool underline) {
|
||||
if (word.empty()) return;
|
||||
|
||||
words.push_back(std::move(word));
|
||||
wordStyles.push_back(fontStyle);
|
||||
wordUnderlines.push_back(underline);
|
||||
EpdFontFamily::Style combinedStyle = style;
|
||||
if (underline) {
|
||||
combinedStyle = static_cast<EpdFontFamily::Style>(combinedStyle | EpdFontFamily::UNDERLINE);
|
||||
}
|
||||
wordStyles.push_back(combinedStyle);
|
||||
}
|
||||
|
||||
// Consumes data to minimize memory usage
|
||||
@ -112,8 +98,7 @@ std::vector<uint16_t> ParsedText::calculateWordWidths(const GfxRenderer& rendere
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
|
||||
while (wordsIt != words.end()) {
|
||||
uint16_t width = measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt);
|
||||
wordWidths.push_back(width);
|
||||
wordWidths.push_back(measureWordWidth(renderer, fontId, *wordsIt, *wordStylesIt));
|
||||
|
||||
std::advance(wordsIt, 1);
|
||||
std::advance(wordStylesIt, 1);
|
||||
@ -129,8 +114,9 @@ std::vector<size_t> ParsedText::computeLineBreaks(const GfxRenderer& renderer, c
|
||||
}
|
||||
|
||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||
const int firstLineIndent = blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
(style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN)
|
||||
const int firstLineIndent =
|
||||
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
|
||||
@ -233,7 +219,7 @@ void ParsedText::applyParagraphIndent() {
|
||||
if (blockStyle.textIndentDefined) {
|
||||
// CSS text-indent is explicitly set (even if 0) - don't use fallback EmSpace
|
||||
// The actual indent positioning is handled in extractLine()
|
||||
} else if (style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN) {
|
||||
} else if (blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left) {
|
||||
// No CSS text-indent defined - use EmSpace fallback for visual indent
|
||||
words.front().insert(0, "\xe2\x80\x83");
|
||||
}
|
||||
@ -244,8 +230,9 @@ std::vector<size_t> ParsedText::computeHyphenatedLineBreaks(const GfxRenderer& r
|
||||
const int pageWidth, const int spaceWidth,
|
||||
std::vector<uint16_t>& wordWidths) {
|
||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||
const int firstLineIndent = blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
(style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN)
|
||||
const int firstLineIndent =
|
||||
blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
|
||||
@ -381,25 +368,16 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
|
||||
// Calculate first line indent (only for left/justified text without extra paragraph spacing)
|
||||
const bool isFirstLine = breakIndex == 0;
|
||||
const int firstLineIndent = isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
(style == TextBlock::JUSTIFIED || style == TextBlock::LEFT_ALIGN)
|
||||
const int firstLineIndent =
|
||||
isFirstLine && blockStyle.textIndent > 0 && !extraParagraphSpacing &&
|
||||
(blockStyle.alignment == CssTextAlign::Justify || blockStyle.alignment == CssTextAlign::Left)
|
||||
? blockStyle.textIndent
|
||||
: 0;
|
||||
|
||||
// Calculate total word width for this line and count actual word gaps
|
||||
// (punctuation that attaches to previous word doesn't count as a gap)
|
||||
// Note: words list starts at the beginning because previous lines were spliced out
|
||||
// Calculate total word width for this line
|
||||
int lineWordWidthSum = 0;
|
||||
size_t actualGapCount = 0;
|
||||
auto countWordIt = words.begin();
|
||||
|
||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||
lineWordWidthSum += wordWidths[lastBreakAt + wordIdx];
|
||||
// Count gaps: each word after the first creates a gap, unless it's attaching punctuation
|
||||
if (wordIdx > 0 && !isAttachingPunctuationWord(*countWordIt)) {
|
||||
actualGapCount++;
|
||||
}
|
||||
++countWordIt;
|
||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||
lineWordWidthSum += wordWidths[i];
|
||||
}
|
||||
|
||||
// Calculate spacing (account for indent reducing effective page width on first line)
|
||||
@ -409,54 +387,37 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
int spacing = spaceWidth;
|
||||
const bool isLastLine = breakIndex == lineBreakIndices.size() - 1;
|
||||
|
||||
// For justified text, calculate spacing based on actual gap count
|
||||
if (style == TextBlock::JUSTIFIED && !isLastLine && actualGapCount >= 1) {
|
||||
spacing = spareSpace / static_cast<int>(actualGapCount);
|
||||
if (blockStyle.alignment == CssTextAlign::Justify && !isLastLine && lineWordCount >= 2) {
|
||||
spacing = spareSpace / (lineWordCount - 1);
|
||||
}
|
||||
|
||||
// Calculate initial x position (first line starts at indent for left/justified text)
|
||||
auto xpos = static_cast<uint16_t>(firstLineIndent);
|
||||
if (style == TextBlock::RIGHT_ALIGN) {
|
||||
xpos = spareSpace - static_cast<int>(actualGapCount) * spaceWidth;
|
||||
} else if (style == TextBlock::CENTER_ALIGN) {
|
||||
xpos = (spareSpace - static_cast<int>(actualGapCount) * spaceWidth) / 2;
|
||||
if (blockStyle.alignment == CssTextAlign::Right) {
|
||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||
} else if (blockStyle.alignment == CssTextAlign::Center) {
|
||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||
}
|
||||
|
||||
// Pre-calculate X positions for words
|
||||
// Punctuation that attaches to the previous word doesn't get space before it
|
||||
// Note: words list starts at the beginning because previous lines were spliced out
|
||||
std::list<uint16_t> lineXPos;
|
||||
auto wordIt = words.begin();
|
||||
|
||||
for (size_t wordIdx = 0; wordIdx < lineWordCount; wordIdx++) {
|
||||
const uint16_t currentWordWidth = wordWidths[lastBreakAt + wordIdx];
|
||||
|
||||
for (size_t i = lastBreakAt; i < lineBreak; i++) {
|
||||
const uint16_t currentWordWidth = wordWidths[i];
|
||||
lineXPos.push_back(xpos);
|
||||
|
||||
// Add spacing after this word, unless the next word is attaching punctuation
|
||||
auto nextWordIt = wordIt;
|
||||
++nextWordIt;
|
||||
const bool nextIsAttachingPunctuation = wordIdx + 1 < lineWordCount && isAttachingPunctuationWord(*nextWordIt);
|
||||
|
||||
xpos += currentWordWidth + (nextIsAttachingPunctuation ? 0 : spacing);
|
||||
++wordIt;
|
||||
xpos += currentWordWidth + spacing;
|
||||
}
|
||||
|
||||
// Iterators always start at the beginning as we are moving content with splice below
|
||||
auto wordEndIt = words.begin();
|
||||
auto wordStyleEndIt = wordStyles.begin();
|
||||
auto wordUnderlineEndIt = wordUnderlines.begin();
|
||||
std::advance(wordEndIt, lineWordCount);
|
||||
std::advance(wordStyleEndIt, lineWordCount);
|
||||
std::advance(wordUnderlineEndIt, lineWordCount);
|
||||
|
||||
// *** CRITICAL STEP: CONSUME DATA USING SPLICE ***
|
||||
std::list<std::string> lineWords;
|
||||
lineWords.splice(lineWords.begin(), words, words.begin(), wordEndIt);
|
||||
std::list<EpdFontFamily::Style> lineWordStyles;
|
||||
lineWordStyles.splice(lineWordStyles.begin(), wordStyles, wordStyles.begin(), wordStyleEndIt);
|
||||
std::list<bool> lineWordUnderlines;
|
||||
lineWordUnderlines.splice(lineWordUnderlines.begin(), wordUnderlines, wordUnderlines.begin(), wordUnderlineEndIt);
|
||||
|
||||
for (auto& word : lineWords) {
|
||||
if (containsSoftHyphen(word)) {
|
||||
@ -464,6 +425,6 @@ void ParsedText::extractLine(const size_t breakIndex, const int pageWidth, const
|
||||
}
|
||||
}
|
||||
|
||||
processLine(std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), style,
|
||||
blockStyle, std::move(lineWordUnderlines)));
|
||||
processLine(
|
||||
std::make_shared<TextBlock>(std::move(lineWords), std::move(lineXPos), std::move(lineWordStyles), blockStyle));
|
||||
}
|
||||
|
||||
@ -16,8 +16,6 @@ class GfxRenderer;
|
||||
class ParsedText {
|
||||
std::list<std::string> words;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::list<bool> wordUnderlines; // Track underline per word
|
||||
TextBlock::Style style;
|
||||
BlockStyle blockStyle;
|
||||
bool extraParagraphSpacing;
|
||||
bool hyphenationEnabled;
|
||||
@ -35,19 +33,14 @@ class ParsedText {
|
||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||
|
||||
public:
|
||||
explicit ParsedText(const TextBlock::Style style, const bool extraParagraphSpacing,
|
||||
const bool hyphenationEnabled = false, const BlockStyle& blockStyle = BlockStyle())
|
||||
: style(style),
|
||||
blockStyle(blockStyle),
|
||||
extraParagraphSpacing(extraParagraphSpacing),
|
||||
hyphenationEnabled(hyphenationEnabled) {}
|
||||
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
|
||||
const BlockStyle& blockStyle = BlockStyle())
|
||||
: blockStyle(blockStyle), extraParagraphSpacing(extraParagraphSpacing), hyphenationEnabled(hyphenationEnabled) {}
|
||||
~ParsedText() = default;
|
||||
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false);
|
||||
void setStyle(const TextBlock::Style style) { this->style = style; }
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
TextBlock::Style getStyle() const { return style; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
BlockStyle& getBlockStyle() { return blockStyle; }
|
||||
size_t size() const { return words.size(); }
|
||||
bool isEmpty() const { return words.empty(); }
|
||||
void layoutAndExtractLines(const GfxRenderer& renderer, int fontId, uint16_t viewportWidth,
|
||||
|
||||
@ -2,26 +2,89 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "Epub/css/CssStyle.h"
|
||||
|
||||
/**
|
||||
* BlockStyle - Block-level CSS properties for paragraphs
|
||||
*
|
||||
* Used to track margin/padding spacing and text indentation for block elements.
|
||||
* Padding is treated similarly to margins for rendering purposes.
|
||||
* BlockStyle - Block-level styling properties
|
||||
*/
|
||||
struct BlockStyle {
|
||||
int16_t marginTop = 0; // pixels
|
||||
int16_t marginBottom = 0; // pixels
|
||||
int16_t marginLeft = 0; // pixels
|
||||
int16_t marginRight = 0; // pixels
|
||||
int16_t paddingTop = 0; // pixels (treated same as margin)
|
||||
int16_t paddingBottom = 0; // pixels (treated same as margin)
|
||||
int16_t paddingLeft = 0; // pixels (treated same as margin)
|
||||
int16_t paddingRight = 0; // pixels (treated same as margin)
|
||||
int16_t textIndent = 0; // pixels
|
||||
CssTextAlign alignment = CssTextAlign::Justify;
|
||||
|
||||
// Spacing (in pixels)
|
||||
int16_t marginTop = 0;
|
||||
int16_t marginBottom = 0;
|
||||
int16_t marginLeft = 0;
|
||||
int16_t marginRight = 0;
|
||||
int16_t paddingTop = 0; // treated same as margin for rendering
|
||||
int16_t paddingBottom = 0; // treated same as margin for rendering
|
||||
int16_t paddingLeft = 0; // treated same as margin for rendering
|
||||
int16_t paddingRight = 0; // treated same as margin for rendering
|
||||
int16_t textIndent = 0;
|
||||
bool textIndentDefined = false; // true if text-indent was explicitly set in CSS
|
||||
bool textAlignDefined = false; // true if text-align was explicitly set in CSS
|
||||
|
||||
// Combined horizontal 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(); }
|
||||
|
||||
// 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;
|
||||
|
||||
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);
|
||||
|
||||
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;
|
||||
} else {
|
||||
combinedBlockStyle.alignment = alignment;
|
||||
combinedBlockStyle.textAlignDefined = textAlignDefined;
|
||||
}
|
||||
return combinedBlockStyle;
|
||||
}
|
||||
|
||||
// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
|
||||
// emSize is the current font line height, used for em/rem unit conversion
|
||||
// paragraphAlignment is the user's paragraphAlignment setting preference
|
||||
static BlockStyle fromCssStyle(const CssStyle& cssStyle, const float emSize, const CssTextAlign paragraphAlignment) {
|
||||
BlockStyle blockStyle;
|
||||
// Resolve all CssLength values to pixels using the current font's em size
|
||||
blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize);
|
||||
blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize);
|
||||
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize);
|
||||
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize);
|
||||
|
||||
blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize);
|
||||
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize);
|
||||
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize);
|
||||
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize);
|
||||
|
||||
blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize);
|
||||
blockStyle.textIndentDefined = cssStyle.hasTextIndent();
|
||||
blockStyle.textAlignDefined = cssStyle.hasTextAlign();
|
||||
if (blockStyle.textAlignDefined) {
|
||||
blockStyle.alignment = cssStyle.textAlign;
|
||||
} else {
|
||||
blockStyle.alignment = paragraphAlignment;
|
||||
}
|
||||
return blockStyle;
|
||||
}
|
||||
};
|
||||
|
||||
@ -14,15 +14,14 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
auto wordIt = words.begin();
|
||||
auto wordStylesIt = wordStyles.begin();
|
||||
auto wordXposIt = wordXpos.begin();
|
||||
auto wordUnderlineIt = wordUnderlines.begin();
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
const int wordX = *wordXposIt + x;
|
||||
renderer.drawText(fontId, wordX, y, wordIt->c_str(), true, *wordStylesIt);
|
||||
const EpdFontFamily::Style currentStyle = *wordStylesIt;
|
||||
renderer.drawText(fontId, wordX, y, wordIt->c_str(), true, currentStyle);
|
||||
|
||||
// Draw underline if word is underlined
|
||||
if (wordUnderlineIt != wordUnderlines.end() && *wordUnderlineIt) {
|
||||
if ((currentStyle & EpdFontFamily::UNDERLINE) != 0) {
|
||||
const std::string& w = *wordIt;
|
||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), *wordStylesIt);
|
||||
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), currentStyle);
|
||||
// y is the top of the text line; add ascender to reach baseline, then offset 2px below
|
||||
const int underlineY = y + renderer.getFontAscenderSize(fontId) + 2;
|
||||
|
||||
@ -33,8 +32,8 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
if (w.size() >= 3 && static_cast<uint8_t>(w[0]) == 0xE2 && static_cast<uint8_t>(w[1]) == 0x80 &&
|
||||
static_cast<uint8_t>(w[2]) == 0x83) {
|
||||
const char* visiblePtr = w.c_str() + 3;
|
||||
const int prefixWidth = renderer.getIndentWidth(fontId, std::string("\xe2\x80\x83").c_str());
|
||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, *wordStylesIt);
|
||||
const int prefixWidth = renderer.getTextAdvanceX(fontId, std::string("\xe2\x80\x83").c_str());
|
||||
const int visibleWidth = renderer.getTextWidth(fontId, visiblePtr, currentStyle);
|
||||
startX = wordX + prefixWidth;
|
||||
underlineWidth = visibleWidth;
|
||||
}
|
||||
@ -45,9 +44,6 @@ void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int
|
||||
std::advance(wordIt, 1);
|
||||
std::advance(wordStylesIt, 1);
|
||||
std::advance(wordXposIt, 1);
|
||||
if (wordUnderlineIt != wordUnderlines.end()) {
|
||||
std::advance(wordUnderlineIt, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,29 +60,9 @@ bool TextBlock::serialize(FsFile& file) const {
|
||||
for (auto x : wordXpos) serialization::writePod(file, x);
|
||||
for (auto s : wordStyles) serialization::writePod(file, s);
|
||||
|
||||
// Underline flags (packed as bytes, 8 words per byte)
|
||||
uint8_t underlineByte = 0;
|
||||
int bitIndex = 0;
|
||||
auto underlineIt = wordUnderlines.begin();
|
||||
for (size_t i = 0; i < words.size(); i++) {
|
||||
if (underlineIt != wordUnderlines.end() && *underlineIt) {
|
||||
underlineByte |= 1 << bitIndex;
|
||||
}
|
||||
bitIndex++;
|
||||
if (bitIndex == 8 || i == words.size() - 1) {
|
||||
serialization::writePod(file, underlineByte);
|
||||
underlineByte = 0;
|
||||
bitIndex = 0;
|
||||
}
|
||||
if (underlineIt != wordUnderlines.end()) {
|
||||
++underlineIt;
|
||||
}
|
||||
}
|
||||
|
||||
// Block style (alignment)
|
||||
serialization::writePod(file, style);
|
||||
|
||||
// Block style (margins/padding/indent)
|
||||
// Style (alignment + margins/padding/indent)
|
||||
serialization::writePod(file, blockStyle.alignment);
|
||||
serialization::writePod(file, blockStyle.textAlignDefined);
|
||||
serialization::writePod(file, blockStyle.marginTop);
|
||||
serialization::writePod(file, blockStyle.marginBottom);
|
||||
serialization::writePod(file, blockStyle.marginLeft);
|
||||
@ -106,8 +82,6 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::list<bool> wordUnderlines;
|
||||
Style style;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
// Word count
|
||||
@ -127,23 +101,9 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
for (auto& x : wordXpos) serialization::readPod(file, x);
|
||||
for (auto& s : wordStyles) serialization::readPod(file, s);
|
||||
|
||||
// Underline flags (packed as bytes, 8 words per byte)
|
||||
wordUnderlines.resize(wc, false);
|
||||
auto underlineIt = wordUnderlines.begin();
|
||||
const int bytesNeeded = (wc + 7) / 8;
|
||||
for (int byteIdx = 0; byteIdx < bytesNeeded; byteIdx++) {
|
||||
uint8_t underlineByte;
|
||||
serialization::readPod(file, underlineByte);
|
||||
for (int bit = 0; bit < 8 && underlineIt != wordUnderlines.end(); bit++) {
|
||||
*underlineIt = (underlineByte & 1 << bit) != 0;
|
||||
++underlineIt;
|
||||
}
|
||||
}
|
||||
|
||||
// Block style (alignment)
|
||||
serialization::readPod(file, style);
|
||||
|
||||
// Block style (margins/padding/indent)
|
||||
// Style (alignment + margins/padding/indent)
|
||||
serialization::readPod(file, blockStyle.alignment);
|
||||
serialization::readPod(file, blockStyle.textAlignDefined);
|
||||
serialization::readPod(file, blockStyle.marginTop);
|
||||
serialization::readPod(file, blockStyle.marginBottom);
|
||||
serialization::readPod(file, blockStyle.marginLeft);
|
||||
@ -155,6 +115,6 @@ std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
|
||||
serialization::readPod(file, blockStyle.textIndent);
|
||||
serialization::readPod(file, blockStyle.textIndentDefined);
|
||||
|
||||
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style,
|
||||
blockStyle, std::move(wordUnderlines)));
|
||||
return std::unique_ptr<TextBlock>(
|
||||
new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), blockStyle));
|
||||
}
|
||||
|
||||
@ -11,41 +11,21 @@
|
||||
|
||||
// Represents a line of text on a page
|
||||
class TextBlock final : public Block {
|
||||
public:
|
||||
enum Style : uint8_t {
|
||||
JUSTIFIED = 0,
|
||||
LEFT_ALIGN = 1,
|
||||
CENTER_ALIGN = 2,
|
||||
RIGHT_ALIGN = 3,
|
||||
};
|
||||
|
||||
private:
|
||||
std::list<std::string> words;
|
||||
std::list<uint16_t> wordXpos;
|
||||
std::list<EpdFontFamily::Style> wordStyles;
|
||||
std::list<bool> wordUnderlines; // Track underline per word
|
||||
Style style;
|
||||
BlockStyle blockStyle;
|
||||
|
||||
public:
|
||||
explicit TextBlock(std::list<std::string> words, std::list<uint16_t> word_xpos,
|
||||
std::list<EpdFontFamily::Style> word_styles, const Style style,
|
||||
const BlockStyle& blockStyle = BlockStyle(), std::list<bool> word_underlines = std::list<bool>())
|
||||
std::list<EpdFontFamily::Style> word_styles, const BlockStyle& blockStyle = BlockStyle())
|
||||
: words(std::move(words)),
|
||||
wordXpos(std::move(word_xpos)),
|
||||
wordStyles(std::move(word_styles)),
|
||||
wordUnderlines(std::move(word_underlines)),
|
||||
style(style),
|
||||
blockStyle(blockStyle) {
|
||||
// Ensure underlines list matches words list size
|
||||
while (this->wordUnderlines.size() < this->words.size()) {
|
||||
this->wordUnderlines.push_back(false);
|
||||
}
|
||||
}
|
||||
blockStyle(blockStyle) {}
|
||||
~TextBlock() override = default;
|
||||
void setStyle(const Style style) { this->style = style; }
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
Style getStyle() const { return style; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
void layout(GfxRenderer& renderer) override {};
|
||||
|
||||
@ -204,15 +204,15 @@ std::vector<std::string> CssParser::splitWhitespace(const std::string& s) {
|
||||
|
||||
// Property value interpreters
|
||||
|
||||
TextAlign CssParser::interpretAlignment(const std::string& val) {
|
||||
CssTextAlign CssParser::interpretAlignment(const std::string& val) {
|
||||
const std::string v = normalized(val);
|
||||
|
||||
if (v == "left" || v == "start") return TextAlign::Left;
|
||||
if (v == "right" || v == "end") return TextAlign::Right;
|
||||
if (v == "center") return TextAlign::Center;
|
||||
if (v == "justify") return TextAlign::Justify;
|
||||
if (v == "left" || v == "start") return CssTextAlign::Left;
|
||||
if (v == "right" || v == "end") return CssTextAlign::Right;
|
||||
if (v == "center") return CssTextAlign::Center;
|
||||
if (v == "justify") return CssTextAlign::Justify;
|
||||
|
||||
return TextAlign::None;
|
||||
return CssTextAlign::Left;
|
||||
}
|
||||
|
||||
CssFontStyle CssParser::interpretFontStyle(const std::string& val) {
|
||||
@ -352,11 +352,8 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||
|
||||
// Match property and set value
|
||||
if (propName == "text-align") {
|
||||
const TextAlign align = interpretAlignment(propValue);
|
||||
if (align != TextAlign::None) {
|
||||
style.alignment = align;
|
||||
style.defined.alignment = 1;
|
||||
}
|
||||
style.textAlign = interpretAlignment(propValue);
|
||||
style.defined.textAlign = 1;
|
||||
} else if (propName == "font-style") {
|
||||
style.fontStyle = interpretFontStyle(propValue);
|
||||
style.defined.fontStyle = 1;
|
||||
@ -364,11 +361,11 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||
style.fontWeight = interpretFontWeight(propValue);
|
||||
style.defined.fontWeight = 1;
|
||||
} else if (propName == "text-decoration" || propName == "text-decoration-line") {
|
||||
style.decoration = interpretDecoration(propValue);
|
||||
style.defined.decoration = 1;
|
||||
style.textDecoration = interpretDecoration(propValue);
|
||||
style.defined.textDecoration = 1;
|
||||
} else if (propName == "text-indent") {
|
||||
style.indent = interpretLength(propValue);
|
||||
style.defined.indent = 1;
|
||||
style.textIndent = interpretLength(propValue);
|
||||
style.defined.textIndent = 1;
|
||||
} else if (propName == "margin-top") {
|
||||
style.marginTop = interpretLength(propValue);
|
||||
style.defined.marginTop = 1;
|
||||
@ -385,14 +382,10 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||
// Shorthand: 1-4 values for top, right, bottom, left
|
||||
const auto values = splitWhitespace(propValue);
|
||||
if (!values.empty()) {
|
||||
const CssLength top = interpretLength(values[0]);
|
||||
const CssLength right = values.size() >= 2 ? interpretLength(values[1]) : top;
|
||||
const CssLength bottom = values.size() >= 3 ? interpretLength(values[2]) : top;
|
||||
const CssLength left = values.size() >= 4 ? interpretLength(values[3]) : right;
|
||||
style.marginTop = top;
|
||||
style.marginRight = right;
|
||||
style.marginBottom = bottom;
|
||||
style.marginLeft = left;
|
||||
style.marginTop = interpretLength(values[0]);
|
||||
style.marginRight = values.size() >= 2 ? interpretLength(values[1]) : style.marginTop;
|
||||
style.marginBottom = values.size() >= 3 ? interpretLength(values[2]) : style.marginTop;
|
||||
style.marginLeft = values.size() >= 4 ? interpretLength(values[3]) : style.marginRight;
|
||||
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
|
||||
}
|
||||
} else if (propName == "padding-top") {
|
||||
@ -411,14 +404,10 @@ CssStyle CssParser::parseDeclarations(const std::string& declBlock) {
|
||||
// Shorthand: 1-4 values for top, right, bottom, left
|
||||
const auto values = splitWhitespace(propValue);
|
||||
if (!values.empty()) {
|
||||
const CssLength top = interpretLength(values[0]);
|
||||
const CssLength right = values.size() >= 2 ? interpretLength(values[1]) : top;
|
||||
const CssLength bottom = values.size() >= 3 ? interpretLength(values[2]) : top;
|
||||
const CssLength left = values.size() >= 4 ? interpretLength(values[3]) : right;
|
||||
style.paddingTop = top;
|
||||
style.paddingRight = right;
|
||||
style.paddingBottom = bottom;
|
||||
style.paddingLeft = left;
|
||||
style.paddingTop = interpretLength(values[0]);
|
||||
style.paddingRight = values.size() >= 2 ? interpretLength(values[1]) : style.paddingTop;
|
||||
style.paddingBottom = values.size() >= 3 ? interpretLength(values[2]) : style.paddingTop;
|
||||
style.paddingLeft = values.size() >= 4 ? interpretLength(values[3]) : style.paddingRight;
|
||||
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom =
|
||||
style.defined.paddingLeft = 1;
|
||||
}
|
||||
@ -525,3 +514,184 @@ CssStyle CssParser::resolveStyle(const std::string& tagName, const std::string&
|
||||
// Inline style parsing (static - doesn't need rule database)
|
||||
|
||||
CssStyle CssParser::parseInlineStyle(const std::string& styleValue) { return parseDeclarations(styleValue); }
|
||||
|
||||
// Cache serialization
|
||||
|
||||
// Cache format version - increment when format changes
|
||||
constexpr uint8_t CSS_CACHE_VERSION = 1;
|
||||
|
||||
bool CssParser::saveToCache(FsFile& file) const {
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write version
|
||||
file.write(CSS_CACHE_VERSION);
|
||||
|
||||
// Write rule count
|
||||
const auto ruleCount = static_cast<uint16_t>(rulesBySelector_.size());
|
||||
file.write(reinterpret_cast<const uint8_t*>(&ruleCount), sizeof(ruleCount));
|
||||
|
||||
// Write each rule: selector string + CssStyle fields
|
||||
for (const auto& pair : rulesBySelector_) {
|
||||
// Write selector string (length-prefixed)
|
||||
const auto selectorLen = static_cast<uint16_t>(pair.first.size());
|
||||
file.write(reinterpret_cast<const uint8_t*>(&selectorLen), sizeof(selectorLen));
|
||||
file.write(reinterpret_cast<const uint8_t*>(pair.first.data()), selectorLen);
|
||||
|
||||
// Write CssStyle fields (all are POD types)
|
||||
const CssStyle& style = pair.second;
|
||||
file.write(static_cast<uint8_t>(style.textAlign));
|
||||
file.write(static_cast<uint8_t>(style.fontStyle));
|
||||
file.write(static_cast<uint8_t>(style.fontWeight));
|
||||
file.write(static_cast<uint8_t>(style.textDecoration));
|
||||
|
||||
// Write CssLength fields (value + unit)
|
||||
auto writeLength = [&file](const CssLength& len) {
|
||||
file.write(reinterpret_cast<const uint8_t*>(&len.value), sizeof(len.value));
|
||||
file.write(static_cast<uint8_t>(len.unit));
|
||||
};
|
||||
|
||||
writeLength(style.textIndent);
|
||||
writeLength(style.marginTop);
|
||||
writeLength(style.marginBottom);
|
||||
writeLength(style.marginLeft);
|
||||
writeLength(style.marginRight);
|
||||
writeLength(style.paddingTop);
|
||||
writeLength(style.paddingBottom);
|
||||
writeLength(style.paddingLeft);
|
||||
writeLength(style.paddingRight);
|
||||
|
||||
// Write defined flags as uint16_t
|
||||
uint16_t definedBits = 0;
|
||||
if (style.defined.textAlign) definedBits |= 1 << 0;
|
||||
if (style.defined.fontStyle) definedBits |= 1 << 1;
|
||||
if (style.defined.fontWeight) definedBits |= 1 << 2;
|
||||
if (style.defined.textDecoration) definedBits |= 1 << 3;
|
||||
if (style.defined.textIndent) definedBits |= 1 << 4;
|
||||
if (style.defined.marginTop) definedBits |= 1 << 5;
|
||||
if (style.defined.marginBottom) definedBits |= 1 << 6;
|
||||
if (style.defined.marginLeft) definedBits |= 1 << 7;
|
||||
if (style.defined.marginRight) definedBits |= 1 << 8;
|
||||
if (style.defined.paddingTop) definedBits |= 1 << 9;
|
||||
if (style.defined.paddingBottom) definedBits |= 1 << 10;
|
||||
if (style.defined.paddingLeft) definedBits |= 1 << 11;
|
||||
if (style.defined.paddingRight) definedBits |= 1 << 12;
|
||||
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [CSS] Saved %u rules to cache\n", millis(), ruleCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CssParser::loadFromCache(FsFile& file) {
|
||||
if (!file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear existing rules
|
||||
clear();
|
||||
|
||||
// Read and verify version
|
||||
uint8_t version = 0;
|
||||
if (file.read(&version, 1) != 1 || version != CSS_CACHE_VERSION) {
|
||||
Serial.printf("[%lu] [CSS] Cache version mismatch (got %u, expected %u)\n", millis(), version, CSS_CACHE_VERSION);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read rule count
|
||||
uint16_t ruleCount = 0;
|
||||
if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read each rule
|
||||
for (uint16_t i = 0; i < ruleCount; ++i) {
|
||||
// Read selector string
|
||||
uint16_t selectorLen = 0;
|
||||
if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string selector;
|
||||
selector.resize(selectorLen);
|
||||
if (file.read(&selector[0], selectorLen) != selectorLen) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read CssStyle fields
|
||||
CssStyle style;
|
||||
uint8_t enumVal;
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.textAlign = static_cast<CssTextAlign>(enumVal);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.fontStyle = static_cast<CssFontStyle>(enumVal);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.fontWeight = static_cast<CssFontWeight>(enumVal);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.textDecoration = static_cast<CssTextDecoration>(enumVal);
|
||||
|
||||
// Read CssLength fields
|
||||
auto readLength = [&file](CssLength& len) -> bool {
|
||||
if (file.read(&len.value, sizeof(len.value)) != sizeof(len.value)) {
|
||||
return false;
|
||||
}
|
||||
uint8_t unitVal;
|
||||
if (file.read(&unitVal, 1) != 1) {
|
||||
return false;
|
||||
}
|
||||
len.unit = static_cast<CssUnit>(unitVal);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!readLength(style.textIndent) || !readLength(style.marginTop) || !readLength(style.marginBottom) ||
|
||||
!readLength(style.marginLeft) || !readLength(style.marginRight) || !readLength(style.paddingTop) ||
|
||||
!readLength(style.paddingBottom) || !readLength(style.paddingLeft) || !readLength(style.paddingRight)) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read defined flags
|
||||
uint16_t definedBits = 0;
|
||||
if (file.read(&definedBits, sizeof(definedBits)) != sizeof(definedBits)) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.defined.textAlign = (definedBits & 1 << 0) != 0;
|
||||
style.defined.fontStyle = (definedBits & 1 << 1) != 0;
|
||||
style.defined.fontWeight = (definedBits & 1 << 2) != 0;
|
||||
style.defined.textDecoration = (definedBits & 1 << 3) != 0;
|
||||
style.defined.textIndent = (definedBits & 1 << 4) != 0;
|
||||
style.defined.marginTop = (definedBits & 1 << 5) != 0;
|
||||
style.defined.marginBottom = (definedBits & 1 << 6) != 0;
|
||||
style.defined.marginLeft = (definedBits & 1 << 7) != 0;
|
||||
style.defined.marginRight = (definedBits & 1 << 8) != 0;
|
||||
style.defined.paddingTop = (definedBits & 1 << 9) != 0;
|
||||
style.defined.paddingBottom = (definedBits & 1 << 10) != 0;
|
||||
style.defined.paddingLeft = (definedBits & 1 << 11) != 0;
|
||||
style.defined.paddingRight = (definedBits & 1 << 12) != 0;
|
||||
|
||||
rulesBySelector_[selector] = style;
|
||||
}
|
||||
|
||||
Serial.printf("[%lu] [CSS] Loaded %u rules from cache\n", millis(), ruleCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -76,6 +76,21 @@ class CssParser {
|
||||
*/
|
||||
void clear() { rulesBySelector_.clear(); }
|
||||
|
||||
/**
|
||||
* Save parsed CSS rules to a cache file.
|
||||
* @param file Open file handle to write to
|
||||
* @return true if cache was written successfully
|
||||
*/
|
||||
bool saveToCache(FsFile& file) const;
|
||||
|
||||
/**
|
||||
* Load CSS rules from a cache file.
|
||||
* Clears any existing rules before loading.
|
||||
* @param file Open file handle to read from
|
||||
* @return true if cache was loaded successfully
|
||||
*/
|
||||
bool loadFromCache(FsFile& file);
|
||||
|
||||
private:
|
||||
// Storage: maps normalized selector -> style properties
|
||||
std::unordered_map<std::string, CssStyle> rulesBySelector_;
|
||||
@ -85,7 +100,7 @@ class CssParser {
|
||||
static CssStyle parseDeclarations(const std::string& declBlock);
|
||||
|
||||
// Individual property value parsers
|
||||
static TextAlign interpretAlignment(const std::string& val);
|
||||
static CssTextAlign interpretAlignment(const std::string& val);
|
||||
static CssFontStyle interpretFontStyle(const std::string& val);
|
||||
static CssFontWeight interpretFontWeight(const std::string& val);
|
||||
static CssTextDecoration interpretDecoration(const std::string& val);
|
||||
|
||||
@ -2,10 +2,8 @@
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Text alignment options matching CSS text-align property
|
||||
enum class TextAlign : uint8_t { None = 0, Left = 1, Right = 2, Center = 3, Justify = 4 };
|
||||
|
||||
// CSS length unit types
|
||||
// Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings
|
||||
enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3 };
|
||||
enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3 };
|
||||
|
||||
// Represents a CSS length value with its unit, allowing deferred resolution to pixels
|
||||
@ -47,11 +45,11 @@ enum class CssTextDecoration : uint8_t { None = 0, Underline = 1 };
|
||||
|
||||
// Bitmask for tracking which properties have been explicitly set
|
||||
struct CssPropertyFlags {
|
||||
uint16_t alignment : 1;
|
||||
uint16_t textAlign : 1;
|
||||
uint16_t fontStyle : 1;
|
||||
uint16_t fontWeight : 1;
|
||||
uint16_t decoration : 1;
|
||||
uint16_t indent : 1;
|
||||
uint16_t textDecoration : 1;
|
||||
uint16_t textIndent : 1;
|
||||
uint16_t marginTop : 1;
|
||||
uint16_t marginBottom : 1;
|
||||
uint16_t marginLeft : 1;
|
||||
@ -60,14 +58,13 @@ struct CssPropertyFlags {
|
||||
uint16_t paddingBottom : 1;
|
||||
uint16_t paddingLeft : 1;
|
||||
uint16_t paddingRight : 1;
|
||||
uint16_t reserved : 3;
|
||||
|
||||
CssPropertyFlags()
|
||||
: alignment(0),
|
||||
: textAlign(0),
|
||||
fontStyle(0),
|
||||
fontWeight(0),
|
||||
decoration(0),
|
||||
indent(0),
|
||||
textDecoration(0),
|
||||
textIndent(0),
|
||||
marginTop(0),
|
||||
marginBottom(0),
|
||||
marginLeft(0),
|
||||
@ -75,16 +72,15 @@ struct CssPropertyFlags {
|
||||
paddingTop(0),
|
||||
paddingBottom(0),
|
||||
paddingLeft(0),
|
||||
paddingRight(0),
|
||||
reserved(0) {}
|
||||
paddingRight(0) {}
|
||||
|
||||
[[nodiscard]] bool anySet() const {
|
||||
return alignment || fontStyle || fontWeight || decoration || indent || marginTop || marginBottom || marginLeft ||
|
||||
marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight;
|
||||
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
|
||||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight;
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
alignment = fontStyle = fontWeight = decoration = indent = 0;
|
||||
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
|
||||
marginTop = marginBottom = marginLeft = marginRight = 0;
|
||||
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
||||
}
|
||||
@ -94,12 +90,12 @@ struct CssPropertyFlags {
|
||||
// Only stores properties relevant to e-ink text rendering
|
||||
// Length values are stored as CssLength (value + unit) for deferred resolution
|
||||
struct CssStyle {
|
||||
TextAlign alignment = TextAlign::None;
|
||||
CssTextAlign textAlign = CssTextAlign::Left;
|
||||
CssFontStyle fontStyle = CssFontStyle::Normal;
|
||||
CssFontWeight fontWeight = CssFontWeight::Normal;
|
||||
CssTextDecoration decoration = CssTextDecoration::None;
|
||||
CssTextDecoration textDecoration = CssTextDecoration::None;
|
||||
|
||||
CssLength indent; // First-line indent (deferred resolution)
|
||||
CssLength textIndent; // First-line indent (deferred resolution)
|
||||
CssLength marginTop; // Vertical spacing before block
|
||||
CssLength marginBottom; // Vertical spacing after block
|
||||
CssLength marginLeft; // Horizontal spacing left of block
|
||||
@ -114,66 +110,65 @@ struct CssStyle {
|
||||
// Apply properties from another style, only overwriting if the other style
|
||||
// has that property explicitly defined
|
||||
void applyOver(const CssStyle& base) {
|
||||
if (base.defined.alignment) {
|
||||
alignment = base.alignment;
|
||||
defined.alignment = 1;
|
||||
if (base.hasTextAlign()) {
|
||||
textAlign = base.textAlign;
|
||||
defined.textAlign = 1;
|
||||
}
|
||||
if (base.defined.fontStyle) {
|
||||
if (base.hasFontStyle()) {
|
||||
fontStyle = base.fontStyle;
|
||||
defined.fontStyle = 1;
|
||||
}
|
||||
if (base.defined.fontWeight) {
|
||||
if (base.hasFontWeight()) {
|
||||
fontWeight = base.fontWeight;
|
||||
defined.fontWeight = 1;
|
||||
}
|
||||
if (base.defined.decoration) {
|
||||
decoration = base.decoration;
|
||||
defined.decoration = 1;
|
||||
if (base.hasTextDecoration()) {
|
||||
textDecoration = base.textDecoration;
|
||||
defined.textDecoration = 1;
|
||||
}
|
||||
if (base.defined.indent) {
|
||||
indent = base.indent;
|
||||
defined.indent = 1;
|
||||
if (base.hasTextIndent()) {
|
||||
textIndent = base.textIndent;
|
||||
defined.textIndent = 1;
|
||||
}
|
||||
if (base.defined.marginTop) {
|
||||
if (base.hasMarginTop()) {
|
||||
marginTop = base.marginTop;
|
||||
defined.marginTop = 1;
|
||||
}
|
||||
if (base.defined.marginBottom) {
|
||||
if (base.hasMarginBottom()) {
|
||||
marginBottom = base.marginBottom;
|
||||
defined.marginBottom = 1;
|
||||
}
|
||||
if (base.defined.marginLeft) {
|
||||
if (base.hasMarginLeft()) {
|
||||
marginLeft = base.marginLeft;
|
||||
defined.marginLeft = 1;
|
||||
}
|
||||
if (base.defined.marginRight) {
|
||||
if (base.hasMarginRight()) {
|
||||
marginRight = base.marginRight;
|
||||
defined.marginRight = 1;
|
||||
}
|
||||
if (base.defined.paddingTop) {
|
||||
if (base.hasPaddingTop()) {
|
||||
paddingTop = base.paddingTop;
|
||||
defined.paddingTop = 1;
|
||||
}
|
||||
if (base.defined.paddingBottom) {
|
||||
if (base.hasPaddingBottom()) {
|
||||
paddingBottom = base.paddingBottom;
|
||||
defined.paddingBottom = 1;
|
||||
}
|
||||
if (base.defined.paddingLeft) {
|
||||
if (base.hasPaddingLeft()) {
|
||||
paddingLeft = base.paddingLeft;
|
||||
defined.paddingLeft = 1;
|
||||
}
|
||||
if (base.defined.paddingRight) {
|
||||
if (base.hasPaddingRight()) {
|
||||
paddingRight = base.paddingRight;
|
||||
defined.paddingRight = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Compatibility accessors for existing code that uses hasX pattern
|
||||
[[nodiscard]] bool hasTextAlign() const { return defined.alignment; }
|
||||
[[nodiscard]] bool hasTextAlign() const { return defined.textAlign; }
|
||||
[[nodiscard]] bool hasFontStyle() const { return defined.fontStyle; }
|
||||
[[nodiscard]] bool hasFontWeight() const { return defined.fontWeight; }
|
||||
[[nodiscard]] bool hasTextDecoration() const { return defined.decoration; }
|
||||
[[nodiscard]] bool hasTextIndent() const { return defined.indent; }
|
||||
[[nodiscard]] bool hasTextDecoration() const { return defined.textDecoration; }
|
||||
[[nodiscard]] bool hasTextIndent() const { return defined.textIndent; }
|
||||
[[nodiscard]] bool hasMarginTop() const { return defined.marginTop; }
|
||||
[[nodiscard]] bool hasMarginBottom() const { return defined.marginBottom; }
|
||||
[[nodiscard]] bool hasMarginLeft() const { return defined.marginLeft; }
|
||||
@ -183,15 +178,12 @@ struct CssStyle {
|
||||
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
|
||||
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
|
||||
|
||||
// Merge another style (alias for applyOver for compatibility)
|
||||
void merge(const CssStyle& other) { applyOver(other); }
|
||||
|
||||
void reset() {
|
||||
alignment = TextAlign::None;
|
||||
textAlign = CssTextAlign::Left;
|
||||
fontStyle = CssFontStyle::Normal;
|
||||
fontWeight = CssFontWeight::Normal;
|
||||
decoration = CssTextDecoration::None;
|
||||
indent = CssLength{};
|
||||
textDecoration = CssTextDecoration::None;
|
||||
textIndent = CssLength{};
|
||||
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
||||
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
||||
defined.clearAll();
|
||||
|
||||
@ -43,39 +43,17 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create a BlockStyle from CSS style properties, resolving CssLength values to pixels
|
||||
// emSize is the current font line height, used for em/rem unit conversion
|
||||
BlockStyle createBlockStyleFromCss(const CssStyle& cssStyle, const float emSize) {
|
||||
BlockStyle blockStyle;
|
||||
// Resolve all CssLength values to pixels using the current font's em size
|
||||
const int16_t marginTopPx = cssStyle.marginTop.toPixelsInt16(emSize);
|
||||
const int16_t marginBottomPx = cssStyle.marginBottom.toPixelsInt16(emSize);
|
||||
const int16_t paddingTopPx = cssStyle.paddingTop.toPixelsInt16(emSize);
|
||||
const int16_t paddingBottomPx = cssStyle.paddingBottom.toPixelsInt16(emSize);
|
||||
|
||||
// Vertical: combine margin and padding for top/bottom spacing
|
||||
blockStyle.marginTop = static_cast<int16_t>(marginTopPx + paddingTopPx);
|
||||
blockStyle.marginBottom = static_cast<int16_t>(marginBottomPx + paddingBottomPx);
|
||||
blockStyle.paddingTop = paddingTopPx;
|
||||
blockStyle.paddingBottom = paddingBottomPx;
|
||||
// Horizontal: store margin and padding separately for layout calculations
|
||||
blockStyle.marginLeft = cssStyle.marginLeft.toPixelsInt16(emSize);
|
||||
blockStyle.marginRight = cssStyle.marginRight.toPixelsInt16(emSize);
|
||||
blockStyle.paddingLeft = cssStyle.paddingLeft.toPixelsInt16(emSize);
|
||||
blockStyle.paddingRight = cssStyle.paddingRight.toPixelsInt16(emSize);
|
||||
// Text indent
|
||||
blockStyle.textIndent = cssStyle.indent.toPixelsInt16(emSize);
|
||||
blockStyle.textIndentDefined = cssStyle.defined.indent;
|
||||
return blockStyle;
|
||||
bool isHeaderOrBlock(const char* name) {
|
||||
return matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
||||
}
|
||||
|
||||
// Update effective bold/italic/underline based on block style and inline style stack
|
||||
void ChapterHtmlSlimParser::updateEffectiveInlineStyle() {
|
||||
// Start with block-level styles
|
||||
effectiveBold = currentBlockStyle.hasFontWeight() && currentBlockStyle.fontWeight == CssFontWeight::Bold;
|
||||
effectiveItalic = currentBlockStyle.hasFontStyle() && currentBlockStyle.fontStyle == CssFontStyle::Italic;
|
||||
effectiveBold = currentCssStyle.hasFontWeight() && currentCssStyle.fontWeight == CssFontWeight::Bold;
|
||||
effectiveItalic = currentCssStyle.hasFontStyle() && currentCssStyle.fontStyle == CssFontStyle::Italic;
|
||||
effectiveUnderline =
|
||||
currentBlockStyle.hasTextDecoration() && currentBlockStyle.decoration == CssTextDecoration::Underline;
|
||||
currentCssStyle.hasTextDecoration() && currentCssStyle.textDecoration == CssTextDecoration::Underline;
|
||||
|
||||
// Apply inline style stack in order
|
||||
for (const auto& entry : inlineStyleStack) {
|
||||
@ -98,69 +76,41 @@ void ChapterHtmlSlimParser::flushPartWordBuffer() {
|
||||
const bool isItalic = italicUntilDepth < depth || effectiveItalic;
|
||||
const bool isUnderline = underlineUntilDepth < depth || effectiveUnderline;
|
||||
|
||||
// Combine style flags using bitwise OR
|
||||
EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR;
|
||||
if (isBold && isItalic) {
|
||||
fontStyle = EpdFontFamily::BOLD_ITALIC;
|
||||
} else if (isBold) {
|
||||
fontStyle = EpdFontFamily::BOLD;
|
||||
} else if (isItalic) {
|
||||
fontStyle = EpdFontFamily::ITALIC;
|
||||
if (isBold) {
|
||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::BOLD);
|
||||
}
|
||||
if (isItalic) {
|
||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::ITALIC);
|
||||
}
|
||||
if (isUnderline) {
|
||||
fontStyle = static_cast<EpdFontFamily::Style>(fontStyle | EpdFontFamily::UNDERLINE);
|
||||
}
|
||||
|
||||
// flush the buffer
|
||||
partWordBuffer[partWordBufferIndex] = '\0';
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle, isUnderline);
|
||||
currentTextBlock->addWord(partWordBuffer, fontStyle);
|
||||
partWordBufferIndex = 0;
|
||||
}
|
||||
|
||||
// Merge block styles for nested block elements
|
||||
// When a child block element is inside a parent with no direct text content,
|
||||
// we accumulate their margins so nested containers properly contribute spacing
|
||||
BlockStyle mergeBlockStyles(const BlockStyle& parent, const BlockStyle& child) {
|
||||
BlockStyle merged;
|
||||
// Vertical margins: sum them (nested blocks create additive spacing)
|
||||
merged.marginTop = static_cast<int16_t>(parent.marginTop + child.marginTop);
|
||||
merged.marginBottom = static_cast<int16_t>(parent.marginBottom + child.marginBottom);
|
||||
// Horizontal margins: sum them (nested blocks create cumulative indentation)
|
||||
merged.marginLeft = static_cast<int16_t>(parent.marginLeft + child.marginLeft);
|
||||
merged.marginRight = static_cast<int16_t>(parent.marginRight + child.marginRight);
|
||||
// Padding: sum them
|
||||
merged.paddingTop = static_cast<int16_t>(parent.paddingTop + child.paddingTop);
|
||||
merged.paddingBottom = static_cast<int16_t>(parent.paddingBottom + child.paddingBottom);
|
||||
merged.paddingLeft = static_cast<int16_t>(parent.paddingLeft + child.paddingLeft);
|
||||
merged.paddingRight = static_cast<int16_t>(parent.paddingRight + child.paddingRight);
|
||||
// Text indent: use child's if defined, otherwise inherit parent's
|
||||
if (child.textIndentDefined) {
|
||||
merged.textIndent = child.textIndent;
|
||||
merged.textIndentDefined = true;
|
||||
} else if (parent.textIndentDefined) {
|
||||
merged.textIndent = parent.textIndent;
|
||||
merged.textIndentDefined = true;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style, const BlockStyle& blockStyle) {
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const BlockStyle& blockStyle) {
|
||||
if (currentTextBlock) {
|
||||
// already have a text block running and it is empty - just reuse it
|
||||
if (currentTextBlock->isEmpty()) {
|
||||
currentTextBlock->setStyle(style);
|
||||
// Merge with existing block style to accumulate margins from parent block elements
|
||||
// This handles cases like <div margin-bottom:2em><h1>text</h1></div> where the
|
||||
// div's margin should be preserved even though it has no direct text content
|
||||
const BlockStyle merged = mergeBlockStyles(currentTextBlock->getBlockStyle(), blockStyle);
|
||||
currentTextBlock->setBlockStyle(merged);
|
||||
// Merge with existing block style to accumulate CSS styling from parent block elements.
|
||||
// This handles cases like <div style="margin-bottom:2em"><h1>text</h1></div> where the
|
||||
// div's margin should be preserved, even though it has no direct text content.
|
||||
currentTextBlock->setBlockStyle(currentTextBlock->getBlockStyle().getCombinedBlockStyle(blockStyle));
|
||||
return;
|
||||
}
|
||||
|
||||
makePages();
|
||||
}
|
||||
currentTextBlock.reset(new ParsedText(style, extraParagraphSpacing, hyphenationEnabled, blockStyle));
|
||||
currentTextBlock.reset(new ParsedText(extraParagraphSpacing, hyphenationEnabled, blockStyle));
|
||||
}
|
||||
|
||||
void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) { startNewTextBlock(style, BlockStyle{}); }
|
||||
|
||||
void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<ChapterHtmlSlimParser*>(userData);
|
||||
|
||||
@ -183,13 +133,17 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
}
|
||||
|
||||
auto centeredBlockStyle = BlockStyle();
|
||||
centeredBlockStyle.textAlignDefined = true;
|
||||
centeredBlockStyle.alignment = CssTextAlign::Center;
|
||||
|
||||
// Special handling for tables - show placeholder text instead of dropping silently
|
||||
if (strcmp(name, "table") == 0) {
|
||||
// Add placeholder text
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
self->startNewTextBlock(centeredBlockStyle);
|
||||
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for a element with text)
|
||||
// Advance depth before processing character data (like you would for an element with text)
|
||||
self->depth += 1;
|
||||
self->characterData(userData, "[Table omitted]", strlen("[Table omitted]"));
|
||||
|
||||
@ -214,9 +168,9 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
|
||||
Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str());
|
||||
|
||||
self->startNewTextBlock(TextBlock::CENTER_ALIGN);
|
||||
self->startNewTextBlock(centeredBlockStyle);
|
||||
self->italicUntilDepth = min(self->italicUntilDepth, self->depth);
|
||||
// Advance depth before processing character data (like you would for a element with text)
|
||||
// Advance depth before processing character data (like you would for an element with text)
|
||||
self->depth += 1;
|
||||
self->characterData(userData, alt.c_str(), alt.length());
|
||||
|
||||
@ -244,9 +198,6 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if this is a block element
|
||||
bool isBlockElement = matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS);
|
||||
|
||||
// Compute CSS style for this element
|
||||
CssStyle cssStyle;
|
||||
if (self->cssParser) {
|
||||
@ -255,34 +206,16 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
// Merge inline style (highest priority)
|
||||
if (!styleAttr.empty()) {
|
||||
CssStyle inlineStyle = CssParser::parseInlineStyle(styleAttr);
|
||||
cssStyle.merge(inlineStyle);
|
||||
cssStyle.applyOver(inlineStyle);
|
||||
}
|
||||
}
|
||||
|
||||
const float emSize = static_cast<float>(self->renderer.getLineHeight(self->fontId)) * self->lineCompression;
|
||||
const auto userAlignment = static_cast<CssTextAlign>(self->paragraphAlignment);
|
||||
|
||||
if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||
// Headers: center aligned, bold, apply CSS overrides
|
||||
TextBlock::Style alignment = TextBlock::CENTER_ALIGN;
|
||||
if (cssStyle.hasTextAlign()) {
|
||||
switch (cssStyle.alignment) {
|
||||
case TextAlign::Left:
|
||||
alignment = TextBlock::LEFT_ALIGN;
|
||||
break;
|
||||
case TextAlign::Right:
|
||||
alignment = TextBlock::RIGHT_ALIGN;
|
||||
break;
|
||||
case TextAlign::Center:
|
||||
alignment = TextBlock::CENTER_ALIGN;
|
||||
break;
|
||||
case TextAlign::Justify:
|
||||
alignment = TextBlock::JUSTIFIED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self->currentBlockStyle = cssStyle;
|
||||
self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle, self->renderer.getLineHeight(self->fontId)));
|
||||
self->currentCssStyle = cssStyle;
|
||||
self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment));
|
||||
self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth);
|
||||
self->updateEffectiveInlineStyle();
|
||||
} else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||
@ -291,31 +224,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
// flush word preceding <br/> to currentTextBlock before calling startNewTextBlock
|
||||
self->flushPartWordBuffer();
|
||||
}
|
||||
self->startNewTextBlock(self->currentTextBlock->getStyle());
|
||||
self->startNewTextBlock(self->currentTextBlock->getBlockStyle());
|
||||
} else {
|
||||
// Determine alignment from CSS or default
|
||||
auto alignment = static_cast<TextBlock::Style>(self->paragraphAlignment);
|
||||
if (cssStyle.hasTextAlign()) {
|
||||
switch (cssStyle.alignment) {
|
||||
case TextAlign::Left:
|
||||
alignment = TextBlock::LEFT_ALIGN;
|
||||
break;
|
||||
case TextAlign::Right:
|
||||
alignment = TextBlock::RIGHT_ALIGN;
|
||||
break;
|
||||
case TextAlign::Center:
|
||||
alignment = TextBlock::CENTER_ALIGN;
|
||||
break;
|
||||
case TextAlign::Justify:
|
||||
alignment = TextBlock::JUSTIFIED;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self->currentBlockStyle = cssStyle;
|
||||
self->startNewTextBlock(alignment, createBlockStyleFromCss(cssStyle, self->renderer.getLineHeight(self->fontId)));
|
||||
self->currentCssStyle = cssStyle;
|
||||
self->startNewTextBlock(BlockStyle::fromCssStyle(cssStyle, emSize, userAlignment));
|
||||
self->updateEffectiveInlineStyle();
|
||||
|
||||
if (strcmp(name, "li") == 0) {
|
||||
@ -352,7 +264,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
if (cssStyle.hasTextDecoration()) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = cssStyle.decoration == CssTextDecoration::Underline;
|
||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
@ -369,11 +281,11 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
if (cssStyle.hasTextDecoration()) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = cssStyle.decoration == CssTextDecoration::Underline;
|
||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
} else if (strcmp(name, "span") == 0 || !isBlockElement) {
|
||||
} else if (strcmp(name, "span") == 0 || !isHeaderOrBlock(name)) {
|
||||
// Handle span and other inline elements for CSS styling
|
||||
if (cssStyle.hasFontWeight() || cssStyle.hasFontStyle() || cssStyle.hasTextDecoration()) {
|
||||
StyleStackEntry entry;
|
||||
@ -388,7 +300,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char*
|
||||
}
|
||||
if (cssStyle.hasTextDecoration()) {
|
||||
entry.hasUnderline = true;
|
||||
entry.underline = cssStyle.decoration == CssTextDecoration::Underline;
|
||||
entry.underline = cssStyle.textDecoration == CssTextDecoration::Underline;
|
||||
}
|
||||
self->inlineStyleStack.push_back(entry);
|
||||
self->updateEffectiveInlineStyle();
|
||||
@ -464,12 +376,12 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
const bool willClearUnderline = self->underlineUntilDepth == self->depth - 1;
|
||||
|
||||
const bool styleWillChange = willPopStyleStack || willClearBold || willClearItalic || willClearUnderline;
|
||||
const bool headerOrBlockTag = isHeaderOrBlock(name);
|
||||
|
||||
// Flush buffer with current style BEFORE any style changes
|
||||
if (self->partWordBufferIndex > 0) {
|
||||
// Flush if style will change OR if we're closing a block/structural element
|
||||
const bool shouldFlush = styleWillChange || matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) ||
|
||||
matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||
const bool shouldFlush = styleWillChange || headerOrBlockTag || matches(name, BOLD_TAGS, NUM_BOLD_TAGS) ||
|
||||
matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) ||
|
||||
matches(name, UNDERLINE_TAGS, NUM_UNDERLINE_TAGS) || strcmp(name, "table") == 0 ||
|
||||
matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1;
|
||||
@ -508,15 +420,18 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n
|
||||
self->updateEffectiveInlineStyle();
|
||||
}
|
||||
|
||||
// Clear block style when leaving block elements
|
||||
if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||
self->currentBlockStyle.reset();
|
||||
// Clear block style when leaving header or block elements
|
||||
if (headerOrBlockTag) {
|
||||
self->currentCssStyle.reset();
|
||||
self->updateEffectiveInlineStyle();
|
||||
}
|
||||
}
|
||||
|
||||
bool ChapterHtmlSlimParser::parseAndBuildPages() {
|
||||
startNewTextBlock((TextBlock::Style)this->paragraphAlignment);
|
||||
auto paragraphAlignmentBlockStyle = BlockStyle();
|
||||
paragraphAlignmentBlockStyle.textAlignDefined = true;
|
||||
paragraphAlignmentBlockStyle.alignment = static_cast<CssTextAlign>(this->paragraphAlignment);
|
||||
startNewTextBlock(paragraphAlignmentBlockStyle);
|
||||
|
||||
const XML_Parser parser = XML_ParserCreate(nullptr);
|
||||
int done;
|
||||
@ -624,11 +539,14 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
|
||||
const int lineHeight = renderer.getLineHeight(fontId) * lineCompression;
|
||||
|
||||
// Apply marginTop before the paragraph (stored in pixels)
|
||||
// Apply top spacing before the paragraph (stored in pixels)
|
||||
const BlockStyle& blockStyle = currentTextBlock->getBlockStyle();
|
||||
if (blockStyle.marginTop > 0) {
|
||||
currentPageNextY += blockStyle.marginTop;
|
||||
}
|
||||
if (blockStyle.paddingTop > 0) {
|
||||
currentPageNextY += blockStyle.paddingTop;
|
||||
}
|
||||
|
||||
// Calculate effective width accounting for horizontal margins/padding
|
||||
const int horizontalInset = blockStyle.totalHorizontalInset();
|
||||
@ -639,10 +557,13 @@ void ChapterHtmlSlimParser::makePages() {
|
||||
renderer, fontId, effectiveWidth,
|
||||
[this](const std::shared_ptr<TextBlock>& textBlock) { addLineToPage(textBlock); });
|
||||
|
||||
// Apply marginBottom after the paragraph (stored in pixels)
|
||||
// Apply bottom spacing after the paragraph (stored in pixels)
|
||||
if (blockStyle.marginBottom > 0) {
|
||||
currentPageNextY += blockStyle.marginBottom;
|
||||
}
|
||||
if (blockStyle.paddingBottom > 0) {
|
||||
currentPageNextY += blockStyle.paddingBottom;
|
||||
}
|
||||
|
||||
// Extra paragraph spacing if enabled (default behavior)
|
||||
if (extraParagraphSpacing) {
|
||||
|
||||
@ -50,14 +50,13 @@ class ChapterHtmlSlimParser {
|
||||
bool hasUnderline = false, underline = false;
|
||||
};
|
||||
std::vector<StyleStackEntry> inlineStyleStack;
|
||||
CssStyle currentBlockStyle;
|
||||
CssStyle currentCssStyle;
|
||||
bool effectiveBold = false;
|
||||
bool effectiveItalic = false;
|
||||
bool effectiveUnderline = false;
|
||||
|
||||
void updateEffectiveInlineStyle();
|
||||
void startNewTextBlock(TextBlock::Style style, const BlockStyle& blockStyle);
|
||||
void startNewTextBlock(TextBlock::Style style);
|
||||
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||
void flushPartWordBuffer();
|
||||
void makePages();
|
||||
// XML callbacks
|
||||
|
||||
@ -470,7 +470,7 @@ int GfxRenderer::getSpaceWidth(const int fontId) const {
|
||||
return fontMap.at(fontId).getGlyph(' ', EpdFontFamily::REGULAR)->advanceX;
|
||||
}
|
||||
|
||||
int GfxRenderer::getIndentWidth(const int fontId, const char* text) const {
|
||||
int GfxRenderer::getTextAdvanceX(const int fontId, const char* text) const {
|
||||
if (fontMap.count(fontId) == 0) {
|
||||
Serial.printf("[%lu] [GFX] Font %d not found\n", millis(), fontId);
|
||||
return 0;
|
||||
|
||||
@ -78,7 +78,7 @@ class GfxRenderer {
|
||||
void drawText(int fontId, int x, int y, const char* text, bool black = true,
|
||||
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
|
||||
int getSpaceWidth(int fontId) const;
|
||||
int getIndentWidth(int fontId, const char* text) const;
|
||||
int getTextAdvanceX(int fontId, const char* text) const;
|
||||
int getFontAscenderSize(int fontId) const;
|
||||
int getLineHeight(int fontId) const;
|
||||
std::string truncatedText(int fontId, const char* text, int maxWidth,
|
||||
|
||||
@ -238,7 +238,8 @@ void SleepActivity::renderCoverSleepScreen() const {
|
||||
} else if (StringUtils::checkFileExtension(APP_STATE.openEpubPath, ".epub")) {
|
||||
// Handle EPUB file
|
||||
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||
if (!lastEpub.load()) {
|
||||
// Skip loading css since we only need metadata here
|
||||
if (!lastEpub.load(true, true)) {
|
||||
Serial.println("[SLP] Failed to load last epub");
|
||||
return renderDefaultSleepScreen();
|
||||
}
|
||||
|
||||
@ -52,7 +52,8 @@ void HomeActivity::onEnter() {
|
||||
// If epub, try to load the metadata for title/author and cover
|
||||
if (StringUtils::checkFileExtension(lastBookTitle, ".epub")) {
|
||||
Epub epub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||
epub.load(false);
|
||||
// Skip loading css since we only need metadata here
|
||||
epub.load(false, true);
|
||||
if (!epub.getTitle().empty()) {
|
||||
lastBookTitle = std::string(epub.getTitle());
|
||||
}
|
||||
|
||||
@ -520,7 +520,7 @@ void WifiSelectionActivity::renderNetworkList() const {
|
||||
const auto height = renderer.getLineHeight(UI_10_FONT_ID);
|
||||
const auto top = (pageHeight - height) / 2;
|
||||
renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found");
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again");
|
||||
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press Connect to scan again");
|
||||
} else {
|
||||
// Calculate how many networks we can display
|
||||
constexpr int startY = 60;
|
||||
@ -546,8 +546,8 @@ void WifiSelectionActivity::renderNetworkList() const {
|
||||
|
||||
// Draw network name (truncate if too long)
|
||||
std::string displayName = network.ssid;
|
||||
if (displayName.length() > 16) {
|
||||
displayName.replace(13, displayName.length() - 13, "...");
|
||||
if (displayName.length() > 33) {
|
||||
displayName.replace(30, displayName.length() - 30, "...");
|
||||
}
|
||||
renderer.drawText(UI_10_FONT_ID, 20, networkY, displayName.c_str());
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user