cottongin dd630dcf72
Improve EPUB rendering and add end-of-book Start Over
EPUB rendering improvements:
- Add margin-left/padding-left CSS parsing for block indentation
- Add vertical bar and italic styling for blockquotes
- Add left margin indentation for list items (ol/ul)
- Fix ordered lists showing bullets instead of numbers
- Fix nested <p> inside <li> causing marker on separate line

End-of-book improvements:
- Add "Start Over" option to wrap to first page when pressing next
- Show "Start Over" button hint on finished book prompt
2026-01-29 19:45:58 -05:00

166 lines
6.3 KiB
C++

#include "TextBlock.h"
#include <GfxRenderer.h>
#include <Serialization.h>
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
// Validate iterator bounds before rendering
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
Serial.printf("[%lu] [TXB] Render skipped: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
(uint32_t)words.size(), (uint32_t)wordXpos.size(), (uint32_t)wordStyles.size());
return;
}
// Draw left border (vertical bar) for blockquotes
if (blockStyle.hasLeftBorder && blockStyle.marginLeft > 0) {
const int lineHeight = renderer.getLineHeight(fontId);
const int barX = x + 4; // Small offset from left edge
const int barTop = y;
const int barBottom = y + lineHeight;
// Draw a 2-pixel wide vertical bar
renderer.drawLine(barX, barTop, barX, barBottom, true);
renderer.drawLine(barX + 1, barTop, barX + 1, barBottom, true);
}
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);
// Draw underline if word is underlined
if (wordUnderlineIt != wordUnderlines.end() && *wordUnderlineIt) {
const std::string& w = *wordIt;
const int fullWordWidth = renderer.getTextWidth(fontId, w.c_str(), *wordStylesIt);
// 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;
int startX = wordX;
int underlineWidth = fullWordWidth;
// if word starts with em-space ("\xe2\x80\x83"), account for the additional indent before drawing the line
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);
startX = wordX + prefixWidth;
underlineWidth = visibleWidth;
}
renderer.drawLine(startX, underlineY, startX + underlineWidth, underlineY, true);
}
std::advance(wordIt, 1);
std::advance(wordStylesIt, 1);
std::advance(wordXposIt, 1);
if (wordUnderlineIt != wordUnderlines.end()) {
std::advance(wordUnderlineIt, 1);
}
}
}
bool TextBlock::serialize(FsFile& file) const {
if (words.size() != wordXpos.size() || words.size() != wordStyles.size()) {
Serial.printf("[%lu] [TXB] Serialization failed: size mismatch (words=%u, xpos=%u, styles=%u)\n", millis(),
words.size(), wordXpos.size(), wordStyles.size());
return false;
}
// Word data
serialization::writePod(file, static_cast<uint16_t>(words.size()));
for (const auto& w : words) serialization::writeString(file, w);
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)
serialization::writePod(file, blockStyle.marginTop);
serialization::writePod(file, blockStyle.marginBottom);
serialization::writePod(file, blockStyle.paddingTop);
serialization::writePod(file, blockStyle.paddingBottom);
serialization::writePod(file, blockStyle.textIndent);
serialization::writePod(file, blockStyle.marginLeft);
serialization::writePod(file, blockStyle.hasLeftBorder);
return true;
}
std::unique_ptr<TextBlock> TextBlock::deserialize(FsFile& file) {
uint16_t wc;
std::vector<std::string> words;
std::vector<uint16_t> wordXpos;
std::vector<EpdFontFamily::Style> wordStyles;
std::vector<bool> wordUnderlines;
Style style;
BlockStyle blockStyle;
// Word count
serialization::readPod(file, wc);
// Sanity check: prevent allocation of unreasonably large vectors (max 10000 words per block)
if (wc > 10000) {
Serial.printf("[%lu] [TXB] Deserialization failed: word count %u exceeds maximum\n", millis(), wc);
return nullptr;
}
// Word data - reserve capacity then resize
words.resize(wc);
wordXpos.resize(wc);
wordStyles.resize(wc);
for (auto& w : words) serialization::readString(file, w);
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);
size_t underlineIdx = 0;
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 && underlineIdx < wc; bit++) {
wordUnderlines[underlineIdx] = (underlineByte & (1 << bit)) != 0;
++underlineIdx;
}
}
// Block style (alignment)
serialization::readPod(file, style);
// Block style (margins/padding/indent)
serialization::readPod(file, blockStyle.marginTop);
serialization::readPod(file, blockStyle.marginBottom);
serialization::readPod(file, blockStyle.paddingTop);
serialization::readPod(file, blockStyle.paddingBottom);
serialization::readPod(file, blockStyle.textIndent);
serialization::readPod(file, blockStyle.marginLeft);
serialization::readPod(file, blockStyle.hasLeftBorder);
return std::unique_ptr<TextBlock>(new TextBlock(std::move(words), std::move(wordXpos), std::move(wordStyles), style,
blockStyle, std::move(wordUnderlines)));
}