Public release
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
#include "EpdFont.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
EpdFont::EpdFont(const EpdFontData* data) { this->data = data; }
|
||||
|
||||
inline int min(const int a, const int b) { return a < b ? a : b; }
|
||||
inline int max(const int a, const int b) { return a < b ? b : a; }
|
||||
|
||||
// TODO: Text properties??
|
||||
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
|
||||
int* maxY) const {
|
||||
*minX = startX;
|
||||
*minY = startY;
|
||||
*maxX = startX;
|
||||
*maxY = startY;
|
||||
|
||||
if (*string == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
int cursorX = startX;
|
||||
const int cursorY = startY;
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||
const EpdGlyph* glyph = getGlyph(cp);
|
||||
|
||||
if (!glyph) {
|
||||
// TODO: Replace with fallback glyph property?
|
||||
glyph = getGlyph('?');
|
||||
}
|
||||
|
||||
if (!glyph) {
|
||||
// TODO: Better handle this?
|
||||
continue;
|
||||
}
|
||||
|
||||
*minX = min(*minX, cursorX + glyph->left);
|
||||
*maxX = max(*maxX, cursorX + glyph->left + glyph->width);
|
||||
*minY = min(*minY, cursorY + glyph->top - glyph->height);
|
||||
*maxY = max(*maxY, cursorY + glyph->top);
|
||||
cursorX += glyph->advanceX;
|
||||
}
|
||||
}
|
||||
|
||||
void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
|
||||
int minX = 0, minY = 0, maxX = 0, maxY = 0;
|
||||
|
||||
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY);
|
||||
|
||||
*w = maxX - minX;
|
||||
*h = maxY - minY;
|
||||
}
|
||||
|
||||
bool EpdFont::hasPrintableChars(const char* string) const {
|
||||
int w = 0, h = 0;
|
||||
|
||||
getTextDimensions(string, &w, &h);
|
||||
|
||||
return w > 0 || h > 0;
|
||||
}
|
||||
|
||||
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
||||
const EpdUnicodeInterval* intervals = data->intervals;
|
||||
for (int i = 0; i < data->intervalCount; i++) {
|
||||
const EpdUnicodeInterval* interval = &intervals[i];
|
||||
if (cp >= interval->first && cp <= interval->last) {
|
||||
return &data->glyph[interval->offset + (cp - interval->first)];
|
||||
}
|
||||
if (cp < interval->first) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include "EpdFontData.h"
|
||||
|
||||
class EpdFont {
|
||||
void getTextBounds(const char* string, int startX, int startY, int* minX, int* minY, int* maxX, int* maxY) const;
|
||||
|
||||
public:
|
||||
const EpdFontData* data;
|
||||
explicit EpdFont(const EpdFontData* data);
|
||||
~EpdFont() = default;
|
||||
void getTextDimensions(const char* string, int* w, int* h) const;
|
||||
bool hasPrintableChars(const char* string) const;
|
||||
|
||||
const EpdGlyph* getGlyph(uint32_t cp) const;
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
// From
|
||||
// https://github.com/vroland/epdiy/blob/c61e9e923ce2418150d54f88cea5d196cdc40c54/src/epd_internals.h
|
||||
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
/// Font data stored PER GLYPH
|
||||
typedef struct {
|
||||
uint8_t width; ///< Bitmap dimensions in pixels
|
||||
uint8_t height; ///< Bitmap dimensions in pixels
|
||||
uint8_t advanceX; ///< Distance to advance cursor (x axis)
|
||||
int16_t left; ///< X dist from cursor pos to UL corner
|
||||
int16_t top; ///< Y dist from cursor pos to UL corner
|
||||
uint16_t compressedSize; ///< Size of the zlib-compressed font data.
|
||||
uint32_t dataOffset; ///< Pointer into EpdFont->bitmap
|
||||
} EpdGlyph;
|
||||
|
||||
/// Glyph interval structure
|
||||
typedef struct {
|
||||
uint32_t first; ///< The first unicode code point of the interval
|
||||
uint32_t last; ///< The last unicode code point of the interval
|
||||
uint32_t offset; ///< Index of the first code point into the glyph array
|
||||
} EpdUnicodeInterval;
|
||||
|
||||
/// Data stored for FONT AS A WHOLE
|
||||
typedef struct {
|
||||
const uint8_t* bitmap; ///< Glyph bitmaps, concatenated
|
||||
const EpdGlyph* glyph; ///< Glyph array
|
||||
const EpdUnicodeInterval* intervals; ///< Valid unicode intervals for this font
|
||||
uint32_t intervalCount; ///< Number of unicode intervals.
|
||||
bool compressed; ///< Does this font use compressed glyph bitmaps?
|
||||
uint8_t advanceY; ///< Newline distance (y axis)
|
||||
int ascender; ///< Maximal height of a glyph above the base line
|
||||
int descender; ///< Maximal height of a glyph below the base line
|
||||
} EpdFontData;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Executable
+237
@@ -0,0 +1,237 @@
|
||||
#!python3
|
||||
import freetype
|
||||
import zlib
|
||||
import sys
|
||||
import re
|
||||
import math
|
||||
import argparse
|
||||
from collections import namedtuple
|
||||
|
||||
# From: https://github.com/vroland/epdiy
|
||||
|
||||
parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.")
|
||||
parser.add_argument("name", action="store", help="name of the font.")
|
||||
parser.add_argument("size", type=int, help="font size to use.")
|
||||
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
|
||||
parser.add_argument("--compress", dest="compress", action="store_true", help="compress glyph bitmaps.")
|
||||
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
|
||||
args = parser.parse_args()
|
||||
|
||||
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "compressed_size", "data_offset", "code_point"])
|
||||
|
||||
font_stack = [freetype.Face(f) for f in args.fontstack]
|
||||
compress = args.compress
|
||||
size = args.size
|
||||
font_name = args.name
|
||||
|
||||
# inclusive unicode code point intervals
|
||||
# must not overlap and be in ascending order
|
||||
intervals = [
|
||||
### Basic Latin ###
|
||||
# ASCII letters, digits, punctuation, control characters
|
||||
(0x0000, 0x007F),
|
||||
### Latin-1 Supplement ###
|
||||
# Accented characters for Western European languages
|
||||
(0x0080, 0x00FF),
|
||||
### Latin Extended-A ###
|
||||
# Eastern European and Baltic languages
|
||||
(0x0100, 0x017F),
|
||||
### General Punctuation (core subset) ###
|
||||
# Smart quotes, en dash, em dash, ellipsis, NO-BREAK SPACE
|
||||
(0x2000, 0x206F),
|
||||
### Basic Symbols From "Latin-1 + Misc" ###
|
||||
# dashes, quotes, prime marks
|
||||
(0x2010, 0x203A),
|
||||
# misc punctuation
|
||||
(0x2040, 0x205F),
|
||||
# common currency symbols
|
||||
(0x20A0, 0x20CF),
|
||||
### Combining Diacritical Marks (minimal subset) ###
|
||||
# Needed for proper rendering of many extended Latin languages
|
||||
(0x0300, 0x036F),
|
||||
### Greek & Coptic ###
|
||||
# Used in science, maths, philosophy, some academic texts
|
||||
# (0x0370, 0x03FF),
|
||||
### Cyrillic ###
|
||||
# Russian, Ukrainian, Bulgarian, etc.
|
||||
# (0x0400, 0x04FF),
|
||||
### Math Symbols (common subset) ###
|
||||
# General math operators
|
||||
(0x2200, 0x22FF),
|
||||
# Arrows
|
||||
(0x2190, 0x21FF),
|
||||
### CJK ###
|
||||
# Core Unified Ideographs
|
||||
# (0x4E00, 0x9FFF),
|
||||
# # Extension A
|
||||
# (0x3400, 0x4DBF),
|
||||
# # Extension B
|
||||
# (0x20000, 0x2A6DF),
|
||||
# # Extension C–F
|
||||
# (0x2A700, 0x2EBEF),
|
||||
# # Extension G
|
||||
# (0x30000, 0x3134F),
|
||||
# # Hiragana
|
||||
# (0x3040, 0x309F),
|
||||
# # Katakana
|
||||
# (0x30A0, 0x30FF),
|
||||
# # Katakana Phonetic Extensions
|
||||
# (0x31F0, 0x31FF),
|
||||
# # Halfwidth Katakana
|
||||
# (0xFF60, 0xFF9F),
|
||||
# # Hangul Syllables
|
||||
# (0xAC00, 0xD7AF),
|
||||
# # Hangul Jamo
|
||||
# (0x1100, 0x11FF),
|
||||
# # Hangul Compatibility Jamo
|
||||
# (0x3130, 0x318F),
|
||||
# # Hangul Jamo Extended-A
|
||||
# (0xA960, 0xA97F),
|
||||
# # Hangul Jamo Extended-B
|
||||
# (0xD7B0, 0xD7FF),
|
||||
# # CJK Radicals Supplement
|
||||
# (0x2E80, 0x2EFF),
|
||||
# # Kangxi Radicals
|
||||
# (0x2F00, 0x2FDF),
|
||||
# # CJK Symbols and Punctuation
|
||||
# (0x3000, 0x303F),
|
||||
# # CJK Compatibility Forms
|
||||
# (0xFE30, 0xFE4F),
|
||||
# # CJK Compatibility Ideographs
|
||||
# (0xF900, 0xFAFF),
|
||||
]
|
||||
|
||||
add_ints = []
|
||||
if args.additional_intervals:
|
||||
add_ints = [tuple([int(n, base=0) for n in i.split(",")]) for i in args.additional_intervals]
|
||||
|
||||
def norm_floor(val):
|
||||
return int(math.floor(val / (1 << 6)))
|
||||
|
||||
def norm_ceil(val):
|
||||
return int(math.ceil(val / (1 << 6)))
|
||||
|
||||
def chunks(l, n):
|
||||
for i in range(0, len(l), n):
|
||||
yield l[i:i + n]
|
||||
|
||||
def load_glyph(code_point):
|
||||
face_index = 0
|
||||
while face_index < len(font_stack):
|
||||
face = font_stack[face_index]
|
||||
glyph_index = face.get_char_index(code_point)
|
||||
if glyph_index > 0:
|
||||
face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER)
|
||||
return face
|
||||
face_index += 1
|
||||
print(f"code point {code_point} ({hex(code_point)}) not found in font stack!", file=sys.stderr)
|
||||
return None
|
||||
|
||||
unmerged_intervals = sorted(intervals + add_ints)
|
||||
intervals = []
|
||||
unvalidated_intervals = []
|
||||
for i_start, i_end in unmerged_intervals:
|
||||
if len(unvalidated_intervals) > 0 and i_start + 1 <= unvalidated_intervals[-1][1]:
|
||||
unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end))
|
||||
continue
|
||||
unvalidated_intervals.append((i_start, i_end))
|
||||
|
||||
for i_start, i_end in unvalidated_intervals:
|
||||
start = i_start
|
||||
for code_point in range(i_start, i_end + 1):
|
||||
face = load_glyph(code_point)
|
||||
if face is None:
|
||||
if start < code_point:
|
||||
intervals.append((start, code_point - 1))
|
||||
start = code_point + 1
|
||||
if start != i_end + 1:
|
||||
intervals.append((start, i_end))
|
||||
|
||||
for face in font_stack:
|
||||
# shift by 6 bytes, because sizes are given as 6-bit fractions
|
||||
# the display has about 150 dpi.
|
||||
face.set_char_size(size << 6, size << 6, 150, 150)
|
||||
|
||||
total_size = 0
|
||||
total_packed = 0
|
||||
all_glyphs = []
|
||||
|
||||
for i_start, i_end in intervals:
|
||||
for code_point in range(i_start, i_end + 1):
|
||||
face = load_glyph(code_point)
|
||||
bitmap = face.glyph.bitmap
|
||||
pixels = []
|
||||
px = 0
|
||||
for i, v in enumerate(bitmap.buffer):
|
||||
y = i / bitmap.width
|
||||
x = i % bitmap.width
|
||||
if x % 2 == 0:
|
||||
px = (v >> 4)
|
||||
else:
|
||||
px = px | (v & 0xF0)
|
||||
pixels.append(px);
|
||||
px = 0
|
||||
# eol
|
||||
if x == bitmap.width - 1 and bitmap.width % 2 > 0:
|
||||
pixels.append(px)
|
||||
px = 0
|
||||
|
||||
packed = bytes(pixels);
|
||||
total_packed += len(packed)
|
||||
compressed = packed
|
||||
if compress:
|
||||
compressed = zlib.compress(packed)
|
||||
|
||||
glyph = GlyphProps(
|
||||
width = bitmap.width,
|
||||
height = bitmap.rows,
|
||||
advance_x = norm_floor(face.glyph.advance.x),
|
||||
left = face.glyph.bitmap_left,
|
||||
top = face.glyph.bitmap_top,
|
||||
compressed_size = len(compressed),
|
||||
data_offset = total_size,
|
||||
code_point = code_point,
|
||||
)
|
||||
total_size += len(compressed)
|
||||
all_glyphs.append((glyph, compressed))
|
||||
|
||||
# pipe seems to be a good heuristic for the "real" descender
|
||||
face = load_glyph(ord('|'))
|
||||
|
||||
glyph_data = []
|
||||
glyph_props = []
|
||||
for index, glyph in enumerate(all_glyphs):
|
||||
props, compressed = glyph
|
||||
glyph_data.extend([b for b in compressed])
|
||||
glyph_props.append(props)
|
||||
|
||||
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * compressed: {compress}\n */")
|
||||
print("#pragma once")
|
||||
print("#include \"EpdFontData.h\"\n")
|
||||
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
|
||||
for c in chunks(glyph_data, 16):
|
||||
print (" " + " ".join(f"0x{b:02X}," for b in c))
|
||||
print ("};\n");
|
||||
|
||||
print(f"static const EpdGlyph {font_name}Glyphs[] = {{")
|
||||
for i, g in enumerate(glyph_props):
|
||||
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else '<backslash>'}")
|
||||
print ("};\n");
|
||||
|
||||
print(f"static const EpdUnicodeInterval {font_name}Intervals[] = {{")
|
||||
offset = 0
|
||||
for i_start, i_end in intervals:
|
||||
print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},")
|
||||
offset += i_end - i_start + 1
|
||||
print ("};\n");
|
||||
|
||||
print(f"static const EpdFontData {font_name} = {{")
|
||||
print(f" {font_name}Bitmaps,")
|
||||
print(f" {font_name}Glyphs,")
|
||||
print(f" {font_name}Intervals,")
|
||||
print(f" {len(intervals)},")
|
||||
print(f" {1 if compress else 0},")
|
||||
print(f" {norm_ceil(face.size.height)},")
|
||||
print(f" {norm_ceil(face.size.ascender)},")
|
||||
print(f" {norm_floor(face.size.descender)},")
|
||||
print("};")
|
||||
@@ -0,0 +1 @@
|
||||
freetype-py==2.5.1
|
||||
@@ -0,0 +1,132 @@
|
||||
#pragma once
|
||||
#include <EpdFont.h>
|
||||
#include <HardwareSerial.h>
|
||||
#include <Utf8.h>
|
||||
#include <miniz.h>
|
||||
|
||||
inline int min(const int a, const int b) { return a < b ? a : b; }
|
||||
inline int max(const int a, const int b) { return a > b ? a : b; }
|
||||
|
||||
static tinfl_decompressor decomp;
|
||||
|
||||
template <typename Renderable>
|
||||
class EpdFontRenderer {
|
||||
Renderable* renderer;
|
||||
void renderChar(uint32_t cp, int* x, const int* y, uint16_t color);
|
||||
|
||||
public:
|
||||
const EpdFont* font;
|
||||
explicit EpdFontRenderer(const EpdFont* font, Renderable* renderer);
|
||||
~EpdFontRenderer() = default;
|
||||
void renderString(const char* string, int* x, int* y, uint16_t color);
|
||||
};
|
||||
|
||||
inline int uncompress(uint8_t* dest, size_t uncompressedSize, const uint8_t* source, size_t sourceSize) {
|
||||
if (uncompressedSize == 0 || dest == nullptr || sourceSize == 0 || source == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
tinfl_init(&decomp);
|
||||
|
||||
// we know everything will fit into the buffer.
|
||||
const tinfl_status decomp_status =
|
||||
tinfl_decompress(&decomp, source, &sourceSize, dest, dest, &uncompressedSize,
|
||||
TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
|
||||
if (decomp_status != TINFL_STATUS_DONE) {
|
||||
return decomp_status;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <typename Renderable>
|
||||
EpdFontRenderer<Renderable>::EpdFontRenderer(const EpdFont* font, Renderable* renderer) {
|
||||
this->font = font;
|
||||
this->renderer = renderer;
|
||||
}
|
||||
|
||||
template <typename Renderable>
|
||||
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color) {
|
||||
// cannot draw a NULL / empty string
|
||||
if (string == nullptr || *string == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
// no printable characters
|
||||
if (!font->hasPrintableChars(string)) {
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t cp;
|
||||
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||
renderChar(cp, x, y, color);
|
||||
}
|
||||
|
||||
*y += font->data->advanceY;
|
||||
}
|
||||
|
||||
template <typename Renderable>
|
||||
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, uint16_t color) {
|
||||
const EpdGlyph* glyph = font->getGlyph(cp);
|
||||
if (!glyph) {
|
||||
// TODO: Replace with fallback glyph property?
|
||||
glyph = font->getGlyph('?');
|
||||
}
|
||||
|
||||
// no glyph?
|
||||
if (!glyph) {
|
||||
Serial.printf("No glyph for codepoint %d\n", cp);
|
||||
return;
|
||||
}
|
||||
|
||||
const uint32_t offset = glyph->dataOffset;
|
||||
const uint8_t width = glyph->width;
|
||||
const uint8_t height = glyph->height;
|
||||
const int left = glyph->left;
|
||||
|
||||
const int byteWidth = width / 2 + width % 2;
|
||||
const unsigned long bitmapSize = byteWidth * height;
|
||||
const uint8_t* bitmap = nullptr;
|
||||
|
||||
if (font->data->compressed) {
|
||||
auto* tmpBitmap = static_cast<uint8_t*>(malloc(bitmapSize));
|
||||
if (tmpBitmap == nullptr && bitmapSize) {
|
||||
// ESP_LOGE("font", "malloc failed.");
|
||||
return;
|
||||
}
|
||||
|
||||
uncompress(tmpBitmap, bitmapSize, &font->data->bitmap[offset], glyph->compressedSize);
|
||||
bitmap = tmpBitmap;
|
||||
} else {
|
||||
bitmap = &font->data->bitmap[offset];
|
||||
}
|
||||
|
||||
if (bitmap != nullptr) {
|
||||
for (int localY = 0; localY < height; localY++) {
|
||||
int yy = *y - glyph->top + localY;
|
||||
const int startPos = *x + left;
|
||||
bool byteComplete = startPos % 2;
|
||||
int localX = max(0, -startPos);
|
||||
const int maxX = startPos + width;
|
||||
|
||||
for (int xx = startPos; xx < maxX; xx++) {
|
||||
uint8_t bm = bitmap[localY * byteWidth + localX / 2];
|
||||
if ((localX & 1) == 0) {
|
||||
bm = bm & 0xF;
|
||||
} else {
|
||||
bm = bm >> 4;
|
||||
}
|
||||
|
||||
if (bm) {
|
||||
renderer->drawPixel(xx, yy, color);
|
||||
}
|
||||
byteComplete = !byteComplete;
|
||||
localX++;
|
||||
}
|
||||
}
|
||||
|
||||
if (font->data->compressed) {
|
||||
free(const_cast<uint8_t*>(bitmap));
|
||||
}
|
||||
}
|
||||
|
||||
*x += glyph->advanceX;
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
#include "EpdRenderer.h"
|
||||
|
||||
#include "builtinFonts/babyblue.h"
|
||||
#include "builtinFonts/bookerly.h"
|
||||
#include "builtinFonts/bookerly_bold.h"
|
||||
#include "builtinFonts/bookerly_bold_italic.h"
|
||||
#include "builtinFonts/bookerly_italic.h"
|
||||
|
||||
EpdRenderer::EpdRenderer(XteinkDisplay* display) {
|
||||
this->display = display;
|
||||
this->regularFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly), display);
|
||||
this->boldFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly_bold), display);
|
||||
this->italicFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly_italic), display);
|
||||
this->bold_italicFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly_bold_italic), display);
|
||||
this->smallFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&babyblue), display);
|
||||
|
||||
this->marginTop = 11;
|
||||
this->marginBottom = 30;
|
||||
this->marginLeft = 10;
|
||||
this->marginRight = 10;
|
||||
this->lineCompression = 0.95f;
|
||||
}
|
||||
|
||||
EpdFontRenderer<XteinkDisplay>* EpdRenderer::getFontRenderer(const bool bold, const bool italic) const {
|
||||
if (bold && italic) {
|
||||
return bold_italicFont;
|
||||
}
|
||||
if (bold) {
|
||||
return boldFont;
|
||||
}
|
||||
if (italic) {
|
||||
return italicFont;
|
||||
}
|
||||
return regularFont;
|
||||
}
|
||||
|
||||
int EpdRenderer::getTextWidth(const char* text, const bool bold, const bool italic) const {
|
||||
int w = 0, h = 0;
|
||||
|
||||
getFontRenderer(bold, italic)->font->getTextDimensions(text, &w, &h);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
int EpdRenderer::getSmallTextWidth(const char* text) const {
|
||||
int w = 0, h = 0;
|
||||
|
||||
smallFont->font->getTextDimensions(text, &w, &h);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
void EpdRenderer::drawText(const int x, const int y, const char* text, const bool bold, const bool italic,
|
||||
const uint16_t color) const {
|
||||
int ypos = y + getLineHeight() + marginTop;
|
||||
int xpos = x + marginLeft;
|
||||
getFontRenderer(bold, italic)->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawSmallText(const int x, const int y, const char* text) const {
|
||||
int ypos = y + smallFont->font->data->advanceY + marginTop;
|
||||
int xpos = x + marginLeft;
|
||||
smallFont->renderString(text, &xpos, &ypos, GxEPD_BLACK);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
|
||||
const bool bold, const bool italic) const {
|
||||
const size_t length = text.length();
|
||||
// fit the text into the box
|
||||
int start = 0;
|
||||
int end = 1;
|
||||
int ypos = 0;
|
||||
while (true) {
|
||||
if (end >= length) {
|
||||
drawText(x, y + ypos, text.substr(start, length - start).c_str(), bold, italic);
|
||||
break;
|
||||
}
|
||||
|
||||
if (ypos + getLineHeight() >= height) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (text[end - 1] == '\n') {
|
||||
drawText(x, y + ypos, text.substr(start, end - start).c_str(), bold, italic);
|
||||
ypos += getLineHeight();
|
||||
start = end;
|
||||
end = start + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (getTextWidth(text.substr(start, end - start).c_str(), bold, italic) > width) {
|
||||
drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), bold, italic);
|
||||
ypos += getLineHeight();
|
||||
start = end - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
end++;
|
||||
}
|
||||
}
|
||||
|
||||
void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, uint16_t color) const {
|
||||
display->drawLine(x1 + marginLeft, y1 + marginTop, x2 + marginLeft, y2 + marginTop,
|
||||
color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
|
||||
display->drawRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::fillRect(const int x, const int y, const int width, const int height,
|
||||
const uint16_t color = 0) const {
|
||||
display->fillRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::clearScreen(const bool black) const {
|
||||
Serial.println("Clearing screen");
|
||||
display->fillScreen(black ? GxEPD_BLACK : GxEPD_WHITE);
|
||||
}
|
||||
|
||||
void EpdRenderer::flushDisplay() const { display->display(true); }
|
||||
|
||||
void EpdRenderer::flushArea(int x, int y, int width, int height) const {
|
||||
// TODO: Fix
|
||||
display->display(true);
|
||||
}
|
||||
|
||||
int EpdRenderer::getPageWidth() const { return display->width() - marginLeft - marginRight; }
|
||||
|
||||
int EpdRenderer::getPageHeight() const { return display->height() - marginTop - marginBottom; }
|
||||
|
||||
int EpdRenderer::getSpaceWidth() const { return regularFont->font->getGlyph(' ')->advanceX; }
|
||||
|
||||
int EpdRenderer::getLineHeight() const { return regularFont->font->data->advanceY * lineCompression; }
|
||||
|
||||
// deep sleep helper - persist any state to disk that may be needed on wake
|
||||
bool EpdRenderer::dehydrate() {
|
||||
// TODO: Implement
|
||||
return false;
|
||||
};
|
||||
|
||||
// deep sleep helper - retrieve any state from disk after wake
|
||||
bool EpdRenderer::hydrate() {
|
||||
// TODO: Implement
|
||||
return false;
|
||||
};
|
||||
|
||||
// really really clear the screen
|
||||
void EpdRenderer::reset() {
|
||||
// TODO: Implement
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include <GxEPD2_BW.h>
|
||||
|
||||
#include <EpdFontRenderer.hpp>
|
||||
|
||||
#define XteinkDisplay GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT>
|
||||
|
||||
class EpdRenderer {
|
||||
XteinkDisplay* display;
|
||||
EpdFontRenderer<XteinkDisplay>* regularFont;
|
||||
EpdFontRenderer<XteinkDisplay>* boldFont;
|
||||
EpdFontRenderer<XteinkDisplay>* italicFont;
|
||||
EpdFontRenderer<XteinkDisplay>* bold_italicFont;
|
||||
EpdFontRenderer<XteinkDisplay>* smallFont;
|
||||
int marginTop;
|
||||
int marginBottom;
|
||||
int marginLeft;
|
||||
int marginRight;
|
||||
float lineCompression;
|
||||
EpdFontRenderer<XteinkDisplay>* getFontRenderer(bool bold, bool italic) const;
|
||||
|
||||
public:
|
||||
explicit EpdRenderer(XteinkDisplay* display);
|
||||
~EpdRenderer() = default;
|
||||
int getTextWidth(const char* text, bool bold = false, bool italic = false) const;
|
||||
int getSmallTextWidth(const char* text) const;
|
||||
void drawText(int x, int y, const char* text, bool bold = false, bool italic = false, uint16_t color = 1) const;
|
||||
void drawSmallText(int x, int y, const char* text) const;
|
||||
void drawTextBox(int x, int y, const std::string& text, int width, int height, bool bold = false,
|
||||
bool italic = false) const;
|
||||
void drawLine(int x1, int y1, int x2, int y2, uint16_t color) const;
|
||||
void drawRect(int x, int y, int width, int height, uint16_t color) const;
|
||||
void fillRect(int x, int y, int width, int height, uint16_t color) const;
|
||||
void clearScreen(bool black = false) const;
|
||||
void flushDisplay() const;
|
||||
void flushArea(int x, int y, int width, int height) const;
|
||||
|
||||
int getPageWidth() const;
|
||||
int getPageHeight() const;
|
||||
int getSpaceWidth() const;
|
||||
int getLineHeight() const;
|
||||
// set margins
|
||||
void setMarginTop(const int newMarginTop) { this->marginTop = newMarginTop; }
|
||||
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }
|
||||
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
|
||||
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
|
||||
// deep sleep helper - persist any state to disk that may be needed on wake
|
||||
bool dehydrate();
|
||||
// deep sleep helper - retrieve any state from disk after wake
|
||||
bool hydrate();
|
||||
// really really clear the screen
|
||||
void reset();
|
||||
|
||||
uint8_t temperature = 20;
|
||||
};
|
||||
@@ -0,0 +1,383 @@
|
||||
#include "Epub.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <SD.h>
|
||||
#include <ZipFile.h>
|
||||
#include <tinyxml2.h>
|
||||
|
||||
#include <map>
|
||||
|
||||
bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
|
||||
// open up the meta data to find where the content.opf file lives
|
||||
size_t s;
|
||||
const auto metaInfo = zip.readTextFileToMemory("META-INF/container.xml", &s);
|
||||
if (!metaInfo) {
|
||||
Serial.println("Could not find META-INF/container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
// parse the meta data
|
||||
tinyxml2::XMLDocument metaDataDoc;
|
||||
const auto result = metaDataDoc.Parse(metaInfo);
|
||||
free(metaInfo);
|
||||
|
||||
if (result != tinyxml2::XML_SUCCESS) {
|
||||
Serial.printf("Could not parse META-INF/container.xml. Error: %d\n", result);
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto container = metaDataDoc.FirstChildElement("container");
|
||||
if (!container) {
|
||||
Serial.println("Could not find container element in META-INF/container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto rootfiles = container->FirstChildElement("rootfiles");
|
||||
if (!rootfiles) {
|
||||
Serial.println("Could not find rootfiles element in META-INF/container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
// find the root file that has the media-type="application/oebps-package+xml"
|
||||
auto rootfile = rootfiles->FirstChildElement("rootfile");
|
||||
while (rootfile) {
|
||||
const char* mediaType = rootfile->Attribute("media-type");
|
||||
if (mediaType && strcmp(mediaType, "application/oebps-package+xml") == 0) {
|
||||
const char* full_path = rootfile->Attribute("full-path");
|
||||
if (full_path) {
|
||||
contentOpfFile = full_path;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
rootfile = rootfile->NextSiblingElement("rootfile");
|
||||
}
|
||||
|
||||
Serial.println("Could not get path to content.opf file");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||
// read in the content.opf file and parse it
|
||||
auto contents = zip.readTextFileToMemory(content_opf_file.c_str());
|
||||
|
||||
// parse the contents
|
||||
tinyxml2::XMLDocument doc;
|
||||
auto result = doc.Parse(contents);
|
||||
free(contents);
|
||||
|
||||
if (result != tinyxml2::XML_SUCCESS) {
|
||||
Serial.printf("Error parsing content.opf - %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto package = doc.FirstChildElement("package");
|
||||
if (!package) package = doc.FirstChildElement("opf:package");
|
||||
|
||||
if (!package) {
|
||||
Serial.println("Could not find package element in content.opf");
|
||||
return false;
|
||||
}
|
||||
|
||||
// get the metadata - title and cover image
|
||||
auto metadata = package->FirstChildElement("metadata");
|
||||
if (!metadata) metadata = package->FirstChildElement("opf:metadata");
|
||||
if (!metadata) {
|
||||
Serial.println("Missing metadata");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto titleEl = metadata->FirstChildElement("dc:title");
|
||||
if (!titleEl) {
|
||||
Serial.println("Missing title");
|
||||
return false;
|
||||
}
|
||||
this->title = titleEl->GetText();
|
||||
|
||||
auto cover = metadata->FirstChildElement("meta");
|
||||
if (!cover) cover = metadata->FirstChildElement("opf:meta");
|
||||
while (cover && cover->Attribute("name") && strcmp(cover->Attribute("name"), "cover") != 0) {
|
||||
cover = cover->NextSiblingElement("meta");
|
||||
}
|
||||
if (!cover) {
|
||||
Serial.println("Missing cover");
|
||||
}
|
||||
auto coverItem = cover ? cover->Attribute("content") : nullptr;
|
||||
|
||||
// read the manifest and spine
|
||||
// the manifest gives us the names of the files
|
||||
// the spine gives us the order of the files
|
||||
// we can then read the files in the order they are in the spine
|
||||
auto manifest = package->FirstChildElement("manifest");
|
||||
if (!manifest) manifest = package->FirstChildElement("opf:manifest");
|
||||
if (!manifest) {
|
||||
Serial.println("Missing manifest");
|
||||
return false;
|
||||
}
|
||||
|
||||
// create a mapping from id to file name
|
||||
auto item = manifest->FirstChildElement("item");
|
||||
if (!item) item = manifest->FirstChildElement("opf:item");
|
||||
std::map<std::string, std::string> items;
|
||||
|
||||
while (item) {
|
||||
std::string itemId = item->Attribute("id");
|
||||
std::string href = contentBasePath + item->Attribute("href");
|
||||
|
||||
// grab the cover image
|
||||
if (coverItem && itemId == coverItem) {
|
||||
coverImageItem = href;
|
||||
}
|
||||
|
||||
// grab the ncx file
|
||||
if (itemId == "ncx" || itemId == "ncxtoc") {
|
||||
tocNcxItem = href;
|
||||
}
|
||||
|
||||
items[itemId] = href;
|
||||
auto nextItem = item->NextSiblingElement("item");
|
||||
if (!nextItem) nextItem = item->NextSiblingElement("opf:item");
|
||||
item = nextItem;
|
||||
}
|
||||
|
||||
// find the spine
|
||||
auto spineEl = package->FirstChildElement("spine");
|
||||
if (!spineEl) spineEl = package->FirstChildElement("opf:spine");
|
||||
if (!spineEl) {
|
||||
Serial.println("Missing spine");
|
||||
return false;
|
||||
}
|
||||
|
||||
// read the spine
|
||||
auto itemref = spineEl->FirstChildElement("itemref");
|
||||
if (!itemref) itemref = spineEl->FirstChildElement("opf:itemref");
|
||||
while (itemref) {
|
||||
auto id = itemref->Attribute("idref");
|
||||
if (items.find(id) != items.end()) {
|
||||
spine.emplace_back(id, items[id]);
|
||||
}
|
||||
auto nextItemRef = itemref->NextSiblingElement("itemref");
|
||||
if (!nextItemRef) nextItemRef = itemref->NextSiblingElement("opf:itemref");
|
||||
itemref = nextItemRef;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseTocNcxFile(ZipFile& zip) {
|
||||
// the ncx file should have been specified in the content.opf file
|
||||
if (tocNcxItem.empty()) {
|
||||
Serial.println("No ncx file specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ncxData = zip.readTextFileToMemory(tocNcxItem.c_str());
|
||||
if (!ncxData) {
|
||||
Serial.printf("Could not find %s\n", tocNcxItem.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse the Toc contents
|
||||
tinyxml2::XMLDocument doc;
|
||||
auto result = doc.Parse(ncxData);
|
||||
free(ncxData);
|
||||
|
||||
if (result != tinyxml2::XML_SUCCESS) {
|
||||
Serial.printf("Error parsing toc %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto ncx = doc.FirstChildElement("ncx");
|
||||
if (!ncx) {
|
||||
Serial.println("Could not find first child ncx in toc");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto navMap = ncx->FirstChildElement("navMap");
|
||||
if (!navMap) {
|
||||
Serial.println("Could not find navMap child in ncx");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto navPoint = navMap->FirstChildElement("navPoint");
|
||||
|
||||
// Fills toc map
|
||||
while (navPoint) {
|
||||
std::string navTitle = navPoint->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value();
|
||||
auto content = navPoint->FirstChildElement("content");
|
||||
std::string href = contentBasePath + content->Attribute("src");
|
||||
// split the href on the # to get the href and the anchor
|
||||
size_t pos = href.find('#');
|
||||
std::string anchor;
|
||||
|
||||
if (pos != std::string::npos) {
|
||||
anchor = href.substr(pos + 1);
|
||||
href = href.substr(0, pos);
|
||||
}
|
||||
|
||||
toc.emplace_back(navTitle, href, anchor, 0);
|
||||
navPoint = navPoint->NextSiblingElement("navPoint");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// load in the meta data for the epub file
|
||||
bool Epub::load() {
|
||||
ZipFile zip("/sd" + filepath);
|
||||
|
||||
std::string contentOpfFile;
|
||||
if (!findContentOpfFile(zip, contentOpfFile)) {
|
||||
Serial.println("Could not open ePub");
|
||||
return false;
|
||||
}
|
||||
|
||||
contentBasePath = contentOpfFile.substr(0, contentOpfFile.find_last_of('/') + 1);
|
||||
|
||||
if (!parseContentOpf(zip, contentOpfFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!parseTocNcxFile(zip)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Epub::clearCache() const { SD.rmdir(cachePath.c_str()); }
|
||||
|
||||
void Epub::setupCacheDir() const {
|
||||
if (SD.exists(cachePath.c_str())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Loop over each segment of the cache path and create directories as needed
|
||||
for (size_t i = 1; i < cachePath.length(); i++) {
|
||||
if (cachePath[i] == '/') {
|
||||
SD.mkdir(cachePath.substr(0, i).c_str());
|
||||
}
|
||||
}
|
||||
SD.mkdir(cachePath.c_str());
|
||||
}
|
||||
|
||||
const std::string& Epub::getCachePath() const { return cachePath; }
|
||||
|
||||
const std::string& Epub::getPath() const { return filepath; }
|
||||
|
||||
const std::string& Epub::getTitle() const { return title; }
|
||||
|
||||
const std::string& Epub::getCoverImageItem() const { return coverImageItem; }
|
||||
|
||||
std::string normalisePath(const std::string& path) {
|
||||
std::vector<std::string> components;
|
||||
std::string component;
|
||||
|
||||
for (const auto c : path) {
|
||||
if (c == '/') {
|
||||
if (!component.empty()) {
|
||||
if (component == "..") {
|
||||
if (!components.empty()) {
|
||||
components.pop_back();
|
||||
}
|
||||
} else {
|
||||
components.push_back(component);
|
||||
}
|
||||
component.clear();
|
||||
}
|
||||
} else {
|
||||
component += c;
|
||||
}
|
||||
}
|
||||
|
||||
if (!component.empty()) {
|
||||
components.push_back(component);
|
||||
}
|
||||
|
||||
std::string result;
|
||||
for (const auto& c : components) {
|
||||
if (!result.empty()) {
|
||||
result += "/";
|
||||
}
|
||||
result += c;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t* Epub::getItemContents(const std::string& itemHref, size_t* size) const {
|
||||
const ZipFile zip("/sd" + filepath);
|
||||
const std::string path = normalisePath(itemHref);
|
||||
|
||||
const auto content = zip.readFileToMemory(path.c_str(), size);
|
||||
if (!content) {
|
||||
Serial.printf("Failed to read item %s\n", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
char* Epub::getTextItemContents(const std::string& itemHref, size_t* size) const {
|
||||
const ZipFile zip("/sd" + filepath);
|
||||
const std::string path = normalisePath(itemHref);
|
||||
|
||||
const auto content = zip.readTextFileToMemory(path.c_str(), size);
|
||||
if (!content) {
|
||||
Serial.printf("Failed to read item %s\n", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
int Epub::getSpineItemsCount() const { return spine.size(); }
|
||||
|
||||
std::string& Epub::getSpineItem(const int spineIndex) {
|
||||
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
||||
Serial.printf("getSpineItem index:%d is out of range\n", spineIndex);
|
||||
return spine.at(0).second;
|
||||
}
|
||||
|
||||
return spine.at(spineIndex).second;
|
||||
}
|
||||
|
||||
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
|
||||
if (tocTndex < 0 || tocTndex >= toc.size()) {
|
||||
Serial.printf("getTocItem index:%d is out of range\n", tocTndex);
|
||||
return toc.at(0);
|
||||
}
|
||||
|
||||
return toc.at(tocTndex);
|
||||
}
|
||||
|
||||
int Epub::getTocItemsCount() const { return toc.size(); }
|
||||
|
||||
// work out the section index for a toc index
|
||||
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||
// the toc entry should have an href that matches the spine item
|
||||
// so we can find the spine index by looking for the href
|
||||
for (int i = 0; i < spine.size(); i++) {
|
||||
if (spine[i].second == toc[tocIndex].href) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println("Section not found");
|
||||
// not found - default to the start of the book
|
||||
return 0;
|
||||
}
|
||||
|
||||
int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
||||
// the toc entry should have an href that matches the spine item
|
||||
// so we can find the toc index by looking for the href
|
||||
Serial.printf("Looking for %s\n", spine[spineIndex].second.c_str());
|
||||
for (int i = 0; i < toc.size(); i++) {
|
||||
Serial.printf("Looking at %s\n", toc[i].href.c_str());
|
||||
if (toc[i].href == spine[spineIndex].second) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
Serial.println("TOC item not found");
|
||||
// not found - default to first item
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
class ZipFile;
|
||||
|
||||
class EpubTocEntry {
|
||||
public:
|
||||
std::string title;
|
||||
std::string href;
|
||||
std::string anchor;
|
||||
int level;
|
||||
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
|
||||
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
|
||||
};
|
||||
|
||||
class Epub {
|
||||
// the title read from the EPUB meta data
|
||||
std::string title;
|
||||
// the cover image
|
||||
std::string coverImageItem;
|
||||
// the ncx file
|
||||
std::string tocNcxItem;
|
||||
// where is the EPUBfile?
|
||||
std::string filepath;
|
||||
// the spine of the EPUB file
|
||||
std::vector<std::pair<std::string, std::string>> spine;
|
||||
// the toc of the EPUB file
|
||||
std::vector<EpubTocEntry> toc;
|
||||
// the base path for items in the EPUB file
|
||||
std::string contentBasePath;
|
||||
// Uniq cache key based on filepath
|
||||
std::string cachePath;
|
||||
|
||||
// find the path for the content.opf file
|
||||
static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile);
|
||||
bool parseContentOpf(ZipFile& zip, std::string& content_opf_file);
|
||||
bool parseTocNcxFile(ZipFile& zip);
|
||||
|
||||
public:
|
||||
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||
// create a cache key based on the filepath
|
||||
|
||||
cachePath = cacheDir + "/epub_" + std::to_string(std::hash<std::string>{}(this->filepath));
|
||||
}
|
||||
~Epub() = default;
|
||||
std::string& getBasePath() { return contentBasePath; }
|
||||
bool load();
|
||||
|
||||
void clearCache() const;
|
||||
|
||||
void setupCacheDir() const;
|
||||
|
||||
const std::string& getCachePath() const;
|
||||
const std::string& getPath() const;
|
||||
const std::string& getTitle() const;
|
||||
const std::string& getCoverImageItem() const;
|
||||
uint8_t* getItemContents(const std::string& itemHref, size_t* size = nullptr) const;
|
||||
char* getTextItemContents(const std::string& itemHref, size_t* size = nullptr) const;
|
||||
|
||||
std::string& getSpineItem(int spineIndex);
|
||||
int getSpineItemsCount() const;
|
||||
|
||||
EpubTocEntry& getTocItem(int tocTndex);
|
||||
int getTocItemsCount() const;
|
||||
// work out the section index for a toc index
|
||||
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||
|
||||
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||
};
|
||||
@@ -0,0 +1,181 @@
|
||||
#include "EpubHtmlParser.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <HardwareSerial.h>
|
||||
|
||||
#include "Page.h"
|
||||
#include "htmlEntities.h"
|
||||
|
||||
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||
|
||||
const char* BLOCK_TAGS[] = {"p", "li", "div", "br"};
|
||||
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||
|
||||
const char* BOLD_TAGS[] = {"b"};
|
||||
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
||||
|
||||
const char* ITALIC_TAGS[] = {"i"};
|
||||
constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
|
||||
|
||||
const char* IMAGE_TAGS[] = {"img"};
|
||||
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||
|
||||
const char* SKIP_TAGS[] = {"head", "table"};
|
||||
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
|
||||
|
||||
// given the start and end of a tag, check to see if it matches a known tag
|
||||
bool matches(const char* tag_name, const char* possible_tags[], const int possible_tag_count) {
|
||||
for (int i = 0; i < possible_tag_count; i++) {
|
||||
if (strcmp(tag_name, possible_tags[i]) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// start a new text block if needed
|
||||
void EpubHtmlParser::startNewTextBlock(const BLOCK_STYLE style) {
|
||||
if (currentTextBlock) {
|
||||
// already have a text block running and it is empty - just reuse it
|
||||
if (currentTextBlock->isEmpty()) {
|
||||
currentTextBlock->set_style(style);
|
||||
return;
|
||||
}
|
||||
|
||||
currentTextBlock->finish();
|
||||
makePages();
|
||||
delete currentTextBlock;
|
||||
}
|
||||
currentTextBlock = new TextBlock(style);
|
||||
}
|
||||
|
||||
bool EpubHtmlParser::VisitEnter(const tinyxml2::XMLElement& element, const tinyxml2::XMLAttribute* firstAttribute) {
|
||||
const char* tag_name = element.Name();
|
||||
if (matches(tag_name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||
const char* src = element.Attribute("src");
|
||||
if (src) {
|
||||
// don't leave an empty text block in the list
|
||||
// const BLOCK_STYLE style = currentTextBlock->get_style();
|
||||
if (currentTextBlock->isEmpty()) {
|
||||
delete currentTextBlock;
|
||||
currentTextBlock = nullptr;
|
||||
}
|
||||
// TODO: Fix this
|
||||
// blocks.push_back(new ImageBlock(m_base_path + src));
|
||||
// start a new text block - with the same style as before
|
||||
// startNewTextBlock(style);
|
||||
} else {
|
||||
// ESP_LOGE(TAG, "Could not find src attribute");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matches(tag_name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Serial.printf("Text: %s\n", element.GetText());
|
||||
|
||||
if (matches(tag_name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||
insideBoldTag = true;
|
||||
startNewTextBlock(CENTER_ALIGN);
|
||||
} else if (matches(tag_name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||
if (strcmp(tag_name, "br") == 0) {
|
||||
startNewTextBlock(currentTextBlock->get_style());
|
||||
} else {
|
||||
startNewTextBlock(JUSTIFIED);
|
||||
}
|
||||
} else if (matches(tag_name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||
insideBoldTag = true;
|
||||
} else if (matches(tag_name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||
insideItalicTag = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/// Visit a text node.
|
||||
bool EpubHtmlParser::Visit(const tinyxml2::XMLText& text) {
|
||||
const char* content = text.Value();
|
||||
currentTextBlock->addSpan(replaceHtmlEntities(content), insideBoldTag, insideItalicTag);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EpubHtmlParser::VisitExit(const tinyxml2::XMLElement& element) {
|
||||
const char* tag_name = element.Name();
|
||||
if (matches(tag_name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||
insideBoldTag = false;
|
||||
} else if (matches(tag_name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||
// nothing to do
|
||||
} else if (matches(tag_name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||
insideBoldTag = false;
|
||||
} else if (matches(tag_name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||
insideItalicTag = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EpubHtmlParser::parseAndBuildPages() {
|
||||
startNewTextBlock(JUSTIFIED);
|
||||
tinyxml2::XMLDocument doc(false, tinyxml2::COLLAPSE_WHITESPACE);
|
||||
|
||||
const tinyxml2::XMLError result = doc.LoadFile(filepath);
|
||||
if (result != tinyxml2::XML_SUCCESS) {
|
||||
Serial.printf("Failed to load file, Error: %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
doc.Accept(this);
|
||||
if (currentTextBlock) {
|
||||
makePages();
|
||||
completePageFn(currentPage);
|
||||
currentPage = nullptr;
|
||||
delete currentTextBlock;
|
||||
currentTextBlock = nullptr;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void EpubHtmlParser::makePages() {
|
||||
if (!currentTextBlock) {
|
||||
Serial.println("!! No text block to make pages for !!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentPage) {
|
||||
currentPage = new Page();
|
||||
}
|
||||
|
||||
const int lineHeight = renderer->getLineHeight();
|
||||
const int pageHeight = renderer->getPageHeight();
|
||||
|
||||
// Long running task, make sure to let other things happen
|
||||
vTaskDelay(1);
|
||||
|
||||
if (currentTextBlock->getType() == TEXT_BLOCK) {
|
||||
const auto lines = currentTextBlock->splitIntoLines(renderer);
|
||||
|
||||
for (const auto line : lines) {
|
||||
if (currentPage->nextY + lineHeight > pageHeight) {
|
||||
completePageFn(currentPage);
|
||||
currentPage = new Page();
|
||||
}
|
||||
|
||||
currentPage->elements.push_back(new PageLine(line, currentPage->nextY));
|
||||
currentPage->nextY += lineHeight;
|
||||
}
|
||||
// TODO: Fix spacing between paras
|
||||
// add some extra line between blocks
|
||||
currentPage->nextY += lineHeight / 2;
|
||||
}
|
||||
// TODO: Image block support
|
||||
// if (block->getType() == BlockType::IMAGE_BLOCK) {
|
||||
// ImageBlock *imageBlock = (ImageBlock *)block;
|
||||
// if (y + imageBlock->height > page_height) {
|
||||
// pages.push_back(new Page());
|
||||
// y = 0;
|
||||
// }
|
||||
// pages.back()->elements.push_back(new PageImage(imageBlock, y));
|
||||
// y += imageBlock->height;
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
#include <tinyxml2.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
class Page;
|
||||
class EpdRenderer;
|
||||
|
||||
class EpubHtmlParser final : public tinyxml2::XMLVisitor {
|
||||
const char* filepath;
|
||||
EpdRenderer* renderer;
|
||||
std::function<void(Page*)> completePageFn;
|
||||
|
||||
bool insideBoldTag = false;
|
||||
bool insideItalicTag = false;
|
||||
TextBlock* currentTextBlock = nullptr;
|
||||
Page* currentPage = nullptr;
|
||||
|
||||
void startNewTextBlock(BLOCK_STYLE style);
|
||||
void makePages();
|
||||
|
||||
// xml parser callbacks
|
||||
bool VisitEnter(const tinyxml2::XMLElement& element, const tinyxml2::XMLAttribute* firstAttribute) override;
|
||||
bool Visit(const tinyxml2::XMLText& text) override;
|
||||
bool VisitExit(const tinyxml2::XMLElement& element) override;
|
||||
// xml parser callbacks
|
||||
public:
|
||||
explicit EpubHtmlParser(const char* filepath, EpdRenderer* renderer, const std::function<void(Page*)>& completePageFn)
|
||||
: filepath(filepath), renderer(renderer), completePageFn(completePageFn) {}
|
||||
~EpubHtmlParser() override = default;
|
||||
bool parseAndBuildPages();
|
||||
};
|
||||
@@ -0,0 +1,65 @@
|
||||
#include "Page.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
void PageLine::render(EpdRenderer* renderer) { block->render(renderer, 0, yPos); }
|
||||
|
||||
void PageLine::serialize(std::ostream& os) {
|
||||
serialization::writePod(os, yPos);
|
||||
|
||||
// serialize TextBlock pointed to by PageLine
|
||||
block->serialize(os);
|
||||
}
|
||||
|
||||
PageLine* PageLine::deserialize(std::istream& is) {
|
||||
int32_t yPos;
|
||||
serialization::readPod(is, yPos);
|
||||
|
||||
const auto tb = TextBlock::deserialize(is);
|
||||
return new PageLine(tb, yPos);
|
||||
}
|
||||
|
||||
void Page::render(EpdRenderer* renderer) const {
|
||||
const auto start = millis();
|
||||
for (const auto element : elements) {
|
||||
element->render(renderer);
|
||||
}
|
||||
Serial.printf("Rendered page elements (%u) in %dms\n", elements.size(), millis() - start);
|
||||
}
|
||||
|
||||
void Page::serialize(std::ostream& os) const {
|
||||
serialization::writePod(os, nextY);
|
||||
|
||||
const uint32_t count = elements.size();
|
||||
serialization::writePod(os, count);
|
||||
|
||||
for (auto* el : elements) {
|
||||
// Only PageLine exists currently
|
||||
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
|
||||
static_cast<PageLine*>(el)->serialize(os);
|
||||
}
|
||||
}
|
||||
|
||||
Page* Page::deserialize(std::istream& is) {
|
||||
auto* page = new Page();
|
||||
|
||||
serialization::readPod(is, page->nextY);
|
||||
|
||||
uint32_t count;
|
||||
serialization::readPod(is, count);
|
||||
|
||||
for (uint32_t i = 0; i < count; i++) {
|
||||
uint8_t tag;
|
||||
serialization::readPod(is, tag);
|
||||
|
||||
if (tag == TAG_PageLine) {
|
||||
auto* pl = PageLine::deserialize(is);
|
||||
page->elements.push_back(pl);
|
||||
} else {
|
||||
throw std::runtime_error("Unknown PageElement tag");
|
||||
}
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
enum PageElementTag : uint8_t {
|
||||
TAG_PageLine = 1,
|
||||
};
|
||||
|
||||
// represents something that has been added to a page
|
||||
class PageElement {
|
||||
public:
|
||||
int yPos;
|
||||
explicit PageElement(const int yPos) : yPos(yPos) {}
|
||||
virtual ~PageElement() = default;
|
||||
virtual void render(EpdRenderer* renderer) = 0;
|
||||
virtual void serialize(std::ostream& os) = 0;
|
||||
};
|
||||
|
||||
// a line from a block element
|
||||
class PageLine final : public PageElement {
|
||||
const TextBlock* block;
|
||||
|
||||
public:
|
||||
PageLine(const TextBlock* block, const int yPos) : PageElement(yPos), block(block) {}
|
||||
~PageLine() override { delete block; }
|
||||
void render(EpdRenderer* renderer) override;
|
||||
void serialize(std::ostream& os) override;
|
||||
static PageLine* deserialize(std::istream& is);
|
||||
};
|
||||
|
||||
class Page {
|
||||
public:
|
||||
int nextY = 0;
|
||||
// the list of block index and line numbers on this page
|
||||
std::vector<PageElement*> elements;
|
||||
void render(EpdRenderer* renderer) const;
|
||||
~Page() {
|
||||
for (const auto element : elements) {
|
||||
delete element;
|
||||
}
|
||||
}
|
||||
void serialize(std::ostream& os) const;
|
||||
static Page* deserialize(std::istream& is);
|
||||
};
|
||||
@@ -0,0 +1,117 @@
|
||||
#include "Section.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <SD.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include "EpubHtmlParser.h"
|
||||
#include "Page.h"
|
||||
|
||||
void Section::onPageComplete(const Page* page) {
|
||||
Serial.printf("Page %d complete\n", pageCount);
|
||||
|
||||
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
||||
// TODO can this be removed?
|
||||
SD.open(filePath.c_str(), FILE_WRITE).close();
|
||||
|
||||
std::ofstream outputFile("/sd" + filePath);
|
||||
page->serialize(outputFile);
|
||||
outputFile.close();
|
||||
|
||||
pageCount++;
|
||||
delete page;
|
||||
}
|
||||
|
||||
bool Section::hasCache() {
|
||||
if (!SD.exists(cachePath.c_str())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto sectionFilePath = cachePath + "/section.bin";
|
||||
if (!SD.exists(sectionFilePath.c_str())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
File sectionFile = SD.open(sectionFilePath.c_str(), FILE_READ);
|
||||
uint8_t pageCountBytes[2] = {0, 0};
|
||||
sectionFile.read(pageCountBytes, 2);
|
||||
sectionFile.close();
|
||||
|
||||
pageCount = pageCountBytes[0] + (pageCountBytes[1] << 8);
|
||||
Serial.printf("Loaded cache: %d pages\n", pageCount);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Section::setupCacheDir() const {
|
||||
epub->setupCacheDir();
|
||||
SD.mkdir(cachePath.c_str());
|
||||
}
|
||||
|
||||
void Section::clearCache() const { SD.rmdir(cachePath.c_str()); }
|
||||
|
||||
bool Section::persistPageDataToSD() {
|
||||
size_t size = 0;
|
||||
auto localPath = epub->getSpineItem(spineIndex);
|
||||
|
||||
const auto html = epub->getItemContents(epub->getSpineItem(spineIndex), &size);
|
||||
if (!html) {
|
||||
Serial.println("Failed to read item contents");
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Would love to stream this through an XML visitor
|
||||
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||
File f = SD.open(tmpHtmlPath.c_str(), FILE_WRITE);
|
||||
const auto written = f.write(html, size);
|
||||
f.close();
|
||||
free(html);
|
||||
|
||||
Serial.printf("Wrote %d bytes to %s\n", written, tmpHtmlPath.c_str());
|
||||
|
||||
if (size != written) {
|
||||
Serial.println("Failed to inflate section contents to SD");
|
||||
SD.remove(tmpHtmlPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
|
||||
auto visitor =
|
||||
EpubHtmlParser(sdTmpHtmlPath.c_str(), renderer, [this](const Page* page) { this->onPageComplete(page); });
|
||||
|
||||
// TODO: Come back and see if mem used here can be lowered?
|
||||
const bool success = visitor.parseAndBuildPages();
|
||||
SD.remove(tmpHtmlPath.c_str());
|
||||
if (!success) {
|
||||
Serial.println("Failed to parse and build pages");
|
||||
return false;
|
||||
}
|
||||
|
||||
File sectionFile = SD.open((cachePath + "/section.bin").c_str(), FILE_WRITE, true);
|
||||
const uint8_t pageCountBytes[2] = {static_cast<uint8_t>(pageCount & 0xFF),
|
||||
static_cast<uint8_t>((pageCount >> 8) & 0xFF)};
|
||||
sectionFile.write(pageCountBytes, 2);
|
||||
sectionFile.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Section::renderPage() {
|
||||
if (0 <= currentPage && currentPage < pageCount) {
|
||||
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
||||
std::ifstream inputFile(filePath);
|
||||
const Page* p = Page::deserialize(inputFile);
|
||||
inputFile.close();
|
||||
p->render(renderer);
|
||||
delete p;
|
||||
} else if (pageCount == 0) {
|
||||
Serial.println("No pages to render");
|
||||
const int width = renderer->getTextWidth("Empty chapter", true);
|
||||
renderer->drawText((renderer->getPageWidth() - width) / 2, 300, "Empty chapter", true);
|
||||
} else {
|
||||
Serial.printf("Page out of bounds: %d (max %d)\n", currentPage, pageCount);
|
||||
const int width = renderer->getTextWidth("Out of bounds", true);
|
||||
renderer->drawText((renderer->getPageWidth() - width) / 2, 300, "Out of bounds", true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#pragma once
|
||||
#include "Epub.h"
|
||||
|
||||
class Page;
|
||||
class EpdRenderer;
|
||||
|
||||
class Section {
|
||||
Epub* epub;
|
||||
const int spineIndex;
|
||||
EpdRenderer* renderer;
|
||||
std::string cachePath;
|
||||
|
||||
void onPageComplete(const Page* page);
|
||||
|
||||
public:
|
||||
int pageCount = 0;
|
||||
int currentPage = 0;
|
||||
|
||||
explicit Section(Epub* epub, const int spineIndex, EpdRenderer* renderer)
|
||||
: epub(epub), spineIndex(spineIndex), renderer(renderer) {
|
||||
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
|
||||
}
|
||||
~Section() = default;
|
||||
bool hasCache();
|
||||
void setupCacheDir() const;
|
||||
void clearCache() const;
|
||||
bool persistPageDataToSD();
|
||||
void renderPage();
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
class EpdRenderer;
|
||||
|
||||
typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
||||
|
||||
// a block of content in the html - either a paragraph or an image
|
||||
class Block {
|
||||
public:
|
||||
virtual ~Block() = default;
|
||||
virtual void layout(EpdRenderer* renderer) = 0;
|
||||
virtual BlockType getType() = 0;
|
||||
virtual bool isEmpty() = 0;
|
||||
virtual void finish() {}
|
||||
};
|
||||
@@ -0,0 +1,235 @@
|
||||
#include "TextBlock.h"
|
||||
|
||||
#include <EpdRenderer.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
static bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n'; }
|
||||
|
||||
// move past anything that should be considered part of a work
|
||||
static int skipWord(const std::string& text, int index, const int length) {
|
||||
while (index < length && !isWhitespace(text[index])) {
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
// skip past any white space characters
|
||||
static int skipWhitespace(const std::string& html, int index, const int length) {
|
||||
while (index < length && isWhitespace(html[index])) {
|
||||
index++;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
void TextBlock::addSpan(const std::string& span, const bool is_bold, const bool is_italic) {
|
||||
// adding a span to text block
|
||||
// make a copy of the text as we'll modify it
|
||||
const int length = span.length();
|
||||
// const auto text = new char[length + 1];
|
||||
// strcpy(text, span);
|
||||
// work out where each word is in the span
|
||||
int index = 0;
|
||||
while (index < length) {
|
||||
// skip past any whitespace to the start of a word
|
||||
index = skipWhitespace(span, index, length);
|
||||
const int wordStart = index;
|
||||
// find the end of the word
|
||||
index = skipWord(span, index, length);
|
||||
const int wordLength = index - wordStart;
|
||||
if (wordLength > 0) {
|
||||
words.push_back(span.substr(wordStart, wordLength));
|
||||
wordStyles.push_back((is_bold ? BOLD_SPAN : 0) | (is_italic ? ITALIC_SPAN : 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer* renderer) {
|
||||
const int totalWordCount = words.size();
|
||||
const int pageWidth = renderer->getPageWidth();
|
||||
const int spaceWidth = renderer->getSpaceWidth();
|
||||
|
||||
words.shrink_to_fit();
|
||||
wordStyles.shrink_to_fit();
|
||||
wordXpos.reserve(totalWordCount);
|
||||
|
||||
// measure each word
|
||||
uint16_t wordWidths[totalWordCount];
|
||||
for (int i = 0; i < words.size(); i++) {
|
||||
// measure the word
|
||||
const int width = renderer->getTextWidth(words[i].c_str(), wordStyles[i] & BOLD_SPAN, wordStyles[i] & ITALIC_SPAN);
|
||||
wordWidths[i] = width;
|
||||
}
|
||||
|
||||
// now apply the dynamic programming algorithm to find the best line breaks
|
||||
// DP table in which dp[i] represents cost of line starting with word words[i]
|
||||
int dp[totalWordCount];
|
||||
|
||||
// Array in which ans[i] store index of last word in line starting with word
|
||||
// word[i]
|
||||
size_t ans[totalWordCount];
|
||||
|
||||
// If only one word is present then only one line is required. Cost of last
|
||||
// line is zero. Hence cost of this line is zero. Ending point is also n-1 as
|
||||
// single word is present
|
||||
dp[totalWordCount - 1] = 0;
|
||||
ans[totalWordCount - 1] = totalWordCount - 1;
|
||||
|
||||
// Make each word first word of line by iterating over each index in arr.
|
||||
for (int i = totalWordCount - 2; i >= 0; i--) {
|
||||
int currlen = -1;
|
||||
dp[i] = INT_MAX;
|
||||
|
||||
// Variable to store possible minimum cost of line.
|
||||
int cost;
|
||||
|
||||
// Keep on adding words in current line by iterating from starting word upto
|
||||
// last word in arr.
|
||||
for (int j = i; j < totalWordCount; j++) {
|
||||
// Update the width of the words in current line + the space between two
|
||||
// words.
|
||||
currlen += wordWidths[j] + spaceWidth;
|
||||
|
||||
// If we're bigger than the current pagewidth then we can't add more words
|
||||
if (currlen > pageWidth) break;
|
||||
|
||||
// if we've run out of words then this is last line and the cost should be
|
||||
// 0 Otherwise the cost is the sqaure of the left over space + the costs
|
||||
// of all the previous lines
|
||||
if (j == totalWordCount - 1)
|
||||
cost = 0;
|
||||
else
|
||||
cost = (pageWidth - currlen) * (pageWidth - currlen) + dp[j + 1];
|
||||
|
||||
// Check if this arrangement gives minimum cost for line starting with
|
||||
// word words[i].
|
||||
if (cost < dp[i]) {
|
||||
dp[i] = cost;
|
||||
ans[i] = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We can now iterate through the answer to find the line break positions
|
||||
std::list<uint16_t> lineBreaks;
|
||||
for (size_t i = 0; i < totalWordCount;) {
|
||||
i = ans[i] + 1;
|
||||
if (i > totalWordCount) {
|
||||
break;
|
||||
}
|
||||
lineBreaks.push_back(i);
|
||||
// Text too big, just exit
|
||||
if (lineBreaks.size() > 1000) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::list<TextBlock*> lines;
|
||||
|
||||
// With the line breaks calculated we can now position the words along the
|
||||
// line
|
||||
int startWord = 0;
|
||||
for (const auto lineBreak : lineBreaks) {
|
||||
const int lineWordCount = lineBreak - startWord;
|
||||
|
||||
int lineWordWidthSum = 0;
|
||||
for (int i = startWord; i < lineBreak; i++) {
|
||||
lineWordWidthSum += wordWidths[i];
|
||||
}
|
||||
|
||||
// Calculate spacing between words
|
||||
const uint16_t spareSpace = pageWidth - lineWordWidthSum;
|
||||
uint16_t spacing = spaceWidth;
|
||||
// evenly space words if using justified style, not the last line, and at
|
||||
// least 2 words
|
||||
if (style == JUSTIFIED && lineBreak != lineBreaks.back() && lineWordCount >= 2) {
|
||||
spacing = spareSpace / (lineWordCount - 1);
|
||||
}
|
||||
|
||||
uint16_t xpos = 0;
|
||||
if (style == RIGHT_ALIGN) {
|
||||
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||
} else if (style == CENTER_ALIGN) {
|
||||
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||
}
|
||||
|
||||
for (int i = startWord; i < lineBreak; i++) {
|
||||
wordXpos[i] = xpos;
|
||||
xpos += wordWidths[i] + spacing;
|
||||
}
|
||||
|
||||
std::vector<std::string> lineWords;
|
||||
std::vector<uint16_t> lineXPos;
|
||||
std::vector<uint8_t> lineWordStyles;
|
||||
lineWords.reserve(lineWordCount);
|
||||
lineXPos.reserve(lineWordCount);
|
||||
lineWordStyles.reserve(lineWordCount);
|
||||
|
||||
for (int i = startWord; i < lineBreak; i++) {
|
||||
lineWords.push_back(words[i]);
|
||||
lineXPos.push_back(wordXpos[i]);
|
||||
lineWordStyles.push_back(wordStyles[i]);
|
||||
}
|
||||
const auto textLine = new TextBlock(lineWords, lineXPos, lineWordStyles, style);
|
||||
lines.push_back(textLine);
|
||||
startWord = lineBreak;
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
void TextBlock::render(const EpdRenderer* renderer, const int x, const int y) const {
|
||||
for (int i = 0; i < words.size(); i++) {
|
||||
// get the style
|
||||
const uint8_t wordStyle = wordStyles[i];
|
||||
// render the word
|
||||
renderer->drawText(x + wordXpos[i], y, words[i].c_str(), wordStyle & BOLD_SPAN, wordStyle & ITALIC_SPAN);
|
||||
}
|
||||
}
|
||||
|
||||
void TextBlock::serialize(std::ostream& os) const {
|
||||
// words
|
||||
const uint32_t wc = words.size();
|
||||
serialization::writePod(os, wc);
|
||||
for (const auto& w : words) serialization::writeString(os, w);
|
||||
|
||||
// wordXpos
|
||||
const uint32_t xc = wordXpos.size();
|
||||
serialization::writePod(os, xc);
|
||||
for (auto x : wordXpos) serialization::writePod(os, x);
|
||||
|
||||
// wordStyles
|
||||
const uint32_t sc = wordStyles.size();
|
||||
serialization::writePod(os, sc);
|
||||
for (auto s : wordStyles) serialization::writePod(os, s);
|
||||
|
||||
// style
|
||||
serialization::writePod(os, style);
|
||||
}
|
||||
|
||||
TextBlock* TextBlock::deserialize(std::istream& is) {
|
||||
uint32_t wc, xc, sc;
|
||||
std::vector<std::string> words;
|
||||
std::vector<uint16_t> wordXpos;
|
||||
std::vector<uint8_t> wordStyles;
|
||||
BLOCK_STYLE style;
|
||||
|
||||
// words
|
||||
serialization::readPod(is, wc);
|
||||
words.resize(wc);
|
||||
for (auto& w : words) serialization::readString(is, w);
|
||||
|
||||
// wordXpos
|
||||
serialization::readPod(is, xc);
|
||||
wordXpos.resize(xc);
|
||||
for (auto& x : wordXpos) serialization::readPod(is, x);
|
||||
|
||||
// wordStyles
|
||||
serialization::readPod(is, sc);
|
||||
wordStyles.resize(sc);
|
||||
for (auto& s : wordStyles) serialization::readPod(is, s);
|
||||
|
||||
// style
|
||||
serialization::readPod(is, style);
|
||||
|
||||
return new TextBlock(words, wordXpos, wordStyles, style);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
#include <list>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Block.h"
|
||||
|
||||
enum SPAN_STYLE : uint8_t {
|
||||
BOLD_SPAN = 1,
|
||||
ITALIC_SPAN = 2,
|
||||
};
|
||||
|
||||
enum BLOCK_STYLE : uint8_t {
|
||||
JUSTIFIED = 0,
|
||||
LEFT_ALIGN = 1,
|
||||
CENTER_ALIGN = 2,
|
||||
RIGHT_ALIGN = 3,
|
||||
};
|
||||
|
||||
// represents a block of words in the html document
|
||||
class TextBlock final : public Block {
|
||||
// pointer to each word
|
||||
std::vector<std::string> words;
|
||||
// x position of each word
|
||||
std::vector<uint16_t> wordXpos;
|
||||
// the styles of each word
|
||||
std::vector<uint8_t> wordStyles;
|
||||
|
||||
// the style of the block - left, center, right aligned
|
||||
BLOCK_STYLE style;
|
||||
|
||||
public:
|
||||
void addSpan(const std::string& span, bool is_bold, bool is_italic);
|
||||
explicit TextBlock(const BLOCK_STYLE style) : style(style) {}
|
||||
explicit TextBlock(const std::vector<std::string>& words, const std::vector<uint16_t>& word_xpos,
|
||||
// the styles of each word
|
||||
const std::vector<uint8_t>& word_styles, const BLOCK_STYLE style)
|
||||
: words(words), wordXpos(word_xpos), wordStyles(word_styles), style(style) {}
|
||||
~TextBlock() override = default;
|
||||
void set_style(const BLOCK_STYLE style) { this->style = style; }
|
||||
BLOCK_STYLE get_style() const { return style; }
|
||||
bool isEmpty() override { return words.empty(); }
|
||||
void layout(EpdRenderer* renderer) override {};
|
||||
// given a renderer works out where to break the words into lines
|
||||
std::list<TextBlock*> splitIntoLines(const EpdRenderer* renderer);
|
||||
void render(const EpdRenderer* renderer, int x, int y) const;
|
||||
BlockType getType() override { return TEXT_BLOCK; }
|
||||
void serialize(std::ostream& os) const;
|
||||
static TextBlock* deserialize(std::istream& is);
|
||||
};
|
||||
@@ -0,0 +1,163 @@
|
||||
// from
|
||||
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||
|
||||
#include "htmlEntities.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
|
||||
const int MAX_ENTITY_LENGTH = 10;
|
||||
|
||||
// Use book: entities_ww2.epub to test this (Page 7: Entities parser test)
|
||||
// Note the supported keys are only in lowercase
|
||||
// Store the mappings in a unordered hash map
|
||||
static std::unordered_map<std::string, std::string> entity_lookup(
|
||||
{{""", "\""}, {"⁄", "⁄"}, {"&", "&"}, {"<", "<"}, {">", ">"},
|
||||
{"À", "À"}, {"Á", "Á"}, {"Â", "Â"}, {"Ã", "Ã"}, {"Ä", "Ä"},
|
||||
{"Å", "Å"}, {"Æ", "Æ"}, {"Ç", "Ç"}, {"È", "È"}, {"É", "É"},
|
||||
{"Ê", "Ê"}, {"Ë", "Ë"}, {"Ì", "Ì"}, {"Í", "Í"}, {"Î", "Î"},
|
||||
{"Ï", "Ï"}, {"Ð", "Ð"}, {"Ñ", "Ñ"}, {"Ò", "Ò"}, {"Ó", "Ó"},
|
||||
{"Ô", "Ô"}, {"Õ", "Õ"}, {"Ö", "Ö"}, {"Ø", "Ø"}, {"Ù", "Ù"},
|
||||
{"Ú", "Ú"}, {"Û", "Û"}, {"Ü", "Ü"}, {"Ý", "Ý"}, {"Þ", "Þ"},
|
||||
{"ß", "ß"}, {"à", "à"}, {"á", "á"}, {"â", "â"}, {"ã", "ã"},
|
||||
{"ä", "ä"}, {"å", "å"}, {"æ", "æ"}, {"ç", "ç"}, {"è", "è"},
|
||||
{"é", "é"}, {"ê", "ê"}, {"ë", "ë"}, {"ì", "ì"}, {"í", "í"},
|
||||
{"î", "î"}, {"ï", "ï"}, {"ð", "ð"}, {"ñ", "ñ"}, {"ò", "ò"},
|
||||
{"ó", "ó"}, {"ô", "ô"}, {"õ", "õ"}, {"ö", "ö"}, {"ø", "ø"},
|
||||
{"ù", "ù"}, {"ú", "ú"}, {"û", "û"}, {"ü", "ü"}, {"ý", "ý"},
|
||||
{"þ", "þ"}, {"ÿ", "ÿ"}, {" ", " "}, {"¡", "¡"}, {"¢", "¢"},
|
||||
{"£", "£"}, {"¤", "¤"}, {"¥", "¥"}, {"¦", "¦"}, {"§", "§"},
|
||||
{"¨", "¨"}, {"©", "©"}, {"ª", "ª"}, {"«", "«"}, {"¬", "¬"},
|
||||
{"­", ""}, {"®", "®"}, {"¯", "¯"}, {"°", "°"}, {"±", "±"},
|
||||
{"²", "²"}, {"³", "³"}, {"´", "´"}, {"µ", "µ"}, {"¶", "¶"},
|
||||
{"¸", "¸"}, {"¹", "¹"}, {"º", "º"}, {"»", "»"}, {"¼", "¼"},
|
||||
{"½", "½"}, {"¾", "¾"}, {"¿", "¿"}, {"×", "×"}, {"÷", "÷"},
|
||||
{"∀", "∀"}, {"∂", "∂"}, {"∃", "∃"}, {"∅", "∅"}, {"∇", "∇"},
|
||||
{"∈", "∈"}, {"∉", "∉"}, {"∋", "∋"}, {"∏", "∏"}, {"∑", "∑"},
|
||||
{"−", "−"}, {"∗", "∗"}, {"√", "√"}, {"∝", "∝"}, {"∞", "∞"},
|
||||
{"∠", "∠"}, {"∧", "∧"}, {"∨", "∨"}, {"∩", "∩"}, {"∪", "∪"},
|
||||
{"∫", "∫"}, {"∴", "∴"}, {"∼", "∼"}, {"≅", "≅"}, {"≈", "≈"},
|
||||
{"≠", "≠"}, {"≡", "≡"}, {"≤", "≤"}, {"≥", "≥"}, {"⊂", "⊂"},
|
||||
{"⊃", "⊃"}, {"⊄", "⊄"}, {"⊆", "⊆"}, {"⊇", "⊇"}, {"⊕", "⊕"},
|
||||
{"⊗", "⊗"}, {"⊥", "⊥"}, {"⋅", "⋅"}, {"Α", "Α"}, {"Β", "Β"},
|
||||
{"Γ", "Γ"}, {"Δ", "Δ"}, {"Ε", "Ε"}, {"Ζ", "Ζ"}, {"Η", "Η"},
|
||||
{"Θ", "Θ"}, {"Ι", "Ι"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"},
|
||||
{"Ν", "Ν"}, {"Ξ", "Ξ"}, {"Ο", "Ο"}, {"Π", "Π"}, {"Ρ", "Ρ"},
|
||||
{"Σ", "Σ"}, {"Τ", "Τ"}, {"Υ", "Υ"}, {"Φ", "Φ"}, {"Χ", "Χ"},
|
||||
{"Ψ", "Ψ"}, {"Ω", "Ω"}, {"α", "α"}, {"β", "β"}, {"γ", "γ"},
|
||||
{"δ", "δ"}, {"ε", "ε"}, {"ζ", "ζ"}, {"η", "η"}, {"θ", "θ"},
|
||||
{"ι", "ι"}, {"κ", "κ"}, {"λ", "λ"}, {"μ", "μ"}, {"ν", "ν"},
|
||||
{"ξ", "ξ"}, {"ο", "ο"}, {"π", "π"}, {"ρ", "ρ"}, {"ς", "ς"},
|
||||
{"σ", "σ"}, {"τ", "τ"}, {"υ", "υ"}, {"φ", "φ"}, {"χ", "χ"},
|
||||
{"ψ", "ψ"}, {"ω", "ω"}, {"ϑ", "ϑ"}, {"ϒ", "ϒ"}, {"ϖ", "ϖ"},
|
||||
{"Œ", "Œ"}, {"œ", "œ"}, {"Š", "Š"}, {"š", "š"}, {"Ÿ", "Ÿ"},
|
||||
{"ƒ", "ƒ"}, {"ˆ", "ˆ"}, {"˜", "˜"}, {" ", ""}, {" ", ""},
|
||||
{" ", ""}, {"‌", ""}, {"‍", ""}, {"‎", ""}, {"‏", ""},
|
||||
{"–", "–"}, {"—", "—"}, {"‘", "‘"}, {"’", "’"}, {"‚", "‚"},
|
||||
{"“", "“"}, {"”", "”"}, {"„", "„"}, {"†", "†"}, {"‡", "‡"},
|
||||
{"•", "•"}, {"…", "…"}, {"‰", "‰"}, {"′", "′"}, {"″", "″"},
|
||||
{"‹", "‹"}, {"›", "›"}, {"‾", "‾"}, {"€", "€"}, {"™", "™"},
|
||||
{"←", "←"}, {"↑", "↑"}, {"→", "→"}, {"↓", "↓"}, {"↔", "↔"},
|
||||
{"↵", "↵"}, {"⌈", "⌈"}, {"⌉", "⌉"}, {"⌊", "⌊"}, {"⌋", "⌋"},
|
||||
{"◊", "◊"}, {"♠", "♠"}, {"♣", "♣"}, {"♥", "♥"}, {"♦", "♦"}});
|
||||
|
||||
// converts from a unicode code point to the utf8 equivalent
|
||||
void convert_to_utf8(const int code, std::string& res) {
|
||||
// convert to a utf8 sequence
|
||||
if (code < 0x80) {
|
||||
res += static_cast<char>(code);
|
||||
} else if (code < 0x800) {
|
||||
res += static_cast<char>(0xc0 | (code >> 6));
|
||||
res += static_cast<char>(0x80 | (code & 0x3f));
|
||||
} else if (code < 0x10000) {
|
||||
res += static_cast<char>(0xe0 | (code >> 12));
|
||||
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
||||
res += static_cast<char>(0x80 | (code & 0x3f));
|
||||
} else if (code < 0x200000) {
|
||||
res += static_cast<char>(0xf0 | (code >> 18));
|
||||
res += static_cast<char>(0x80 | ((code >> 12) & 0x3f));
|
||||
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
||||
res += static_cast<char>(0x80 | (code & 0x3f));
|
||||
} else if (code < 0x4000000) {
|
||||
res += static_cast<char>(0xf8 | (code >> 24));
|
||||
res += static_cast<char>(0x80 | ((code >> 18) & 0x3f));
|
||||
res += static_cast<char>(0x80 | ((code >> 12) & 0x3f));
|
||||
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
||||
res += static_cast<char>(0x80 | (code & 0x3f));
|
||||
} else if (code < 0x80000000) {
|
||||
res += static_cast<char>(0xfc | (code >> 30));
|
||||
res += static_cast<char>(0x80 | ((code >> 24) & 0x3f));
|
||||
res += static_cast<char>(0x80 | ((code >> 18) & 0x3f));
|
||||
res += static_cast<char>(0x80 | ((code >> 12) & 0x3f));
|
||||
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
||||
}
|
||||
}
|
||||
|
||||
// handles numeric entities - e.g. Ӓ or ሴ
|
||||
bool process_numeric_entity(const std::string& entity, std::string& res) {
|
||||
int code = 0;
|
||||
// is it hex?
|
||||
if (entity[2] == 'x' || entity[2] == 'X') {
|
||||
// parse the hex code
|
||||
code = strtol(entity.substr(3, entity.size() - 3).c_str(), nullptr, 16);
|
||||
} else {
|
||||
code = strtol(entity.substr(2, entity.size() - 3).c_str(), nullptr, 10);
|
||||
}
|
||||
if (code != 0) {
|
||||
// special handling for nbsp
|
||||
if (code == 0xA0) {
|
||||
res += " ";
|
||||
} else {
|
||||
convert_to_utf8(code, res);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// handles named entities - e.g. &
|
||||
bool process_string_entity(const std::string& entity, std::string& res) {
|
||||
// it's a named entity - find it in the lookup table
|
||||
// find it in the map
|
||||
const auto it = entity_lookup.find(entity);
|
||||
if (it != entity_lookup.end()) {
|
||||
res += it->second;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// replace all the entities in the string
|
||||
std::string replaceHtmlEntities(const char* text) {
|
||||
std::string res;
|
||||
res.reserve(strlen(text));
|
||||
for (int i = 0; i < strlen(text); ++i) {
|
||||
bool flag = false;
|
||||
// do we have a potential entity?
|
||||
if (text[i] == '&') {
|
||||
// find the end of the entity
|
||||
int j = i + 1;
|
||||
while (j < strlen(text) && text[j] != ';' && j - i < MAX_ENTITY_LENGTH) {
|
||||
j++;
|
||||
}
|
||||
if (j - i > 2) {
|
||||
char entity[j - i + 1];
|
||||
strncpy(entity, text + i, j - i);
|
||||
// is it a numeric code?
|
||||
if (entity[1] == '#') {
|
||||
flag = process_numeric_entity(entity, res);
|
||||
} else {
|
||||
flag = process_string_entity(entity, res);
|
||||
}
|
||||
// skip past the entity if we successfully decoded it
|
||||
if (flag) {
|
||||
i = j;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!flag) {
|
||||
res += text[i];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// from
|
||||
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
std::string replaceHtmlEntities(const char* text);
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
|
||||
This directory is intended for project specific (private) libraries.
|
||||
PlatformIO will compile them to static libraries and link into the executable file.
|
||||
|
||||
The source code of each library should be placed in a separate directory
|
||||
("lib/your_library_name/[Code]").
|
||||
|
||||
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||
|
||||
|--lib
|
||||
| |
|
||||
| |--Bar
|
||||
| | |--docs
|
||||
| | |--examples
|
||||
| | |--src
|
||||
| | |- Bar.c
|
||||
| | |- Bar.h
|
||||
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||
| |
|
||||
| |--Foo
|
||||
| | |- Foo.c
|
||||
| | |- Foo.h
|
||||
| |
|
||||
| |- README --> THIS FILE
|
||||
|
|
||||
|- platformio.ini
|
||||
|--src
|
||||
|- main.c
|
||||
|
||||
Example contents of `src/main.c` using Foo and Bar:
|
||||
```
|
||||
#include <Foo.h>
|
||||
#include <Bar.h>
|
||||
|
||||
int main (void)
|
||||
{
|
||||
...
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||
libraries by scanning project source files.
|
||||
|
||||
More information about PlatformIO Library Dependency Finder
|
||||
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
#include <iostream>
|
||||
|
||||
namespace serialization {
|
||||
template <typename T>
|
||||
static void writePod(std::ostream& os, const T& value) {
|
||||
os.write(reinterpret_cast<const char*>(&value), sizeof(T));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static void readPod(std::istream& is, T& value) {
|
||||
is.read(reinterpret_cast<char*>(&value), sizeof(T));
|
||||
}
|
||||
|
||||
static void writeString(std::ostream& os, const std::string& s) {
|
||||
const uint32_t len = s.size();
|
||||
writePod(os, len);
|
||||
os.write(s.data(), len);
|
||||
}
|
||||
|
||||
static void readString(std::istream& is, std::string& s) {
|
||||
uint32_t len;
|
||||
readPod(is, len);
|
||||
s.resize(len);
|
||||
is.read(&s[0], len);
|
||||
}
|
||||
} // namespace serialization
|
||||
@@ -0,0 +1,31 @@
|
||||
#include "Utf8.h"
|
||||
|
||||
int utf8CodepointLen(const unsigned char c) {
|
||||
if (c < 0x80) return 1; // 0xxxxxxx
|
||||
if ((c >> 5) == 0x6) return 2; // 110xxxxx
|
||||
if ((c >> 4) == 0xE) return 3; // 1110xxxx
|
||||
if ((c >> 3) == 0x1E) return 4; // 11110xxx
|
||||
return 1; // fallback for invalid
|
||||
}
|
||||
|
||||
uint32_t utf8NextCodepoint(const unsigned char** string) {
|
||||
if (**string == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int bytes = utf8CodepointLen(**string);
|
||||
const uint8_t* chr = *string;
|
||||
*string += bytes;
|
||||
|
||||
if (bytes == 1) {
|
||||
return chr[0];
|
||||
}
|
||||
|
||||
uint32_t cp = chr[0] & ((1 << (7 - bytes)) - 1); // mask header bits
|
||||
|
||||
for (int i = 1; i < bytes; i++) {
|
||||
cp = (cp << 6) | (chr[i] & 0x3F);
|
||||
}
|
||||
|
||||
return cp;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
uint32_t utf8NextCodepoint(const unsigned char** string);
|
||||
@@ -0,0 +1,140 @@
|
||||
#include "ZipFile.h"
|
||||
|
||||
#include <HardwareSerial.h>
|
||||
#include <miniz.h>
|
||||
|
||||
int libzInflateOneShot(const uint8_t* inputBuff, const size_t compSize, uint8_t* outputBuff, const size_t uncompSize) {
|
||||
mz_stream pStream = {
|
||||
.next_in = inputBuff,
|
||||
.avail_in = compSize,
|
||||
.total_in = 0,
|
||||
.next_out = outputBuff,
|
||||
.avail_out = uncompSize,
|
||||
.total_out = 0,
|
||||
};
|
||||
|
||||
int status = 0;
|
||||
status = mz_inflateInit2(&pStream, -MZ_DEFAULT_WINDOW_BITS);
|
||||
|
||||
if (status != MZ_OK) {
|
||||
Serial.printf("inflateInit2 failed: %d\n", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
status = mz_inflate(&pStream, MZ_FINISH);
|
||||
if (status != MZ_STREAM_END) {
|
||||
Serial.printf("inflate failed: %d\n", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
status = mz_inflateEnd(&pStream);
|
||||
if (status != MZ_OK) {
|
||||
Serial.printf("inflateEnd failed: %d\n", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
char* ZipFile::readTextFileToMemory(const char* filename, size_t* size) const {
|
||||
const auto data = readFileToMemory(filename, size, true);
|
||||
return data ? reinterpret_cast<char*>(data) : nullptr;
|
||||
}
|
||||
|
||||
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, bool trailingNullByte) const {
|
||||
mz_zip_archive zipArchive = {};
|
||||
const bool status = mz_zip_reader_init_file(&zipArchive, filePath.c_str(), 0);
|
||||
|
||||
if (!status) {
|
||||
Serial.printf("mz_zip_reader_init_file() failed!\nError %s\n", mz_zip_get_error_string(zipArchive.m_last_error));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// find the file
|
||||
mz_uint32 fileIndex = 0;
|
||||
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
|
||||
Serial.printf("Could not find file %s\n", filename);
|
||||
mz_zip_reader_end(&zipArchive);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
mz_zip_archive_file_stat fileStat;
|
||||
if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, &fileStat)) {
|
||||
Serial.printf("mz_zip_reader_file_stat() failed!\nError %s\n", mz_zip_get_error_string(zipArchive.m_last_error));
|
||||
mz_zip_reader_end(&zipArchive);
|
||||
return nullptr;
|
||||
}
|
||||
mz_zip_reader_end(&zipArchive);
|
||||
|
||||
uint8_t pLocalHeader[30];
|
||||
uint64_t fileOffset = fileStat.m_local_header_ofs;
|
||||
|
||||
// Reopen the file to manual read out delated bytes
|
||||
FILE* file = fopen(filePath.c_str(), "rb");
|
||||
fseek(file, fileOffset, SEEK_SET);
|
||||
|
||||
const size_t read = fread(pLocalHeader, 1, 30, file);
|
||||
if (read != 30) {
|
||||
Serial.println("Something went wrong reading the local header");
|
||||
fclose(file);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
||||
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
||||
Serial.println("Not a valid zip file header");
|
||||
fclose(file);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const uint16_t filenameLength = pLocalHeader[26] + (pLocalHeader[27] << 8);
|
||||
const uint16_t extraOffset = pLocalHeader[28] + (pLocalHeader[29] << 8);
|
||||
fileOffset += 30 + filenameLength + extraOffset;
|
||||
fseek(file, fileOffset, SEEK_SET);
|
||||
|
||||
const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size);
|
||||
const auto inflatedDataSize = static_cast<size_t>(fileStat.m_uncomp_size);
|
||||
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
|
||||
const auto data = static_cast<uint8_t*>(malloc(dataSize));
|
||||
|
||||
if (!fileStat.m_method) {
|
||||
// no deflation, just read content
|
||||
const size_t dataRead = fread(data, 1, inflatedDataSize, file);
|
||||
fclose(file);
|
||||
if (dataRead != inflatedDataSize) {
|
||||
Serial.println("Failed to read data");
|
||||
return nullptr;
|
||||
}
|
||||
} else {
|
||||
// Read out deflated content from file
|
||||
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
||||
if (deflatedData == nullptr) {
|
||||
Serial.println("Failed to allocate memory for decompression buffer");
|
||||
fclose(file);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const size_t dataRead = fread(deflatedData, 1, deflatedDataSize, file);
|
||||
fclose(file);
|
||||
if (dataRead != deflatedDataSize) {
|
||||
Serial.printf("Failed to read data, expected %d got %d\n", deflatedDataSize, dataRead);
|
||||
free(deflatedData);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const int result = libzInflateOneShot(deflatedData, deflatedDataSize, data, inflatedDataSize);
|
||||
free(deflatedData);
|
||||
if (result != MZ_OK) {
|
||||
Serial.println("Failed to inflate file");
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
if (trailingNullByte) {
|
||||
data[inflatedDataSize] = '\0';
|
||||
}
|
||||
if (size) {
|
||||
*size = inflatedDataSize;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
class ZipFile {
|
||||
std::string filePath;
|
||||
|
||||
public:
|
||||
explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {}
|
||||
~ZipFile() = default;
|
||||
char* readTextFileToMemory(const char* filename, size_t* size = nullptr) const;
|
||||
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const;
|
||||
};
|
||||
+7072
File diff suppressed because it is too large
Load Diff
+1709
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user