Extract book cover utilities into BookCoverUtils
Refactors book cover and metadata handling out of Epub class into a dedicated BookCoverUtils utility. Moves FootnoteEntry struct into reader activity directory. Simplifies cache clearing to use direct storage operations instead of Epub class.
This commit is contained in:
@@ -1,927 +0,0 @@
|
||||
#include "Epub.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <Logging.h>
|
||||
#include <PngToBmpConverter.h>
|
||||
#include <Utf8.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
#include "Epub/parsers/ContainerParser.h"
|
||||
#include "Epub/parsers/ContentOpfParser.h"
|
||||
#include "Epub/parsers/TocNavParser.h"
|
||||
#include "Epub/parsers/TocNcxParser.h"
|
||||
|
||||
bool Epub::findContentOpfFile(std::string* contentOpfFile) const {
|
||||
const auto containerPath = "META-INF/container.xml";
|
||||
size_t containerSize;
|
||||
|
||||
// Get file size without loading it all into heap
|
||||
if (!getItemSize(containerPath, &containerSize)) {
|
||||
LOG_ERR("EBP", "Could not find or size META-INF/container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
ContainerParser containerParser(containerSize);
|
||||
|
||||
if (!containerParser.setup()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stream read (reusing your existing stream logic)
|
||||
if (!readItemContentsToStream(containerPath, containerParser, 512)) {
|
||||
LOG_ERR("EBP", "Could not read META-INF/container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Extract the result
|
||||
if (containerParser.fullPath.empty()) {
|
||||
LOG_ERR("EBP", "Could not find valid rootfile in container.xml");
|
||||
return false;
|
||||
}
|
||||
|
||||
*contentOpfFile = std::move(containerParser.fullPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseContentOpf(BookMetadataCache::BookMetadata& bookMetadata, const bool writeSpineEntries) {
|
||||
std::string contentOpfFilePath;
|
||||
if (!findContentOpfFile(&contentOpfFilePath)) {
|
||||
LOG_ERR("EBP", "Could not find content.opf in zip");
|
||||
return false;
|
||||
}
|
||||
|
||||
contentBasePath = contentOpfFilePath.substr(0, contentOpfFilePath.find_last_of('/') + 1);
|
||||
|
||||
LOG_DBG("EBP", "Parsing content.opf: %s", contentOpfFilePath.c_str());
|
||||
|
||||
size_t contentOpfSize;
|
||||
if (!getItemSize(contentOpfFilePath, &contentOpfSize)) {
|
||||
LOG_ERR("EBP", "Could not get size of content.opf");
|
||||
return false;
|
||||
}
|
||||
|
||||
ContentOpfParser opfParser(getCachePath(), getBasePath(), contentOpfSize,
|
||||
writeSpineEntries ? bookMetadataCache.get() : nullptr);
|
||||
if (!opfParser.setup()) {
|
||||
LOG_ERR("EBP", "Could not setup content.opf parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!readItemContentsToStream(contentOpfFilePath, opfParser, 1024)) {
|
||||
LOG_ERR("EBP", "Could not read content.opf");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Grab data from opfParser into epub. Normalize titles to NFC so NFD (combining
|
||||
// mark) text renders correctly — the device fonts have no mark positioning.
|
||||
bookMetadata.title = utf8ComposeNfc(opfParser.title);
|
||||
bookMetadata.author = opfParser.author;
|
||||
bookMetadata.language = opfParser.language;
|
||||
bookMetadata.coverItemHref = opfParser.coverItemHref;
|
||||
|
||||
// Guide-based cover fallback: if no cover found via metadata/properties,
|
||||
// try extracting the image reference from the guide's cover page XHTML
|
||||
if (bookMetadata.coverItemHref.empty() && !opfParser.guideCoverPageHref.empty()) {
|
||||
LOG_DBG("EBP", "No cover from metadata, trying guide cover page: %s", opfParser.guideCoverPageHref.c_str());
|
||||
size_t coverPageSize;
|
||||
uint8_t* coverPageData = readItemContentsToBytes(opfParser.guideCoverPageHref, &coverPageSize, true);
|
||||
if (coverPageData) {
|
||||
const std::string coverPageHtml(reinterpret_cast<char*>(coverPageData), coverPageSize);
|
||||
free(coverPageData);
|
||||
|
||||
// Determine base path of the cover page for resolving relative image references
|
||||
std::string coverPageBase;
|
||||
const auto lastSlash = opfParser.guideCoverPageHref.rfind('/');
|
||||
if (lastSlash != std::string::npos) {
|
||||
coverPageBase = opfParser.guideCoverPageHref.substr(0, lastSlash + 1);
|
||||
}
|
||||
|
||||
// Search for image references: xlink:href="..." (SVG) and src="..." (img)
|
||||
std::string imageRef;
|
||||
for (const char* pattern : {"xlink:href=\"", "src=\""}) {
|
||||
auto pos = coverPageHtml.find(pattern);
|
||||
while (pos != std::string::npos) {
|
||||
pos += strlen(pattern);
|
||||
const auto endPos = coverPageHtml.find('"', pos);
|
||||
if (endPos != std::string::npos) {
|
||||
const auto ref = std::string_view{coverPageHtml}.substr(pos, endPos - pos);
|
||||
// Cover BMP generation supports JPG/PNG only; skip GIF so an unsupported wrapper image
|
||||
// does not block a later supported cover reference.
|
||||
if (FsHelpers::hasPngExtension(ref) || FsHelpers::hasJpgExtension(ref)) {
|
||||
imageRef = ref;
|
||||
break;
|
||||
}
|
||||
}
|
||||
pos = coverPageHtml.find(pattern, pos);
|
||||
}
|
||||
if (!imageRef.empty()) break;
|
||||
}
|
||||
|
||||
if (!imageRef.empty()) {
|
||||
bookMetadata.coverItemHref = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(coverPageBase + imageRef));
|
||||
LOG_DBG("EBP", "Found cover image from guide: %s", bookMetadata.coverItemHref.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bookMetadata.textReferenceHref = opfParser.textReferenceHref;
|
||||
|
||||
if (!opfParser.tocNcxPath.empty()) {
|
||||
tocNcxItem = opfParser.tocNcxPath;
|
||||
}
|
||||
|
||||
if (!opfParser.tocNavPath.empty()) {
|
||||
tocNavItem = opfParser.tocNavPath;
|
||||
}
|
||||
|
||||
if (!opfParser.cssFiles.empty()) {
|
||||
cssFiles = opfParser.cssFiles;
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "Successfully parsed content.opf");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseTocNcxFile() const {
|
||||
// the ncx file should have been specified in the content.opf file
|
||||
if (tocNcxItem.empty()) {
|
||||
LOG_DBG("EBP", "No ncx file specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "Parsing toc ncx file: %s", tocNcxItem.c_str());
|
||||
|
||||
const auto tmpNcxPath = getCachePath() + "/toc.ncx";
|
||||
HalFile tempNcxFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNcxItem, tempNcxFile, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
tempNcxFile.close();
|
||||
if (!Storage.openFileForRead("EBP", tmpNcxPath, tempNcxFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto ncxSize = tempNcxFile.size();
|
||||
|
||||
TocNcxParser ncxParser(contentBasePath, ncxSize, bookMetadataCache.get());
|
||||
|
||||
if (!ncxParser.setup()) {
|
||||
LOG_ERR("EBP", "Could not setup toc ncx parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto ncxBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!ncxBuffer) {
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc ncx parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (tempNcxFile.available()) {
|
||||
const auto readSize = tempNcxFile.read(ncxBuffer, 1024);
|
||||
if (readSize == 0) break;
|
||||
const auto processedSize = ncxParser.write(ncxBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
LOG_ERR("EBP", "Could not process all toc ncx data");
|
||||
free(ncxBuffer);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
free(ncxBuffer);
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
tempNcxFile.close();
|
||||
Storage.remove(tmpNcxPath.c_str());
|
||||
|
||||
LOG_DBG("EBP", "Parsed TOC items");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::parseTocNavFile() const {
|
||||
// the nav file should have been specified in the content.opf file (EPUB 3)
|
||||
if (tocNavItem.empty()) {
|
||||
LOG_DBG("EBP", "No nav file specified");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "Parsing toc nav file: %s", tocNavItem.c_str());
|
||||
|
||||
const auto tmpNavPath = getCachePath() + "/toc.nav";
|
||||
HalFile tempNavFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpNavPath, tempNavFile)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(tocNavItem, tempNavFile, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
tempNavFile.close();
|
||||
if (!Storage.openFileForRead("EBP", tmpNavPath, tempNavFile)) {
|
||||
return false;
|
||||
}
|
||||
const auto navSize = tempNavFile.size();
|
||||
|
||||
// Note: We can't use `contentBasePath` here as the nav file may be in a different folder to the content.opf
|
||||
// and the HTMLX nav file will have hrefs relative to itself
|
||||
const std::string navContentBasePath = tocNavItem.substr(0, tocNavItem.find_last_of('/') + 1);
|
||||
TocNavParser navParser(navContentBasePath, navSize, bookMetadataCache.get());
|
||||
|
||||
if (!navParser.setup()) {
|
||||
LOG_ERR("EBP", "Could not setup toc nav parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto navBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!navBuffer) {
|
||||
LOG_ERR("EBP", "Could not allocate memory for toc nav parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
while (tempNavFile.available()) {
|
||||
const auto readSize = tempNavFile.read(navBuffer, 1024);
|
||||
const auto processedSize = navParser.write(navBuffer, readSize);
|
||||
|
||||
if (processedSize != readSize) {
|
||||
LOG_ERR("EBP", "Could not process all toc nav data");
|
||||
free(navBuffer);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
free(navBuffer);
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
tempNavFile.close();
|
||||
Storage.remove(tmpNavPath.c_str());
|
||||
|
||||
LOG_DBG("EBP", "Parsed TOC nav items");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Epub::discoverCssFilesFromZip() {
|
||||
const std::string& opfDir = contentBasePath;
|
||||
ZipFile zf(filepath);
|
||||
|
||||
if (!zf.enumerateFilePaths([&](std::string_view filePath) {
|
||||
if (!opfDir.empty() && filePath.find(opfDir) != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!FsHelpers::hasCssExtension(filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::find(cssFiles.begin(), cssFiles.end(), filePath) != cssFiles.end()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "Discovered CSS file via ZIP enumeration: %.*s", (int)filePath.size(), filePath.data());
|
||||
cssFiles.push_back(std::string{filePath});
|
||||
})) {
|
||||
LOG_ERR("EBP", "Failed to enumerate ZIP file paths for CSS discovery");
|
||||
}
|
||||
}
|
||||
|
||||
void Epub::parseCssFiles() const {
|
||||
// Maximum CSS file size we'll attempt to parse (uncompressed)
|
||||
// Larger files risk memory exhaustion on ESP32
|
||||
constexpr size_t MAX_CSS_FILE_SIZE = 128 * 1024; // 128KB
|
||||
// Minimum heap required before attempting CSS parsing
|
||||
constexpr size_t MIN_HEAP_FOR_CSS_PARSING = 64 * 1024; // 64KB
|
||||
|
||||
if (cssFiles.empty()) {
|
||||
LOG_DBG("EBP", "No CSS files to parse, but CssParser created for inline styles");
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "CSS files to parse: %zu", cssFiles.size());
|
||||
|
||||
// See if we have a cached version of the CSS rules
|
||||
if (cssParser->hasCache()) {
|
||||
LOG_DBG("EBP", "CSS cache exists, skipping parseCssFiles");
|
||||
return;
|
||||
}
|
||||
|
||||
// No cache yet - parse CSS files
|
||||
for (const auto& cssPath : cssFiles) {
|
||||
LOG_DBG("EBP", "Parsing CSS file: %s", cssPath.c_str());
|
||||
|
||||
// Check heap before parsing - CSS parsing allocates heavily
|
||||
const uint32_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_HEAP_FOR_CSS_PARSING) {
|
||||
LOG_ERR("EBP", "Insufficient heap for CSS parsing (%u bytes free, need %zu), skipping: %s", freeHeap,
|
||||
MIN_HEAP_FOR_CSS_PARSING, cssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check CSS file size before decompressing - skip files that are too large
|
||||
size_t cssFileSize = 0;
|
||||
if (getItemSize(cssPath, &cssFileSize)) {
|
||||
if (cssFileSize > MAX_CSS_FILE_SIZE) {
|
||||
LOG_ERR("EBP", "CSS file too large (%zu bytes > %zu max), skipping: %s", cssFileSize, MAX_CSS_FILE_SIZE,
|
||||
cssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract CSS file to temp location
|
||||
const auto tmpCssPath = getCachePath() + "/.tmp.css";
|
||||
HalFile tempCssFile;
|
||||
if (!Storage.openFileForWrite("EBP", tmpCssPath, tempCssFile)) {
|
||||
LOG_ERR("EBP", "Could not create temp CSS file");
|
||||
continue;
|
||||
}
|
||||
if (!readItemContentsToStream(cssPath, tempCssFile, 1024)) {
|
||||
LOG_ERR("EBP", "Could not read CSS file: %s", cssPath.c_str());
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
tempCssFile.close();
|
||||
Storage.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
// Explicitly close() file before reopening for reading
|
||||
tempCssFile.close();
|
||||
|
||||
// Parse the CSS file
|
||||
if (!Storage.openFileForRead("EBP", tmpCssPath, tempCssFile)) {
|
||||
LOG_ERR("EBP", "Could not open temp CSS file for reading");
|
||||
Storage.remove(tmpCssPath.c_str());
|
||||
continue;
|
||||
}
|
||||
cssParser->loadFromStream(tempCssFile);
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
tempCssFile.close();
|
||||
Storage.remove(tmpCssPath.c_str());
|
||||
}
|
||||
|
||||
// Save to cache for next time
|
||||
if (!cssParser->saveToCache()) {
|
||||
LOG_ERR("EBP", "Failed to save CSS rules to cache");
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "Loaded %zu CSS style rules from %zu files", cssParser->ruleCount(), cssFiles.size());
|
||||
cssParser->clear();
|
||||
}
|
||||
|
||||
// load in the meta data for the epub file
|
||||
bool Epub::load(const bool buildIfMissing, const bool skipLoadingCss) {
|
||||
LOG_DBG("EBP", "Loading ePub: %s", 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(cachePath));
|
||||
|
||||
// Try to load existing cache first
|
||||
if (bookMetadataCache->load()) {
|
||||
if (!skipLoadingCss) {
|
||||
// Rebuild CSS cache when missing or when cache version changed (loadFromCache removes stale file)
|
||||
if (!cssParser->hasCache() || !cssParser->loadFromCache()) {
|
||||
LOG_DBG("EBP", "CSS rules cache missing or stale, attempting to parse CSS files");
|
||||
cssParser->deleteCache();
|
||||
|
||||
BookMetadataCache::BookMetadata cachedMetadata = bookMetadataCache->coreMetadata;
|
||||
if (!parseContentOpf(cachedMetadata, /*writeSpineEntries=*/false)) {
|
||||
LOG_ERR("EBP", "Could not parse content.opf from cached bookMetadata for CSS files");
|
||||
// continue anyway - book will work without CSS and we'll still load any inline style CSS
|
||||
} else {
|
||||
discoverCssFilesFromZip();
|
||||
}
|
||||
bookMetadataCache.reset();
|
||||
parseCssFiles();
|
||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||
if (!bookMetadataCache->load()) {
|
||||
LOG_ERR("EBP", "Failed to reload cache after CSS rebuild");
|
||||
return false;
|
||||
}
|
||||
// Invalidate section caches so they are rebuilt with the new CSS
|
||||
Storage.removeDir((cachePath + "/sections").c_str());
|
||||
}
|
||||
}
|
||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
// If we didn't load from cache above and we aren't allowed to build, fail now
|
||||
if (!buildIfMissing) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Cache doesn't exist or is invalid, build it
|
||||
LOG_DBG("EBP", "Cache not found, building spine/TOC cache");
|
||||
setupCacheDir();
|
||||
|
||||
const uint32_t indexingStart = millis();
|
||||
|
||||
// Begin building cache - stream entries to disk immediately
|
||||
if (!bookMetadataCache->beginWrite()) {
|
||||
LOG_ERR("EBP", "Could not begin writing cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
// OPF Pass
|
||||
const uint32_t opfStart = millis();
|
||||
BookMetadataCache::BookMetadata bookMetadata;
|
||||
if (!bookMetadataCache->beginContentOpfPass()) {
|
||||
LOG_ERR("EBP", "Could not begin writing content.opf pass");
|
||||
return false;
|
||||
}
|
||||
if (!parseContentOpf(bookMetadata)) {
|
||||
LOG_ERR("EBP", "Could not parse content.opf");
|
||||
return false;
|
||||
}
|
||||
discoverCssFilesFromZip();
|
||||
if (!bookMetadataCache->endContentOpfPass()) {
|
||||
LOG_ERR("EBP", "Could not end writing content.opf pass");
|
||||
return false;
|
||||
}
|
||||
LOG_DBG("EBP", "OPF pass completed in %lu ms", millis() - opfStart);
|
||||
|
||||
// TOC Pass - try EPUB 3 nav first, fall back to NCX
|
||||
const uint32_t tocStart = millis();
|
||||
if (!bookMetadataCache->beginTocPass()) {
|
||||
LOG_ERR("EBP", "Could not begin writing toc pass");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool tocParsed = false;
|
||||
|
||||
// Try EPUB 3 nav document first (preferred)
|
||||
if (!tocNavItem.empty()) {
|
||||
LOG_DBG("EBP", "Attempting to parse EPUB 3 nav document");
|
||||
tocParsed = parseTocNavFile();
|
||||
}
|
||||
|
||||
// Fall back to NCX if nav parsing failed or wasn't available
|
||||
if (!tocParsed && !tocNcxItem.empty()) {
|
||||
LOG_DBG("EBP", "Falling back to NCX TOC");
|
||||
tocParsed = parseTocNcxFile();
|
||||
}
|
||||
|
||||
if (!tocParsed) {
|
||||
LOG_ERR("EBP", "Warning: Could not parse any TOC format");
|
||||
// Continue anyway - book will work without TOC
|
||||
}
|
||||
|
||||
if (!bookMetadataCache->endTocPass()) {
|
||||
LOG_ERR("EBP", "Could not end writing toc pass");
|
||||
return false;
|
||||
}
|
||||
LOG_DBG("EBP", "TOC pass completed in %lu ms", millis() - tocStart);
|
||||
|
||||
// Close the cache files
|
||||
if (!bookMetadataCache->endWrite()) {
|
||||
LOG_ERR("EBP", "Could not end writing cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Build final book.bin
|
||||
const uint32_t buildStart = millis();
|
||||
if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) {
|
||||
LOG_ERR("EBP", "Could not update mappings and sizes");
|
||||
return false;
|
||||
}
|
||||
LOG_DBG("EBP", "buildBookBin completed in %lu ms", millis() - buildStart);
|
||||
LOG_DBG("EBP", "Total indexing completed in %lu ms", millis() - indexingStart);
|
||||
|
||||
if (!bookMetadataCache->cleanupTmpFiles()) {
|
||||
LOG_DBG("EBP", "Could not cleanup tmp files - ignoring");
|
||||
}
|
||||
|
||||
if (!skipLoadingCss) {
|
||||
// Parse CSS before reloading book.bin to leave more heap for CSS rule-table growth.
|
||||
bookMetadataCache.reset();
|
||||
parseCssFiles();
|
||||
Storage.removeDir((cachePath + "/sections").c_str());
|
||||
}
|
||||
|
||||
// Reload the cache from disk so it's in the correct state
|
||||
bookMetadataCache.reset(new BookMetadataCache(cachePath));
|
||||
if (!bookMetadataCache->load()) {
|
||||
LOG_ERR("EBP", "Failed to reload cache after writing");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("EBP", "Loaded ePub: %s", filepath.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Epub::clearCache() const {
|
||||
if (!Storage.exists(cachePath.c_str())) {
|
||||
LOG_DBG("EPB", "Cache does not exist, no action needed");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Storage.removeDir(cachePath.c_str())) {
|
||||
LOG_ERR("EPB", "Failed to clear cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("EPB", "Cache cleared successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
void Epub::setupCacheDir() const {
|
||||
if (Storage.exists(cachePath.c_str())) {
|
||||
return;
|
||||
}
|
||||
|
||||
Storage.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 {
|
||||
static std::string blank;
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
return blank;
|
||||
}
|
||||
|
||||
return bookMetadataCache->coreMetadata.title;
|
||||
}
|
||||
|
||||
const std::string& Epub::getAuthor() const {
|
||||
static std::string blank;
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
return blank;
|
||||
}
|
||||
|
||||
return bookMetadataCache->coreMetadata.author;
|
||||
}
|
||||
|
||||
const std::string& Epub::getLanguage() const {
|
||||
static std::string blank;
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
return blank;
|
||||
}
|
||||
|
||||
return bookMetadataCache->coreMetadata.language;
|
||||
}
|
||||
|
||||
std::string Epub::getCoverBmpPath(bool cropped) const {
|
||||
const auto coverFileName = std::string("cover") + (cropped ? "_crop" : "");
|
||||
return cachePath + "/" + coverFileName + ".bmp";
|
||||
}
|
||||
|
||||
bool Epub::generateCoverBmp(bool cropped) const {
|
||||
// Already generated, return true
|
||||
if (Storage.exists(getCoverBmpPath(cropped).c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
LOG_ERR("EBP", "Cannot generate cover BMP, cache not loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
LOG_ERR("EBP", "No known cover image");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FsHelpers::hasJpgExtension(coverImageHref)) {
|
||||
LOG_DBG("EBP", "Generating BMP from JPG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
HalFile coverJpg;
|
||||
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
coverJpg.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HalFile coverBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
return false;
|
||||
}
|
||||
const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped);
|
||||
// Explicitly close() files before calling Storage.remove()
|
||||
coverJpg.close();
|
||||
coverBmp.close();
|
||||
Storage.remove(coverJpgTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("EBP", "Failed to generate BMP from cover image");
|
||||
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
}
|
||||
|
||||
if (FsHelpers::hasPngExtension(coverImageHref)) {
|
||||
LOG_DBG("EBP", "Generating BMP from PNG cover image (%s mode)", cropped ? "cropped" : "fit");
|
||||
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||
|
||||
HalFile coverPng;
|
||||
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
coverPng.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HalFile coverBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getCoverBmpPath(cropped), coverBmp)) {
|
||||
return false;
|
||||
}
|
||||
const bool success = PngToBmpConverter::pngFileToBmpStream(coverPng, coverBmp, cropped);
|
||||
// Explicitly close() files before calling Storage.remove()
|
||||
coverPng.close();
|
||||
coverBmp.close();
|
||||
Storage.remove(coverPngTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("EBP", "Failed to generate BMP from PNG cover image");
|
||||
Storage.remove(getCoverBmpPath(cropped).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
}
|
||||
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string Epub::getThumbBmpPath() const { return cachePath + "/thumb_[HEIGHT].bmp"; }
|
||||
std::string Epub::getThumbBmpPath(int height) const { return cachePath + "/thumb_" + std::to_string(height) + ".bmp"; }
|
||||
|
||||
bool Epub::generateThumbBmp(int height) const {
|
||||
// Already generated, return true
|
||||
if (Storage.exists(getThumbBmpPath(height).c_str())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
LOG_ERR("EBP", "Cannot generate thumb BMP, cache not loaded");
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto coverImageHref = bookMetadataCache->coreMetadata.coverItemHref;
|
||||
if (coverImageHref.empty()) {
|
||||
LOG_DBG("EBP", "No known cover image for thumbnail");
|
||||
} else if (FsHelpers::hasJpgExtension(coverImageHref)) {
|
||||
LOG_DBG("EBP", "Generating thumb BMP from JPG cover image");
|
||||
const auto coverJpgTempPath = getCachePath() + "/.cover.jpg";
|
||||
|
||||
HalFile coverJpg;
|
||||
if (!Storage.openFileForWrite("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverJpg, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
coverJpg.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverJpgTempPath, coverJpg)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HalFile thumbBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
return false;
|
||||
}
|
||||
// Use smaller target size for Continue Reading card (half of screen: 240x400)
|
||||
// Generate 1-bit BMP for fast home screen rendering (no gray passes needed)
|
||||
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||
int THUMB_TARGET_HEIGHT = height;
|
||||
const bool success = JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverJpg, thumbBmp, THUMB_TARGET_WIDTH,
|
||||
THUMB_TARGET_HEIGHT);
|
||||
// Explicitly close() files before calling Storage.remove()
|
||||
coverJpg.close();
|
||||
thumbBmp.close();
|
||||
Storage.remove(coverJpgTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("EBP", "Failed to generate thumb BMP from JPG cover image");
|
||||
Storage.remove(getThumbBmpPath(height).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated thumb BMP from JPG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else if (FsHelpers::hasPngExtension(coverImageHref)) {
|
||||
LOG_DBG("EBP", "Generating thumb BMP from PNG cover image");
|
||||
const auto coverPngTempPath = getCachePath() + "/.cover.png";
|
||||
|
||||
HalFile coverPng;
|
||||
if (!Storage.openFileForWrite("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
readItemContentsToStream(coverImageHref, coverPng, 1024);
|
||||
// Explicitly close() file before reopening for reading
|
||||
coverPng.close();
|
||||
|
||||
if (!Storage.openFileForRead("EBP", coverPngTempPath, coverPng)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HalFile thumbBmp;
|
||||
if (!Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp)) {
|
||||
return false;
|
||||
}
|
||||
int THUMB_TARGET_WIDTH = height * 0.6;
|
||||
int THUMB_TARGET_HEIGHT = height;
|
||||
const bool success =
|
||||
PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverPng, thumbBmp, THUMB_TARGET_WIDTH, THUMB_TARGET_HEIGHT);
|
||||
// Explicitly close() files before calling Storage.remove()
|
||||
coverPng.close();
|
||||
thumbBmp.close();
|
||||
Storage.remove(coverPngTempPath.c_str());
|
||||
|
||||
if (!success) {
|
||||
LOG_ERR("EBP", "Failed to generate thumb BMP from PNG cover image");
|
||||
Storage.remove(getThumbBmpPath(height).c_str());
|
||||
}
|
||||
LOG_DBG("EBP", "Generated thumb BMP from PNG cover image, success: %s", success ? "yes" : "no");
|
||||
return success;
|
||||
} else {
|
||||
LOG_ERR("EBP", "Cover image is not a supported format, skipping thumbnail");
|
||||
}
|
||||
|
||||
// Write an empty bmp file to avoid generation attempts in the future
|
||||
HalFile thumbBmp;
|
||||
Storage.openFileForWrite("EBP", getThumbBmpPath(height), thumbBmp);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t* Epub::readItemContentsToBytes(const std::string& itemHref, size_t* size, const bool trailingNullByte) const {
|
||||
if (itemHref.empty()) {
|
||||
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||
|
||||
const auto content = ZipFile(filepath).readFileToMemory(path.c_str(), size, trailingNullByte);
|
||||
if (!content) {
|
||||
LOG_DBG("EBP", "Failed to read item %s", path.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
bool Epub::readItemContentsToStream(const std::string& itemHref, Print& out, const size_t chunkSize) const {
|
||||
if (itemHref.empty()) {
|
||||
LOG_DBG("EBP", "Failed to read item, empty href");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||
return ZipFile(filepath).readFileToStream(path.c_str(), out, chunkSize);
|
||||
}
|
||||
|
||||
bool Epub::getItemSize(const std::string& itemHref, size_t* size) const {
|
||||
const std::string path = FsHelpers::normalisePath(itemHref);
|
||||
return ZipFile(filepath).getInflatedFileSize(path.c_str(), size);
|
||||
}
|
||||
|
||||
int Epub::getSpineItemsCount() const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
return 0;
|
||||
}
|
||||
return bookMetadataCache->getSpineCount();
|
||||
}
|
||||
|
||||
size_t Epub::getCumulativeSpineItemSize(const int spineIndex) const { return getSpineItem(spineIndex).cumulativeSize; }
|
||||
|
||||
BookMetadataCache::SpineEntry Epub::getSpineItem(const int spineIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
LOG_ERR("EBP", "getSpineItem called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (spineIndex < 0 || spineIndex >= bookMetadataCache->getSpineCount()) {
|
||||
LOG_ERR("EBP", "getSpineItem index:%d is out of range", spineIndex);
|
||||
return bookMetadataCache->getSpineEntry(0);
|
||||
}
|
||||
|
||||
return bookMetadataCache->getSpineEntry(spineIndex);
|
||||
}
|
||||
|
||||
BookMetadataCache::TocEntry Epub::getTocItem(const int tocIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
LOG_DBG("EBP", "getTocItem called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||
LOG_DBG("EBP", "getTocItem index:%d is out of range", tocIndex);
|
||||
return {};
|
||||
}
|
||||
|
||||
return bookMetadataCache->getTocEntry(tocIndex);
|
||||
}
|
||||
|
||||
int Epub::getTocItemsCount() const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return bookMetadataCache->getTocCount();
|
||||
}
|
||||
|
||||
// work out the section index for a toc index
|
||||
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
LOG_ERR("EBP", "getSpineIndexForTocIndex called but cache not loaded");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (tocIndex < 0 || tocIndex >= bookMetadataCache->getTocCount()) {
|
||||
LOG_ERR("EBP", "getSpineIndexForTocIndex: tocIndex %d out of range", tocIndex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int spineIndex = bookMetadataCache->getTocEntry(tocIndex).spineIndex;
|
||||
if (spineIndex < 0) {
|
||||
LOG_DBG("EBP", "Section not found for TOC index %d", tocIndex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
return spineIndex;
|
||||
}
|
||||
|
||||
int Epub::getTocIndexForSpineIndex(const int spineIndex) const { return getSpineItem(spineIndex).tocIndex; }
|
||||
|
||||
size_t Epub::getBookSize() const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded() || bookMetadataCache->getSpineCount() == 0) {
|
||||
return 0;
|
||||
}
|
||||
return getCumulativeSpineItemSize(getSpineItemsCount() - 1);
|
||||
}
|
||||
|
||||
int Epub::getSpineIndexForTextReference() const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) {
|
||||
LOG_ERR("EBP", "getSpineIndexForTextReference called but cache not loaded");
|
||||
return 0;
|
||||
}
|
||||
LOG_DBG("EBP", "Core Metadata: cover(%d)=%s, textReference(%d)=%s",
|
||||
bookMetadataCache->coreMetadata.coverItemHref.size(), bookMetadataCache->coreMetadata.coverItemHref.c_str(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.size(),
|
||||
bookMetadataCache->coreMetadata.textReferenceHref.c_str());
|
||||
|
||||
if (bookMetadataCache->coreMetadata.textReferenceHref.empty()) {
|
||||
// there was no textReference in epub, so we return 0 (the first chapter)
|
||||
return 0;
|
||||
}
|
||||
|
||||
// loop through spine items to get the correct index matching the text href
|
||||
for (size_t i = 0; i < getSpineItemsCount(); i++) {
|
||||
if (getSpineItem(i).href == bookMetadataCache->coreMetadata.textReferenceHref) {
|
||||
LOG_DBG("EBP", "Text reference %s found at index %d", bookMetadataCache->coreMetadata.textReferenceHref.c_str(),
|
||||
i);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
// This should not happen, as we checked for empty textReferenceHref earlier
|
||||
LOG_DBG("EBP", "Section not found for text reference");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calculate progress in book (returns 0.0-1.0)
|
||||
float Epub::calculateProgress(const int currentSpineIndex, const float currentSpineRead) const {
|
||||
const size_t bookSize = getBookSize();
|
||||
if (bookSize == 0) {
|
||||
return 0.0f;
|
||||
}
|
||||
const size_t prevChapterSize = (currentSpineIndex >= 1) ? getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
|
||||
const size_t curChapterSize = getCumulativeSpineItemSize(currentSpineIndex) - prevChapterSize;
|
||||
const float sectionProgSize = currentSpineRead * static_cast<float>(curChapterSize);
|
||||
const float totalProgress = static_cast<float>(prevChapterSize) + sectionProgSize;
|
||||
return totalProgress / static_cast<float>(bookSize);
|
||||
}
|
||||
|
||||
int Epub::resolveHrefToSpineIndex(const std::string& href) const {
|
||||
if (!bookMetadataCache || !bookMetadataCache->isLoaded()) return -1;
|
||||
|
||||
// Split before decoding so escaped '#' characters in filenames stay part of the path.
|
||||
const size_t hashPos = href.find('#');
|
||||
const std::string rawTarget = hashPos != std::string::npos ? href.substr(0, hashPos) : href;
|
||||
const std::string target = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(rawTarget));
|
||||
|
||||
// Same-file reference (anchor-only)
|
||||
if (target.empty()) return -1;
|
||||
|
||||
// Extract just the filename for comparison
|
||||
size_t targetSlash = target.find_last_of('/');
|
||||
std::string targetFilename = (targetSlash != std::string::npos) ? target.substr(targetSlash + 1) : target;
|
||||
|
||||
for (int i = 0; i < getSpineItemsCount(); i++) {
|
||||
const auto& spineHref = getSpineItem(i).href;
|
||||
// Try exact match first
|
||||
if (spineHref == target) return i;
|
||||
// Then filename-only match
|
||||
size_t spineSlash = spineHref.find_last_of('/');
|
||||
std::string spineFilename = (spineSlash != std::string::npos) ? spineHref.substr(spineSlash + 1) : spineHref;
|
||||
if (spineFilename == targetFilename) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Print.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub/BookMetadataCache.h"
|
||||
#include "Epub/css/CssParser.h"
|
||||
|
||||
class ZipFile;
|
||||
|
||||
class Epub {
|
||||
// the ncx file (EPUB 2)
|
||||
std::string tocNcxItem;
|
||||
// the nav file (EPUB 3)
|
||||
std::string tocNavItem;
|
||||
// where is the EPUBfile?
|
||||
std::string filepath;
|
||||
// the base path for items in the EPUB file
|
||||
std::string contentBasePath;
|
||||
// Uniq cache key based on filepath
|
||||
std::string cachePath;
|
||||
// Spine and TOC cache
|
||||
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 writeSpineEntries = true);
|
||||
bool parseTocNcxFile() const;
|
||||
bool parseTocNavFile() const;
|
||||
void discoverCssFilesFromZip();
|
||||
void parseCssFiles() const;
|
||||
|
||||
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(bool buildIfMissing = true, bool skipLoadingCss = false);
|
||||
bool clearCache() const;
|
||||
void setupCacheDir() const;
|
||||
const std::string& getCachePath() const;
|
||||
const std::string& getPath() const;
|
||||
const std::string& getTitle() const;
|
||||
const std::string& getAuthor() const;
|
||||
const std::string& getLanguage() const;
|
||||
std::string getCoverBmpPath(bool cropped = false) const;
|
||||
bool generateCoverBmp(bool cropped = false) const;
|
||||
std::string getThumbBmpPath() const;
|
||||
std::string getThumbBmpPath(int height) const;
|
||||
bool generateThumbBmp(int height) const;
|
||||
uint8_t* readItemContentsToBytes(const std::string& itemHref, size_t* size = nullptr,
|
||||
bool trailingNullByte = false) const;
|
||||
bool readItemContentsToStream(const std::string& itemHref, Print& out, size_t chunkSize) const;
|
||||
bool getItemSize(const std::string& itemHref, size_t* size) const;
|
||||
BookMetadataCache::SpineEntry getSpineItem(int spineIndex) const;
|
||||
BookMetadataCache::TocEntry getTocItem(int tocIndex) const;
|
||||
int getSpineItemsCount() const;
|
||||
int getTocItemsCount() const;
|
||||
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||
size_t getCumulativeSpineItemSize(int spineIndex) const;
|
||||
int getSpineIndexForTextReference() const;
|
||||
|
||||
size_t getBookSize() const;
|
||||
float calculateProgress(int currentSpineIndex, float currentSpineRead) const;
|
||||
CssParser* getCssParser() const { return cssParser.get(); }
|
||||
int resolveHrefToSpineIndex(const std::string& href) const;
|
||||
};
|
||||
@@ -1,460 +0,0 @@
|
||||
#include "BookMetadataCache.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
#include <Utf8.h>
|
||||
#include <ZipFile.h>
|
||||
|
||||
#include <deque>
|
||||
|
||||
#include "FsHelpers.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t BOOK_CACHE_VERSION = 8; // v8: TOC/book titles stored NFC-composed
|
||||
constexpr char bookBinFile[] = "/book.bin";
|
||||
constexpr char tmpSpineBinFile[] = "/spine.bin.tmp";
|
||||
constexpr char tmpTocBinFile[] = "/toc.bin.tmp";
|
||||
} // namespace
|
||||
|
||||
/* ============= WRITING / BUILDING FUNCTIONS ================ */
|
||||
|
||||
bool BookMetadataCache::beginWrite() {
|
||||
buildMode = true;
|
||||
spineCount = 0;
|
||||
tocCount = 0;
|
||||
LOG_DBG("BMC", "Entering write mode");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::beginContentOpfPass() {
|
||||
LOG_DBG("BMC", "Beginning content opf pass");
|
||||
|
||||
// Open spine file for writing
|
||||
return Storage.openFileForWrite("BMC", cachePath + tmpSpineBinFile, spineFile);
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endContentOpfPass() {
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
spineFile.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::beginTocPass() {
|
||||
LOG_DBG("BMC", "Beginning toc pass");
|
||||
|
||||
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
return false;
|
||||
}
|
||||
if (!Storage.openFileForWrite("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
spineFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||
spineHrefIndex.clear();
|
||||
spineHrefIndex.resize(spineCount);
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto entry = readSpineEntry(spineFile);
|
||||
SpineHrefIndexEntry idx;
|
||||
idx.hrefHash = fnvHash64(entry.href);
|
||||
idx.hrefLen = static_cast<uint16_t>(entry.href.size());
|
||||
idx.spineIndex = static_cast<int16_t>(i);
|
||||
spineHrefIndex[i] = idx;
|
||||
}
|
||||
std::sort(spineHrefIndex.begin(), spineHrefIndex.end(),
|
||||
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
|
||||
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
|
||||
});
|
||||
spineFile.seek(0);
|
||||
useSpineHrefIndex = true;
|
||||
LOG_DBG("BMC", "Using fast index for %d spine items", spineCount);
|
||||
} else {
|
||||
useSpineHrefIndex = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endTocPass() {
|
||||
// Explicit close() required: member variables persist beyond function scope
|
||||
tocFile.close();
|
||||
spineFile.close();
|
||||
|
||||
spineHrefIndex.clear();
|
||||
spineHrefIndex.shrink_to_fit();
|
||||
useSpineHrefIndex = false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::endWrite() {
|
||||
if (!buildMode) {
|
||||
LOG_DBG("BMC", "endWrite called but not in build mode");
|
||||
return false;
|
||||
}
|
||||
|
||||
buildMode = false;
|
||||
LOG_DBG("BMC", "Wrote %d spine, %d TOC entries", spineCount, tocCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMetadata& metadata) {
|
||||
// Open all three files, writing to meta, reading from spine and toc
|
||||
if (!Storage.openFileForWrite("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Storage.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) {
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
bookFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Storage.openFileForRead("BMC", cachePath + tmpTocBinFile, tocFile)) {
|
||||
// Explicit close() required: member variables persist beyond function scope
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr uint32_t headerASize =
|
||||
sizeof(BOOK_CACHE_VERSION) + /* LUT Offset */ sizeof(uint32_t) + sizeof(spineCount) + sizeof(tocCount);
|
||||
const uint32_t metadataSize = metadata.title.size() + metadata.author.size() + metadata.language.size() +
|
||||
metadata.coverItemHref.size() + metadata.textReferenceHref.size() +
|
||||
sizeof(uint32_t) * 5;
|
||||
const uint32_t lutSize = sizeof(uint32_t) * spineCount + sizeof(uint32_t) * tocCount;
|
||||
const uint32_t lutOffset = headerASize + metadataSize;
|
||||
|
||||
// Header A
|
||||
serialization::writePod(bookFile, BOOK_CACHE_VERSION);
|
||||
serialization::writePod(bookFile, lutOffset);
|
||||
serialization::writePod(bookFile, spineCount);
|
||||
serialization::writePod(bookFile, tocCount);
|
||||
// Metadata
|
||||
serialization::writeString(bookFile, metadata.title);
|
||||
serialization::writeString(bookFile, metadata.author);
|
||||
serialization::writeString(bookFile, metadata.language);
|
||||
serialization::writeString(bookFile, metadata.coverItemHref);
|
||||
serialization::writeString(bookFile, metadata.textReferenceHref);
|
||||
|
||||
// Loop through spine entries, writing LUT positions
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
uint32_t pos = spineFile.position();
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
serialization::writePod(bookFile, pos + lutOffset + lutSize);
|
||||
}
|
||||
|
||||
// Loop through toc entries, writing LUT positions
|
||||
tocFile.seek(0);
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
uint32_t pos = tocFile.position();
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
serialization::writePod(bookFile, pos + lutOffset + lutSize + static_cast<uint32_t>(spineFile.position()));
|
||||
}
|
||||
|
||||
// LUTs complete
|
||||
// Loop through spines from spine file matching up TOC indexes, calculating cumulative size and writing to book.bin
|
||||
|
||||
// Build spineIndex->tocIndex mapping in one pass (O(n) instead of O(n*m))
|
||||
std::deque<int16_t> spineToTocIndex(spineCount, -1);
|
||||
tocFile.seek(0);
|
||||
for (int j = 0; j < tocCount; j++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
if (tocEntry.spineIndex >= 0 && tocEntry.spineIndex < spineCount) {
|
||||
if (spineToTocIndex[tocEntry.spineIndex] == -1) {
|
||||
spineToTocIndex[tocEntry.spineIndex] = static_cast<int16_t>(j);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZipFile zip(epubPath);
|
||||
// Pre-open zip file to speed up size calculations
|
||||
if (!zip.open()) {
|
||||
LOG_ERR("BMC", "Could not open EPUB zip for size calculations");
|
||||
// Explicit close() required: member variables persist beyond function scope
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
return false;
|
||||
}
|
||||
// NOTE: We intentionally skip calling loadAllFileStatSlims() here.
|
||||
// For large EPUBs (2000+ chapters), pre-loading all ZIP central directory entries
|
||||
// into memory causes OOM crashes on ESP32-C3's limited ~380KB RAM.
|
||||
// Instead, for large books we use a one-pass batch lookup that scans the ZIP
|
||||
// central directory once and matches against spine targets using hash comparison.
|
||||
// This is O(n*log(m)) instead of O(n*m) while avoiding memory exhaustion.
|
||||
// See: https://github.com/crosspoint-reader/crosspoint-reader/issues/134
|
||||
|
||||
std::deque<uint32_t> spineSizes;
|
||||
bool useBatchSizes = false;
|
||||
|
||||
if (spineCount >= LARGE_SPINE_THRESHOLD) {
|
||||
LOG_DBG("BMC", "Using batch size lookup for %d spine items", spineCount);
|
||||
|
||||
std::deque<ZipFile::SizeTarget> targets;
|
||||
targets.resize(spineCount);
|
||||
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto entry = readSpineEntry(spineFile);
|
||||
std::string path = FsHelpers::normalisePath(entry.href);
|
||||
|
||||
ZipFile::SizeTarget t;
|
||||
t.hash = ZipFile::fnvHash64(path.c_str(), path.size());
|
||||
t.len = static_cast<uint16_t>(path.size());
|
||||
t.index = static_cast<uint16_t>(i);
|
||||
targets[i] = t;
|
||||
}
|
||||
|
||||
std::sort(targets.begin(), targets.end(), [](const ZipFile::SizeTarget& a, const ZipFile::SizeTarget& b) {
|
||||
return a.hash < b.hash || (a.hash == b.hash && a.len < b.len);
|
||||
});
|
||||
|
||||
spineSizes.resize(spineCount, 0);
|
||||
int matched = zip.fillUncompressedSizes(targets, spineSizes);
|
||||
LOG_DBG("BMC", "Batch lookup matched %d/%d spine items", matched, spineCount);
|
||||
|
||||
targets.clear();
|
||||
targets.shrink_to_fit();
|
||||
|
||||
useBatchSizes = true;
|
||||
}
|
||||
|
||||
uint32_t cumSize = 0;
|
||||
spineFile.seek(0);
|
||||
int lastSpineTocIndex = -1;
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
|
||||
spineEntry.tocIndex = spineToTocIndex[i];
|
||||
|
||||
// Not a huge deal if we don't fine a TOC entry for the spine entry, this is expected behaviour for EPUBs
|
||||
// Logging here is for debugging
|
||||
if (spineEntry.tocIndex == -1) {
|
||||
LOG_DBG("BMC", "Warning: Could not find TOC entry for spine item %d: %s, using title from last section", i,
|
||||
spineEntry.href.c_str());
|
||||
spineEntry.tocIndex = lastSpineTocIndex;
|
||||
}
|
||||
lastSpineTocIndex = spineEntry.tocIndex;
|
||||
|
||||
size_t itemSize = 0;
|
||||
if (useBatchSizes) {
|
||||
itemSize = spineSizes[i];
|
||||
if (itemSize == 0) {
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const std::string path = FsHelpers::normalisePath(spineEntry.href);
|
||||
if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) {
|
||||
LOG_ERR("BMC", "Warning: Could not get size for spine item: %s", path.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
cumSize += itemSize;
|
||||
spineEntry.cumulativeSize = cumSize;
|
||||
|
||||
// Write out spine data to book.bin
|
||||
writeSpineEntry(bookFile, spineEntry);
|
||||
}
|
||||
// Close opened zip file
|
||||
zip.close();
|
||||
|
||||
// Loop through toc entries from toc file writing to book.bin
|
||||
tocFile.seek(0);
|
||||
for (int i = 0; i < tocCount; i++) {
|
||||
auto tocEntry = readTocEntry(tocFile);
|
||||
writeTocEntry(bookFile, tocEntry);
|
||||
}
|
||||
|
||||
// Explicit close() required: member variables persist beyond function scope
|
||||
bookFile.close();
|
||||
spineFile.close();
|
||||
tocFile.close();
|
||||
|
||||
LOG_DBG("BMC", "Successfully built book.bin");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BookMetadataCache::cleanupTmpFiles() const {
|
||||
const auto spineBinFile = cachePath + tmpSpineBinFile;
|
||||
if (Storage.exists(spineBinFile.c_str())) {
|
||||
Storage.remove(spineBinFile.c_str());
|
||||
}
|
||||
const auto tocBinFile = cachePath + tmpTocBinFile;
|
||||
if (Storage.exists(tocBinFile.c_str())) {
|
||||
Storage.remove(tocBinFile.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t BookMetadataCache::writeSpineEntry(HalFile& file, const SpineEntry& entry) const {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writePod(file, entry.cumulativeSize);
|
||||
serialization::writePod(file, entry.tocIndex);
|
||||
return pos;
|
||||
}
|
||||
|
||||
uint32_t BookMetadataCache::writeTocEntry(HalFile& file, const TocEntry& entry) const {
|
||||
const uint32_t pos = file.position();
|
||||
serialization::writeString(file, entry.title);
|
||||
serialization::writeString(file, entry.href);
|
||||
serialization::writeString(file, entry.anchor);
|
||||
serialization::writePod(file, entry.level);
|
||||
serialization::writePod(file, entry.spineIndex);
|
||||
return pos;
|
||||
}
|
||||
|
||||
// Note: for the LUT to be accurate, this **MUST** be called for all spine items before `addTocEntry` is ever called
|
||||
// this is because in this function we're marking positions of the items
|
||||
void BookMetadataCache::createSpineEntry(const std::string& href) {
|
||||
if (!buildMode || !spineFile) {
|
||||
LOG_DBG("BMC", "createSpineEntry called but not in build mode");
|
||||
return;
|
||||
}
|
||||
|
||||
const SpineEntry entry(href, 0, -1);
|
||||
writeSpineEntry(spineFile, entry);
|
||||
spineCount++;
|
||||
}
|
||||
|
||||
void BookMetadataCache::createTocEntry(const std::string& title, const std::string& href, const std::string& anchor,
|
||||
const uint8_t level) {
|
||||
if (!buildMode || !tocFile || !spineFile) {
|
||||
LOG_DBG("BMC", "createTocEntry called but not in build mode");
|
||||
return;
|
||||
}
|
||||
|
||||
int16_t spineIndex = -1;
|
||||
|
||||
if (useSpineHrefIndex) {
|
||||
uint64_t targetHash = fnvHash64(href);
|
||||
uint16_t targetLen = static_cast<uint16_t>(href.size());
|
||||
|
||||
auto it =
|
||||
std::lower_bound(spineHrefIndex.begin(), spineHrefIndex.end(), SpineHrefIndexEntry{targetHash, targetLen, 0},
|
||||
[](const SpineHrefIndexEntry& a, const SpineHrefIndexEntry& b) {
|
||||
return a.hrefHash < b.hrefHash || (a.hrefHash == b.hrefHash && a.hrefLen < b.hrefLen);
|
||||
});
|
||||
|
||||
while (it != spineHrefIndex.end() && it->hrefHash == targetHash && it->hrefLen == targetLen) {
|
||||
spineIndex = it->spineIndex;
|
||||
break;
|
||||
}
|
||||
|
||||
if (spineIndex == -1) {
|
||||
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||
}
|
||||
} else {
|
||||
spineFile.seek(0);
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
auto spineEntry = readSpineEntry(spineFile);
|
||||
if (spineEntry.href == href) {
|
||||
spineIndex = static_cast<int16_t>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (spineIndex == -1) {
|
||||
LOG_DBG("BMC", "createTocEntry: Could not find spine item for TOC href %s", href.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Compose the title to NFC at index time so the cache stores precomposed glyphs;
|
||||
// device fonts have no combining-mark positioning, so NFD titles render broken.
|
||||
const TocEntry entry(utf8ComposeNfc(title), href, anchor, level, spineIndex);
|
||||
writeTocEntry(tocFile, entry);
|
||||
tocCount++;
|
||||
}
|
||||
|
||||
/* ============= READING / LOADING FUNCTIONS ================ */
|
||||
|
||||
bool BookMetadataCache::load() {
|
||||
if (!Storage.openFileForRead("BMC", cachePath + bookBinFile, bookFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t version;
|
||||
serialization::readPod(bookFile, version);
|
||||
if (version != BOOK_CACHE_VERSION) {
|
||||
LOG_DBG("BMC", "Cache version mismatch: expected %d, got %d", BOOK_CACHE_VERSION, version);
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
bookFile.close();
|
||||
return false;
|
||||
}
|
||||
|
||||
serialization::readPod(bookFile, lutOffset);
|
||||
serialization::readPod(bookFile, spineCount);
|
||||
serialization::readPod(bookFile, tocCount);
|
||||
|
||||
serialization::readString(bookFile, coreMetadata.title);
|
||||
serialization::readString(bookFile, coreMetadata.author);
|
||||
serialization::readString(bookFile, coreMetadata.language);
|
||||
serialization::readString(bookFile, coreMetadata.coverItemHref);
|
||||
serialization::readString(bookFile, coreMetadata.textReferenceHref);
|
||||
|
||||
loaded = true;
|
||||
LOG_DBG("BMC", "Loaded cache data: %d spine, %d TOC entries", spineCount, tocCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry BookMetadataCache::getSpineEntry(const int index) {
|
||||
if (!loaded) {
|
||||
LOG_ERR("BMC", "getSpineEntry called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index < 0 || index >= static_cast<int>(spineCount)) {
|
||||
LOG_ERR("BMC", "getSpineEntry index %d out of range", index);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Seek to spine LUT item, read from LUT and get out data
|
||||
bookFile.seek(lutOffset + sizeof(uint32_t) * index);
|
||||
uint32_t spineEntryPos;
|
||||
serialization::readPod(bookFile, spineEntryPos);
|
||||
bookFile.seek(spineEntryPos);
|
||||
return readSpineEntry(bookFile);
|
||||
}
|
||||
|
||||
BookMetadataCache::TocEntry BookMetadataCache::getTocEntry(const int index) {
|
||||
if (!loaded) {
|
||||
LOG_ERR("BMC", "getTocEntry called but cache not loaded");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (index < 0 || index >= static_cast<int>(tocCount)) {
|
||||
LOG_ERR("BMC", "getTocEntry index %d out of range", index);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Seek to TOC LUT item, read from LUT and get out data
|
||||
bookFile.seek(lutOffset + sizeof(uint32_t) * spineCount + sizeof(uint32_t) * index);
|
||||
uint32_t tocEntryPos;
|
||||
serialization::readPod(bookFile, tocEntryPos);
|
||||
bookFile.seek(tocEntryPos);
|
||||
return readTocEntry(bookFile);
|
||||
}
|
||||
|
||||
BookMetadataCache::SpineEntry BookMetadataCache::readSpineEntry(HalFile& file) const {
|
||||
SpineEntry entry;
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readPod(file, entry.cumulativeSize);
|
||||
serialization::readPod(file, entry.tocIndex);
|
||||
return entry;
|
||||
}
|
||||
|
||||
BookMetadataCache::TocEntry BookMetadataCache::readTocEntry(HalFile& file) const {
|
||||
TocEntry entry;
|
||||
serialization::readString(file, entry.title);
|
||||
serialization::readString(file, entry.href);
|
||||
serialization::readString(file, entry.anchor);
|
||||
serialization::readPod(file, entry.level);
|
||||
serialization::readPod(file, entry.spineIndex);
|
||||
return entry;
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
|
||||
class BookMetadataCache {
|
||||
public:
|
||||
struct BookMetadata {
|
||||
std::string title;
|
||||
std::string author;
|
||||
std::string language;
|
||||
std::string coverItemHref;
|
||||
std::string textReferenceHref;
|
||||
};
|
||||
|
||||
struct SpineEntry {
|
||||
std::string href;
|
||||
uint32_t cumulativeSize;
|
||||
int16_t tocIndex;
|
||||
|
||||
SpineEntry() : cumulativeSize(0), tocIndex(-1) {}
|
||||
SpineEntry(std::string href, const uint32_t cumulativeSize, const int16_t tocIndex)
|
||||
: href(std::move(href)), cumulativeSize(cumulativeSize), tocIndex(tocIndex) {}
|
||||
};
|
||||
|
||||
struct TocEntry {
|
||||
std::string title;
|
||||
std::string href;
|
||||
std::string anchor;
|
||||
uint8_t level;
|
||||
int16_t spineIndex;
|
||||
|
||||
TocEntry() : level(0), spineIndex(-1) {}
|
||||
TocEntry(std::string title, std::string href, std::string anchor, const uint8_t level, const int16_t spineIndex)
|
||||
: title(std::move(title)),
|
||||
href(std::move(href)),
|
||||
anchor(std::move(anchor)),
|
||||
level(level),
|
||||
spineIndex(spineIndex) {}
|
||||
};
|
||||
|
||||
private:
|
||||
std::string cachePath;
|
||||
uint32_t lutOffset;
|
||||
uint16_t spineCount;
|
||||
uint16_t tocCount;
|
||||
bool loaded;
|
||||
bool buildMode;
|
||||
|
||||
HalFile bookFile;
|
||||
// Temp file handles during build
|
||||
HalFile spineFile;
|
||||
HalFile tocFile;
|
||||
|
||||
// Index for fast href→spineIndex lookup (used only for large EPUBs)
|
||||
struct SpineHrefIndexEntry {
|
||||
uint64_t hrefHash; // FNV-1a 64-bit hash
|
||||
uint16_t hrefLen; // length for collision reduction
|
||||
int16_t spineIndex;
|
||||
};
|
||||
std::deque<SpineHrefIndexEntry> spineHrefIndex;
|
||||
bool useSpineHrefIndex = false;
|
||||
|
||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||
|
||||
// FNV-1a 64-bit hash function
|
||||
static uint64_t fnvHash64(const std::string& s) {
|
||||
uint64_t hash = 14695981039346656037ull;
|
||||
for (char c : s) {
|
||||
hash ^= static_cast<uint8_t>(c);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
uint32_t writeSpineEntry(HalFile& file, const SpineEntry& entry) const;
|
||||
uint32_t writeTocEntry(HalFile& file, const TocEntry& entry) const;
|
||||
SpineEntry readSpineEntry(HalFile& file) const;
|
||||
TocEntry readTocEntry(HalFile& file) const;
|
||||
|
||||
public:
|
||||
BookMetadata coreMetadata;
|
||||
|
||||
explicit BookMetadataCache(std::string cachePath)
|
||||
: cachePath(std::move(cachePath)), lutOffset(0), spineCount(0), tocCount(0), loaded(false), buildMode(false) {}
|
||||
~BookMetadataCache() = default;
|
||||
|
||||
// Building phase (stream to disk immediately)
|
||||
bool beginWrite();
|
||||
bool beginContentOpfPass();
|
||||
void createSpineEntry(const std::string& href);
|
||||
bool endContentOpfPass();
|
||||
bool beginTocPass();
|
||||
void createTocEntry(const std::string& title, const std::string& href, const std::string& anchor, uint8_t level);
|
||||
bool endTocPass();
|
||||
bool endWrite();
|
||||
bool cleanupTmpFiles() const;
|
||||
|
||||
// Post-processing to update mappings and sizes
|
||||
bool buildBookBin(const std::string& epubPath, const BookMetadata& metadata);
|
||||
|
||||
// Reading phase (read mode)
|
||||
bool load();
|
||||
SpineEntry getSpineEntry(int index);
|
||||
TocEntry getTocEntry(int index);
|
||||
int getSpineCount() const { return spineCount; }
|
||||
int getTocCount() const { return tocCount; }
|
||||
bool isLoaded() const { return loaded; }
|
||||
};
|
||||
@@ -1,210 +0,0 @@
|
||||
#include "Page.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <new>
|
||||
|
||||
namespace {
|
||||
|
||||
template <typename Predicate>
|
||||
void renderFilteredPageElements(const std::vector<std::shared_ptr<PageElement>>& elements, GfxRenderer& renderer,
|
||||
const int fontId, const int xOffset, const int yOffset, Predicate&& predicate) {
|
||||
for (const auto& element : elements) {
|
||||
if (predicate(*element)) {
|
||||
element->render(renderer, fontId, xOffset, yOffset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void PageLine::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
block->render(renderer, fontId, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
|
||||
bool PageLine::serialize(HalFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
|
||||
// serialize TextBlock pointed to by PageLine
|
||||
return block->serialize(file);
|
||||
}
|
||||
|
||||
std::unique_ptr<PageLine> PageLine::deserialize(HalFile& file) {
|
||||
int16_t xPos;
|
||||
int16_t yPos;
|
||||
serialization::readPod(file, xPos);
|
||||
serialization::readPod(file, yPos);
|
||||
|
||||
auto tb = TextBlock::deserialize(file);
|
||||
if (!tb) {
|
||||
LOG_ERR("PGE", "Deserialization failed: null TextBlock");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* line = new (std::nothrow) PageLine(std::move(tb), xPos, yPos);
|
||||
if (!line) {
|
||||
LOG_ERR("PGE", "Deserialization failed: could not allocate PageLine");
|
||||
return nullptr;
|
||||
}
|
||||
return std::unique_ptr<PageLine>(line);
|
||||
}
|
||||
|
||||
void PageImage::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
// Images don't use fontId or text rendering
|
||||
imageBlock->render(renderer, xPos + xOffset, yPos + yOffset);
|
||||
}
|
||||
|
||||
bool PageImage::serialize(HalFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
|
||||
// serialize ImageBlock
|
||||
return imageBlock->serialize(file);
|
||||
}
|
||||
|
||||
std::unique_ptr<PageImage> PageImage::deserialize(HalFile& file) {
|
||||
int16_t xPos;
|
||||
int16_t yPos;
|
||||
serialization::readPod(file, xPos);
|
||||
serialization::readPod(file, yPos);
|
||||
|
||||
auto ib = ImageBlock::deserialize(file);
|
||||
return std::unique_ptr<PageImage>(new PageImage(std::move(ib), xPos, yPos));
|
||||
}
|
||||
|
||||
void PageHorizontalRule::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) {
|
||||
(void)fontId;
|
||||
if (width == 0 || thickness == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.drawLine(xPos + xOffset, yPos + yOffset, xPos + xOffset + width - 1, yPos + yOffset, thickness, true);
|
||||
}
|
||||
|
||||
bool PageHorizontalRule::serialize(HalFile& file) {
|
||||
serialization::writePod(file, xPos);
|
||||
serialization::writePod(file, yPos);
|
||||
serialization::writePod(file, width);
|
||||
serialization::writePod(file, thickness);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<PageHorizontalRule> PageHorizontalRule::deserialize(HalFile& file) {
|
||||
int16_t xPos = 0;
|
||||
int16_t yPos = 0;
|
||||
uint16_t width = 0;
|
||||
uint8_t thickness = 0;
|
||||
serialization::readPod(file, xPos);
|
||||
serialization::readPod(file, yPos);
|
||||
serialization::readPod(file, width);
|
||||
serialization::readPod(file, thickness);
|
||||
|
||||
if (width == 0 || thickness == 0) {
|
||||
LOG_ERR("PGE", "Deserialization failed: invalid horizontal rule metadata (width=%u thickness=%u)", width,
|
||||
thickness);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto* rule = new (std::nothrow) PageHorizontalRule(width, thickness, xPos, yPos);
|
||||
if (!rule) {
|
||||
LOG_ERR("PGE", "Deserialization failed: could not allocate PageHorizontalRule");
|
||||
return nullptr;
|
||||
}
|
||||
return std::unique_ptr<PageHorizontalRule>(rule);
|
||||
}
|
||||
|
||||
void Page::render(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||
renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset, [](const PageElement&) { return true; });
|
||||
}
|
||||
|
||||
void Page::renderImages(GfxRenderer& renderer, const int fontId, const int xOffset, const int yOffset) const {
|
||||
renderFilteredPageElements(elements, renderer, fontId, xOffset, yOffset,
|
||||
[](const PageElement& element) { return element.getTag() == TAG_PageImage; });
|
||||
}
|
||||
|
||||
bool Page::serialize(HalFile& file) const {
|
||||
const uint16_t count = elements.size();
|
||||
serialization::writePod(file, count);
|
||||
|
||||
for (const auto& el : elements) {
|
||||
// Use getTag() method to determine type
|
||||
serialization::writePod(file, static_cast<uint8_t>(el->getTag()));
|
||||
|
||||
if (!el->serialize(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize footnotes (clamp to MAX_FOOTNOTES_PER_PAGE to match addFootnote/deserialize limits)
|
||||
const uint16_t fnCount = std::min<uint16_t>(footnotes.size(), MAX_FOOTNOTES_PER_PAGE);
|
||||
serialization::writePod(file, fnCount);
|
||||
for (uint16_t i = 0; i < fnCount; i++) {
|
||||
const auto& fn = footnotes[i];
|
||||
if (file.write(fn.number, sizeof(fn.number)) != sizeof(fn.number) ||
|
||||
file.write(fn.href, sizeof(fn.href)) != sizeof(fn.href)) {
|
||||
LOG_ERR("PGE", "Failed to write footnote");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<Page> Page::deserialize(HalFile& file) {
|
||||
auto page = std::unique_ptr<Page>(new Page());
|
||||
|
||||
uint16_t count;
|
||||
serialization::readPod(file, count);
|
||||
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
uint8_t tag;
|
||||
serialization::readPod(file, tag);
|
||||
|
||||
if (tag == TAG_PageLine) {
|
||||
auto pl = PageLine::deserialize(file);
|
||||
if (!pl) {
|
||||
return nullptr;
|
||||
}
|
||||
page->elements.push_back(std::move(pl));
|
||||
} else if (tag == TAG_PageImage) {
|
||||
auto pi = PageImage::deserialize(file);
|
||||
if (!pi) {
|
||||
return nullptr;
|
||||
}
|
||||
page->elements.push_back(std::move(pi));
|
||||
} else if (tag == TAG_PageHorizontalRule) {
|
||||
auto rule = PageHorizontalRule::deserialize(file);
|
||||
if (!rule) {
|
||||
return nullptr;
|
||||
}
|
||||
page->elements.push_back(std::move(rule));
|
||||
} else {
|
||||
LOG_ERR("PGE", "Deserialization failed: Unknown tag %u", tag);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialize footnotes
|
||||
uint16_t fnCount;
|
||||
serialization::readPod(file, fnCount);
|
||||
if (fnCount > MAX_FOOTNOTES_PER_PAGE) {
|
||||
LOG_ERR("PGE", "Invalid footnote count %u", fnCount);
|
||||
return nullptr;
|
||||
}
|
||||
page->footnotes.resize(fnCount);
|
||||
for (uint16_t i = 0; i < fnCount; i++) {
|
||||
auto& entry = page->footnotes[i];
|
||||
if (file.read(entry.number, sizeof(entry.number)) != sizeof(entry.number) ||
|
||||
file.read(entry.href, sizeof(entry.href)) != sizeof(entry.href)) {
|
||||
LOG_ERR("PGE", "Failed to read footnote %u", i);
|
||||
return nullptr;
|
||||
}
|
||||
entry.number[sizeof(entry.number) - 1] = '\0';
|
||||
entry.href[sizeof(entry.href) - 1] = '\0';
|
||||
}
|
||||
|
||||
return page;
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
#pragma once
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "FootnoteEntry.h"
|
||||
#include "blocks/ImageBlock.h"
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
enum PageElementTag : uint8_t {
|
||||
TAG_PageLine = 1,
|
||||
TAG_PageImage = 2,
|
||||
TAG_PageHorizontalRule = 3,
|
||||
};
|
||||
|
||||
// represents something that has been added to a page
|
||||
class PageElement {
|
||||
public:
|
||||
int16_t xPos;
|
||||
int16_t yPos;
|
||||
explicit PageElement(const int16_t xPos, const int16_t yPos) : xPos(xPos), yPos(yPos) {}
|
||||
virtual ~PageElement() = default;
|
||||
virtual void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) = 0;
|
||||
virtual bool serialize(HalFile& file) = 0;
|
||||
virtual PageElementTag getTag() const = 0; // Add type identification
|
||||
};
|
||||
|
||||
// a line from a block element
|
||||
class PageLine final : public PageElement {
|
||||
std::shared_ptr<TextBlock> block;
|
||||
|
||||
public:
|
||||
PageLine(std::shared_ptr<TextBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), block(std::move(block)) {}
|
||||
const std::shared_ptr<TextBlock>& getBlock() const { return block; }
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(HalFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageLine; }
|
||||
static std::unique_ptr<PageLine> deserialize(HalFile& file);
|
||||
};
|
||||
|
||||
// New PageImage class
|
||||
class PageImage final : public PageElement {
|
||||
std::shared_ptr<ImageBlock> imageBlock;
|
||||
|
||||
public:
|
||||
PageImage(std::shared_ptr<ImageBlock> block, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), imageBlock(std::move(block)) {}
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(HalFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageImage; }
|
||||
static std::unique_ptr<PageImage> deserialize(HalFile& file);
|
||||
const ImageBlock& getImageBlock() const { return *imageBlock; }
|
||||
};
|
||||
|
||||
class PageHorizontalRule final : public PageElement {
|
||||
uint16_t width;
|
||||
uint8_t thickness;
|
||||
|
||||
public:
|
||||
PageHorizontalRule(uint16_t width, uint8_t thickness, const int16_t xPos, const int16_t yPos)
|
||||
: PageElement(xPos, yPos), width(width), thickness(thickness) {}
|
||||
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) override;
|
||||
bool serialize(HalFile& file) override;
|
||||
PageElementTag getTag() const override { return TAG_PageHorizontalRule; }
|
||||
static std::unique_ptr<PageHorizontalRule> deserialize(HalFile& file);
|
||||
};
|
||||
|
||||
class Page {
|
||||
public:
|
||||
// the list of block index and line numbers on this page
|
||||
std::vector<std::shared_ptr<PageElement>> elements;
|
||||
std::vector<FootnoteEntry> footnotes;
|
||||
static constexpr uint16_t MAX_FOOTNOTES_PER_PAGE = 16;
|
||||
|
||||
void addFootnote(const char* number, const char* href) {
|
||||
if (footnotes.size() >= MAX_FOOTNOTES_PER_PAGE) return; // Cap per-page footnotes
|
||||
FootnoteEntry entry;
|
||||
strncpy(entry.number, number, sizeof(entry.number) - 1);
|
||||
entry.number[sizeof(entry.number) - 1] = '\0';
|
||||
strncpy(entry.href, href, sizeof(entry.href) - 1);
|
||||
entry.href[sizeof(entry.href) - 1] = '\0';
|
||||
footnotes.push_back(entry);
|
||||
}
|
||||
|
||||
void render(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
void renderImages(GfxRenderer& renderer, int fontId, int xOffset, int yOffset) const;
|
||||
bool serialize(HalFile& file) const;
|
||||
static std::unique_ptr<Page> deserialize(HalFile& file);
|
||||
|
||||
// Check if page contains any images (used to force full refresh)
|
||||
bool hasImages() const {
|
||||
return std::any_of(elements.begin(), elements.end(),
|
||||
[](const std::shared_ptr<PageElement>& el) { return el->getTag() == TAG_PageImage; });
|
||||
}
|
||||
|
||||
// Get bounding box of all images on the page (union of image rects)
|
||||
// Returns false if no images. Coordinates are relative to page origin.
|
||||
bool getImageBoundingBox(int16_t& outX, int16_t& outY, int16_t& outW, int16_t& outH) const {
|
||||
bool found = false;
|
||||
int16_t minX = INT16_MAX, minY = INT16_MAX, maxX = INT16_MIN, maxY = INT16_MIN;
|
||||
for (const auto& el : elements) {
|
||||
if (el->getTag() == TAG_PageImage) {
|
||||
const auto& img = static_cast<const PageImage&>(*el);
|
||||
int16_t x = img.xPos;
|
||||
int16_t y = img.yPos;
|
||||
int16_t right = x + img.getImageBlock().getWidth();
|
||||
int16_t bottom = y + img.getImageBlock().getHeight();
|
||||
minX = std::min(minX, x);
|
||||
minY = std::min(minY, y);
|
||||
maxX = std::max(maxX, right);
|
||||
maxY = std::max(maxY, bottom);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (found) {
|
||||
outX = minX;
|
||||
outY = minY;
|
||||
outW = maxX - minX;
|
||||
outH = maxY - minY;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,70 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <EpdFontFamily.h>
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "blocks/BlockStyle.h"
|
||||
#include "blocks/TextBlock.h"
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
class ParsedText {
|
||||
std::vector<std::string> words;
|
||||
std::vector<EpdFontFamily::Style> wordStyles;
|
||||
std::vector<bool> wordContinues; // true = word attaches to previous with no break
|
||||
std::vector<bool> wordNoSpaceBefore; // true = may break before token, but no synthetic space when joined
|
||||
std::vector<bool> wordIsFocusSuffix; // true = token is the regular tail of a focus bold-prefix split
|
||||
BlockStyle blockStyle;
|
||||
bool extraParagraphSpacing;
|
||||
bool hyphenationEnabled;
|
||||
bool focusReadingEnabled;
|
||||
bool isNaturalAlign;
|
||||
bool hasRtlWord;
|
||||
std::vector<std::string> reorderedWordsScratch;
|
||||
std::vector<EpdFontFamily::Style> reorderedStylesScratch;
|
||||
std::vector<uint16_t> reorderedWidthsScratch;
|
||||
std::vector<bool> reorderedContinuesScratch;
|
||||
std::vector<bool> reorderedNoSpaceBeforeScratch;
|
||||
std::vector<bool> reorderedFocusSuffixScratch;
|
||||
std::vector<uint16_t> visualOrderScratch;
|
||||
|
||||
int resolveFirstLineIndent(bool isFirstLine, const GfxRenderer& renderer, int fontId) const;
|
||||
std::vector<size_t> computeLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||
std::vector<bool>& noSpaceBeforeVec);
|
||||
std::vector<size_t> computeHyphenatedLineBreaks(const GfxRenderer& renderer, int fontId, int pageWidth,
|
||||
std::vector<uint16_t>& wordWidths, std::vector<bool>& continuesVec,
|
||||
std::vector<bool>& noSpaceBeforeVec);
|
||||
bool hyphenateWordAtIndex(size_t wordIndex, int availableWidth, const GfxRenderer& renderer, int fontId,
|
||||
std::vector<uint16_t>& wordWidths, bool allowFallbackBreaks);
|
||||
void extractLine(size_t breakIndex, int pageWidth, const std::vector<uint16_t>& wordWidths,
|
||||
const std::vector<bool>& continuesVec, const std::vector<bool>& noSpaceBeforeVec,
|
||||
const std::vector<size_t>& lineBreakIndices,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine, const GfxRenderer& renderer,
|
||||
int fontId);
|
||||
std::vector<uint16_t> calculateWordWidths(const GfxRenderer& renderer, int fontId);
|
||||
|
||||
public:
|
||||
explicit ParsedText(const bool extraParagraphSpacing, const bool hyphenationEnabled = false,
|
||||
const bool focusReadingEnabled = false, const BlockStyle& blockStyle = BlockStyle())
|
||||
: blockStyle(blockStyle),
|
||||
extraParagraphSpacing(extraParagraphSpacing),
|
||||
hyphenationEnabled(hyphenationEnabled),
|
||||
focusReadingEnabled(focusReadingEnabled),
|
||||
isNaturalAlign(false),
|
||||
hasRtlWord(false) {}
|
||||
~ParsedText() = default;
|
||||
|
||||
void addWord(std::string word, EpdFontFamily::Style fontStyle, bool underline = false, bool attachToPrevious = false);
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = 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,
|
||||
const std::function<void(std::shared_ptr<TextBlock>)>& processLine,
|
||||
bool includeLastLine = true);
|
||||
};
|
||||
@@ -1,926 +0,0 @@
|
||||
#include "Section.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Memory.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "Epub/css/CssParser.h"
|
||||
#include "Page.h"
|
||||
#include "hyphenation/Hyphenator.h"
|
||||
#include "parsers/ChapterHtmlSlimParser.h"
|
||||
|
||||
namespace {
|
||||
// v29: TextBlock word data stored as one flat arena (offset table + NUL-terminated
|
||||
// text blob) instead of length-prefixed strings and per-field arrays.
|
||||
constexpr uint8_t SECTION_FILE_VERSION = 29;
|
||||
// Written into the version field while a build is in progress; patched to
|
||||
// SECTION_FILE_VERSION only when the build is finalized. An abandoned /
|
||||
// crash-interrupted .bin therefore carries version 0, which loadSectionFile rejects
|
||||
// as unknown and clears -- so an incomplete file is never mistaken for a valid one.
|
||||
constexpr uint8_t SECTION_FILE_INCOMPLETE_VERSION = 0;
|
||||
// Written when a build is suspended partway (reader exited or device slept mid-build).
|
||||
// The file carries valid pages 0..pageCount-1, all LUTs, and a trailer with the parse
|
||||
// watermark (bytesConsumed, totalBytes) appended after the li LUT. loadSectionFile
|
||||
// accepts it so a resume shows those pages instantly; the reader extends it by
|
||||
// rebuilding in the background. Uses the same header layout as SECTION_FILE_VERSION,
|
||||
// so finalized files are untouched by this feature; older firmware treats the sentinel
|
||||
// as an unknown version and rebuilds, which is a safe downgrade.
|
||||
// MUST change in lockstep with SECTION_FILE_VERSION: the sentinel IS the partial's
|
||||
// format version, so a stale-format partial otherwise passes the header check and
|
||||
// only fails (noisily, via the block-decode error path) when a page is loaded.
|
||||
// Derived so the pairing can't be forgotten: 0xFE for v28, 0xFD for v29, ...
|
||||
constexpr uint8_t SECTION_FILE_PARTIAL_VERSION = 0xFE - (SECTION_FILE_VERSION - 28);
|
||||
constexpr uint32_t HEADER_SIZE = sizeof(uint8_t) + sizeof(int) + sizeof(float) + sizeof(bool) + sizeof(uint8_t) +
|
||||
sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint16_t) + sizeof(bool) + sizeof(bool) +
|
||||
sizeof(uint8_t) + sizeof(bool) + sizeof(uint32_t) + sizeof(uint32_t) +
|
||||
sizeof(uint32_t) + sizeof(uint32_t);
|
||||
} // namespace
|
||||
|
||||
// Out-of-line so the unique_ptr<ChapterHtmlSlimParser> in BuildContext can be
|
||||
// constructed/destroyed where the parser's full definition is visible.
|
||||
Section::Section(const std::shared_ptr<Epub>& epub, const int spineIndex, GfxRenderer& renderer)
|
||||
: epub(epub),
|
||||
spineIndex(spineIndex),
|
||||
renderer(renderer),
|
||||
filePath(epub->getCachePath() + "/sections/" + std::to_string(spineIndex) + ".bin") {}
|
||||
|
||||
// Suspend any in-progress build so every section.reset() / navigation / sleep path
|
||||
// persists the pages already laid out as a partial .bin instead of discarding them
|
||||
// (no-op once a build has completed or never started).
|
||||
Section::~Section() { suspendBuild(); }
|
||||
|
||||
uint32_t Section::onPageComplete(std::unique_ptr<Page> page) {
|
||||
if (!file) {
|
||||
LOG_ERR("SCT", "File not open for writing page %d", builtPageCount_);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const uint32_t position = file.position();
|
||||
if (!page->serialize(file)) {
|
||||
LOG_ERR("SCT", "Failed to serialize page %d", builtPageCount_);
|
||||
return 0;
|
||||
}
|
||||
LOG_DBG("SCT", "Page %d processed", builtPageCount_);
|
||||
|
||||
builtPageCount_++;
|
||||
// pageCount is the pages available to read: a rebuild over a partial only raises it
|
||||
// once it has laid out more pages than the partial already covers.
|
||||
if (builtPageCount_ > pageCount) {
|
||||
pageCount = builtPageCount_;
|
||||
}
|
||||
return position;
|
||||
}
|
||||
|
||||
void Section::writeSectionFileHeader(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const bool embeddedStyle, const uint8_t imageRendering,
|
||||
const bool focusReadingEnabled) {
|
||||
if (!file) {
|
||||
LOG_DBG("SCT", "File not open for writing header");
|
||||
return;
|
||||
}
|
||||
static_assert(HEADER_SIZE == sizeof(SECTION_FILE_VERSION) + sizeof(fontId) + sizeof(lineCompression) +
|
||||
sizeof(extraParagraphSpacing) + sizeof(paragraphAlignment) + sizeof(viewportWidth) +
|
||||
sizeof(viewportHeight) + sizeof(pageCount) + sizeof(hyphenationEnabled) +
|
||||
sizeof(embeddedStyle) + sizeof(imageRendering) + sizeof(focusReadingEnabled) +
|
||||
sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t) + sizeof(uint32_t),
|
||||
"Header size mismatch");
|
||||
// Written as the incomplete sentinel; finalizeBuild() patches it to
|
||||
// SECTION_FILE_VERSION as the last step, committing the file.
|
||||
serialization::writePod(file, SECTION_FILE_INCOMPLETE_VERSION);
|
||||
serialization::writePod(file, fontId);
|
||||
serialization::writePod(file, lineCompression);
|
||||
serialization::writePod(file, extraParagraphSpacing);
|
||||
serialization::writePod(file, paragraphAlignment);
|
||||
serialization::writePod(file, viewportWidth);
|
||||
serialization::writePod(file, viewportHeight);
|
||||
serialization::writePod(file, hyphenationEnabled);
|
||||
serialization::writePod(file, embeddedStyle);
|
||||
serialization::writePod(file, imageRendering);
|
||||
serialization::writePod(file, focusReadingEnabled);
|
||||
serialization::writePod(file, pageCount); // Placeholder for page count (will be initially 0, patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for LUT offset (patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for anchor map offset (patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for paragraph LUT offset (patched later)
|
||||
serialization::writePod(file, static_cast<uint32_t>(0)); // Placeholder for li LUT offset (patched later)
|
||||
}
|
||||
|
||||
bool Section::loadSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const uint8_t imageRendering, const bool focusReadingEnabled) {
|
||||
if (!Storage.openFileForRead("SCT", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match parameters
|
||||
bool filePartial = false;
|
||||
{
|
||||
uint8_t version;
|
||||
serialization::readPod(file, version);
|
||||
if (version != SECTION_FILE_VERSION && version != SECTION_FILE_PARTIAL_VERSION) {
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
file.close();
|
||||
LOG_ERR("SCT", "Deserialization failed: Unknown version %u", version);
|
||||
clearCache();
|
||||
return false;
|
||||
}
|
||||
filePartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
||||
|
||||
int fileFontId;
|
||||
uint16_t fileViewportWidth, fileViewportHeight;
|
||||
float fileLineCompression;
|
||||
bool fileExtraParagraphSpacing;
|
||||
uint8_t fileParagraphAlignment;
|
||||
bool fileHyphenationEnabled;
|
||||
bool fileEmbeddedStyle;
|
||||
uint8_t fileImageRendering;
|
||||
bool fileFocusReadingEnabled;
|
||||
serialization::readPod(file, fileFontId);
|
||||
serialization::readPod(file, fileLineCompression);
|
||||
serialization::readPod(file, fileExtraParagraphSpacing);
|
||||
serialization::readPod(file, fileParagraphAlignment);
|
||||
serialization::readPod(file, fileViewportWidth);
|
||||
serialization::readPod(file, fileViewportHeight);
|
||||
serialization::readPod(file, fileHyphenationEnabled);
|
||||
serialization::readPod(file, fileEmbeddedStyle);
|
||||
serialization::readPod(file, fileImageRendering);
|
||||
serialization::readPod(file, fileFocusReadingEnabled);
|
||||
|
||||
if (fontId != fileFontId || lineCompression != fileLineCompression ||
|
||||
extraParagraphSpacing != fileExtraParagraphSpacing || paragraphAlignment != fileParagraphAlignment ||
|
||||
viewportWidth != fileViewportWidth || viewportHeight != fileViewportHeight ||
|
||||
hyphenationEnabled != fileHyphenationEnabled || embeddedStyle != fileEmbeddedStyle ||
|
||||
imageRendering != fileImageRendering || focusReadingEnabled != fileFocusReadingEnabled) {
|
||||
file.close();
|
||||
LOG_ERR("SCT", "Deserialization failed: Parameters do not match");
|
||||
clearCache();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
serialization::readPod(file, pageCount);
|
||||
|
||||
if (filePartial) {
|
||||
// A partial's pageCount is the watermark of a suspended build. Read the watermark
|
||||
// trailer (appended after the li LUT) so estimatedTotalPages can extrapolate.
|
||||
uint32_t liLutOffset = 0;
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
serialization::readPod(file, liLutOffset);
|
||||
const uint32_t trailerOffset = liLutOffset + static_cast<uint32_t>(pageCount) * sizeof(uint16_t);
|
||||
const bool trailerValid =
|
||||
pageCount > 0 && liLutOffset >= HEADER_SIZE && trailerOffset + 2 * sizeof(uint32_t) <= file.size();
|
||||
if (!trailerValid) {
|
||||
file.close();
|
||||
LOG_ERR("SCT", "Deserialization failed: malformed partial section");
|
||||
clearCache();
|
||||
pageCount = 0;
|
||||
return false;
|
||||
}
|
||||
file.seek(trailerOffset);
|
||||
serialization::readPod(file, partialBytesConsumed_);
|
||||
serialization::readPod(file, partialTotalBytes_);
|
||||
partial_ = true;
|
||||
partialPageCount_ = pageCount;
|
||||
}
|
||||
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
file.close();
|
||||
LOG_DBG("SCT", "Deserialization succeeded: %d pages%s", pageCount, filePartial ? " (partial)" : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Your updated class method (assuming you are using the 'SD' object, which is a wrapper for a specific filesystem)
|
||||
bool Section::clearCache() const {
|
||||
const std::string tmpBin = binTmpPath();
|
||||
if (Storage.exists(tmpBin.c_str())) {
|
||||
Storage.remove(tmpBin.c_str());
|
||||
}
|
||||
if (!Storage.exists(filePath.c_str())) {
|
||||
LOG_DBG("SCT", "Cache does not exist, no action needed");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!Storage.remove(filePath.c_str())) {
|
||||
LOG_ERR("SCT", "Failed to clear cache");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("SCT", "Cache cleared successfully");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Section::createSectionFile(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled, const bool embeddedStyle,
|
||||
const uint8_t imageRendering, const bool focusReadingEnabled,
|
||||
const std::function<void()>& popupFn) {
|
||||
// One-shot build: start, then lay out the whole section in a single pass.
|
||||
if (!startBuild(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth, viewportHeight,
|
||||
hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled, popupFn)) {
|
||||
return false;
|
||||
}
|
||||
if (!buildSomeMore(0)) { // 0 = build to completion
|
||||
return false;
|
||||
}
|
||||
return buildComplete_;
|
||||
}
|
||||
|
||||
bool Section::startBuild(const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth, const uint16_t viewportHeight,
|
||||
const bool hyphenationEnabled, const bool embeddedStyle, const uint8_t imageRendering,
|
||||
const bool focusReadingEnabled, const std::function<void()>& popupFn) {
|
||||
if (build_) {
|
||||
LOG_ERR("SCT", "startBuild called while a build is already active");
|
||||
return false;
|
||||
}
|
||||
buildComplete_ = false;
|
||||
builtPageCount_ = 0;
|
||||
// Pages from a loaded partial stay readable (from filePath) while this build writes
|
||||
// to the tmp .bin, so availability never drops below the partial's watermark.
|
||||
pageCount = partial_ ? partialPageCount_ : 0;
|
||||
|
||||
// Remove a stale tmp .bin from a crash-interrupted build; this build recreates it.
|
||||
{
|
||||
const std::string staleTmp = binTmpPath();
|
||||
if (Storage.exists(staleTmp.c_str())) {
|
||||
Storage.remove(staleTmp.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const auto localPath = epub->getSpineItem(spineIndex).href;
|
||||
const auto htmlDir = epub->getCachePath() + "/html";
|
||||
const auto htmlPath = htmlDir + "/" + std::to_string(spineIndex) + ".html";
|
||||
const auto tmpHtmlPath = htmlDir + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||
|
||||
// Create cache directory if it doesn't exist
|
||||
{
|
||||
const auto sectionsDir = epub->getCachePath() + "/sections";
|
||||
Storage.mkdir(sectionsDir.c_str());
|
||||
}
|
||||
|
||||
// Reuse the previously unzipped HTML if we already have it. The unzipped HTML is keyed only on the
|
||||
// book (it lives in the per-book cache dir), not on render settings, so it survives the invalidation
|
||||
// that wipes the layout (.bin) caches when font/margin/orientation change -- rebuilds then skip zip
|
||||
// inflation entirely. It's promoted by an atomic rename as soon as the inflate succeeds (below), so
|
||||
// even a window-only giant spine -- whose .bin never finalizes -- still caches its HTML, letting a
|
||||
// reopen skip the multi-second inflate. If htmlPath exists it is known-complete.
|
||||
const bool reusedHtml = Storage.exists(htmlPath.c_str());
|
||||
bool htmlCached = reusedHtml;
|
||||
if (reusedHtml) {
|
||||
LOG_DBG("SCT", "Reusing cached HTML %s", htmlPath.c_str());
|
||||
} else {
|
||||
Storage.mkdir(htmlDir.c_str());
|
||||
|
||||
// Retry logic for SD card timing issues
|
||||
bool streamed = false;
|
||||
uint32_t fileSize = 0;
|
||||
for (int attempt = 0; attempt < 3 && !streamed; attempt++) {
|
||||
if (attempt > 0) {
|
||||
LOG_DBG("SCT", "Retrying stream (attempt %d)...", attempt + 1);
|
||||
delay(50); // Brief delay before retry
|
||||
}
|
||||
|
||||
// Remove any incomplete file from previous attempt before retrying
|
||||
if (Storage.exists(tmpHtmlPath.c_str())) {
|
||||
Storage.remove(tmpHtmlPath.c_str());
|
||||
}
|
||||
|
||||
HalFile tmpHtml;
|
||||
if (!Storage.openFileForWrite("SCT", tmpHtmlPath, tmpHtml)) {
|
||||
continue;
|
||||
}
|
||||
// Larger chunks mean far fewer SD writes inflating the HTML; a 1KB chunk turned a 584KB
|
||||
// single-spine novel into ~570 tiny writes (multi-second). 8KB keeps the transient buffers
|
||||
// small while cutting the write count 8x.
|
||||
streamed = epub->readItemContentsToStream(localPath, tmpHtml, 8192);
|
||||
fileSize = tmpHtml.size();
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
tmpHtml.close();
|
||||
|
||||
// If streaming failed, remove the incomplete file immediately
|
||||
if (!streamed && Storage.exists(tmpHtmlPath.c_str())) {
|
||||
Storage.remove(tmpHtmlPath.c_str());
|
||||
LOG_DBG("SCT", "Removed incomplete temp file after failed attempt");
|
||||
}
|
||||
}
|
||||
|
||||
if (!streamed) {
|
||||
LOG_ERR("SCT", "Failed to stream item contents to temp file after retries");
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("SCT", "Streamed temp HTML to %s (%d bytes)", tmpHtmlPath.c_str(), fileSize);
|
||||
|
||||
// Promote to the persistent HTML cache immediately -- the inflate is complete and the bytes are
|
||||
// valid regardless of whether the layout build finishes, so reopening (even a window-only spine
|
||||
// that never finalizes its .bin) skips re-inflation. If the rename fails we just parse the temp.
|
||||
if (Storage.rename(tmpHtmlPath.c_str(), htmlPath.c_str())) {
|
||||
htmlCached = true;
|
||||
} else {
|
||||
LOG_DBG("SCT", "Failed to promote HTML cache; parsing from temp");
|
||||
}
|
||||
}
|
||||
|
||||
if (!Storage.openFileForWrite("SCT", binTmpPath(), file)) {
|
||||
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||
return false;
|
||||
}
|
||||
// Header is written with the incomplete-version sentinel; finalizeBuild() commits it.
|
||||
writeSectionFileHeader(fontId, lineCompression, extraParagraphSpacing, paragraphAlignment, viewportWidth,
|
||||
viewportHeight, hyphenationEnabled, embeddedStyle, imageRendering, focusReadingEnabled);
|
||||
|
||||
auto ctx = makeUniqueNoThrow<BuildContext>();
|
||||
if (!ctx) {
|
||||
LOG_ERR("SCT", "OOM: BuildContext");
|
||||
file.close();
|
||||
Storage.remove(binTmpPath().c_str());
|
||||
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||
return false;
|
||||
}
|
||||
// htmlCached == "htmlPath is the live cache" (reused, or just promoted). finalizeBuild/abandonBuild
|
||||
// then leave the cached HTML alone; only an un-promoted temp (rename failed) is theirs to clean up.
|
||||
ctx->reusedHtml = htmlCached;
|
||||
ctx->htmlPath = htmlPath;
|
||||
ctx->tmpHtmlPath = tmpHtmlPath;
|
||||
ctx->parsePath = htmlCached ? htmlPath : tmpHtmlPath;
|
||||
|
||||
// Derive the content base directory and image cache path prefix for the parser
|
||||
const size_t lastSlash = localPath.find_last_of('/');
|
||||
ctx->contentBase = (lastSlash != std::string::npos) ? localPath.substr(0, lastSlash + 1) : "";
|
||||
ctx->imageBasePath = epub->getCachePath() + "/img_" + std::to_string(spineIndex) + "_";
|
||||
|
||||
if (embeddedStyle) {
|
||||
ctx->cssParser = epub->getCssParser();
|
||||
if (ctx->cssParser && !ctx->cssParser->loadFromCache()) {
|
||||
LOG_ERR("SCT", "Failed to load CSS from cache");
|
||||
}
|
||||
}
|
||||
|
||||
// Collect TOC anchors for this spine so the parser can insert page breaks at chapter boundaries
|
||||
std::vector<std::string> tocAnchors;
|
||||
const int startTocIndex = epub->getTocIndexForSpineIndex(spineIndex);
|
||||
if (startTocIndex >= 0) {
|
||||
for (int i = startTocIndex; i < epub->getTocItemsCount(); i++) {
|
||||
auto entry = epub->getTocItem(i);
|
||||
if (entry.spineIndex != spineIndex) break;
|
||||
if (!entry.anchor.empty()) {
|
||||
tocAnchors.push_back(std::move(entry.anchor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The parser stores the path/contentBase/imageBasePath by reference, so they must
|
||||
// live in the BuildContext (which outlives the parser). The page-complete callback
|
||||
// captures the BuildContext pointer to append to its in-RAM LUT; build_ owns the
|
||||
// context for the parser's whole lifetime.
|
||||
BuildContext* ctxPtr = ctx.get();
|
||||
ctx->parser = makeUniqueNoThrow<ChapterHtmlSlimParser>(
|
||||
epub, ctxPtr->parsePath, renderer, fontId, lineCompression, extraParagraphSpacing, paragraphAlignment,
|
||||
viewportWidth, viewportHeight, hyphenationEnabled, focusReadingEnabled,
|
||||
[this, ctxPtr](std::unique_ptr<Page> page, const uint16_t paragraphIndex, const uint16_t listItemIndex) {
|
||||
ctxPtr->lut.push_back({this->onPageComplete(std::move(page)), paragraphIndex, listItemIndex});
|
||||
},
|
||||
embeddedStyle, ctxPtr->contentBase, ctxPtr->imageBasePath, imageRendering, std::move(tocAnchors), popupFn,
|
||||
ctxPtr->cssParser);
|
||||
if (!ctx->parser) {
|
||||
LOG_ERR("SCT", "OOM: ChapterHtmlSlimParser");
|
||||
if (ctx->cssParser) ctx->cssParser->clear();
|
||||
file.close();
|
||||
Storage.remove(binTmpPath().c_str());
|
||||
if (!reusedHtml) Storage.remove(tmpHtmlPath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
Hyphenator::setPreferredLanguage(epub->getLanguage());
|
||||
build_ = std::move(ctx);
|
||||
|
||||
if (!build_->parser->beginParse()) {
|
||||
LOG_ERR("SCT", "Failed to begin parse");
|
||||
abandonBuild();
|
||||
return false;
|
||||
}
|
||||
build_->totalBytes = build_->parser->parseTotalBytes();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Section::buildSomeMore(const int maxPages) {
|
||||
if (!build_ || !build_->parser) {
|
||||
LOG_ERR("SCT", "buildSomeMore with no active build");
|
||||
return false;
|
||||
}
|
||||
// Pace on pages laid out by THIS build, not pageCount: during a rebuild over a partial,
|
||||
// pageCount stays pinned at the partial's watermark until the build passes it, which
|
||||
// would otherwise turn one "small" chunk into a blocking rebuild of the whole watermark.
|
||||
const int startCount = builtPageCount_;
|
||||
for (;;) {
|
||||
const auto status = build_->parser->parseStep();
|
||||
if (status == ChapterHtmlSlimParser::ParseStatus::Error) {
|
||||
LOG_ERR("SCT", "Parse error during incremental build");
|
||||
abandonBuild();
|
||||
return false;
|
||||
}
|
||||
if (status == ChapterHtmlSlimParser::ParseStatus::Done) {
|
||||
return finalizeBuild();
|
||||
}
|
||||
// ParseStatus::More: yield once we've laid out the requested number of pages.
|
||||
if (maxPages > 0 && (builtPageCount_ - startCount) >= maxPages) {
|
||||
build_->bytesConsumed = build_->parser->parseBytesConsumed();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Section::hasHtmlCache() const {
|
||||
const std::string htmlPath = epub->getCachePath() + "/html/" + std::to_string(spineIndex) + ".html";
|
||||
return Storage.exists(htmlPath.c_str());
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::findAnchorDuringBuild(const std::string& anchor) const {
|
||||
if (!build_ || !build_->parser) return std::nullopt;
|
||||
for (const auto& [key, page] : build_->parser->getAnchors()) {
|
||||
if (key == anchor) return page;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::findAnchor(const std::string& anchor) const {
|
||||
if (const auto page = findAnchorDuringBuild(anchor)) {
|
||||
return page;
|
||||
}
|
||||
// Fall back to the on-disk anchor map: a finalized section, or a partial whose map
|
||||
// covers everything up to its watermark (nullopt past it -- build further and retry).
|
||||
return getPageForAnchor(anchor);
|
||||
}
|
||||
|
||||
uint16_t Section::estimatedTotalPages() const {
|
||||
// Extrapolation from a suspended session's watermark trailer. A static snapshot, so no EMA
|
||||
// damping is needed. Also the best guess while a rebuild is running but hasn't laid out
|
||||
// enough pages yet to extrapolate from its own progress.
|
||||
const auto partialEstimate = [this]() -> uint16_t {
|
||||
if (!partial_ || partialBytesConsumed_ == 0 || partialTotalBytes_ <= partialBytesConsumed_) {
|
||||
return pageCount;
|
||||
}
|
||||
const uint64_t est = static_cast<uint64_t>(partialPageCount_) * partialTotalBytes_ / partialBytesConsumed_;
|
||||
if (est <= pageCount) return pageCount;
|
||||
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
|
||||
};
|
||||
|
||||
if (!build_) {
|
||||
return partial_ ? partialEstimate() : pageCount; // partial -> extrapolate, finalized -> exact
|
||||
}
|
||||
const uint32_t consumed = build_->bytesConsumed;
|
||||
const uint32_t total = build_->totalBytes;
|
||||
if (builtPageCount_ == 0 || consumed == 0 || total <= consumed) return partialEstimate();
|
||||
|
||||
// Raw extrapolation: scale the pages built so far by the fraction of HTML still unparsed. This
|
||||
// re-derives from a growing, non-uniform sample, so it jitters up and down as the build crosses
|
||||
// dense vs sparse regions of the chapter.
|
||||
const uint64_t raw = static_cast<uint64_t>(builtPageCount_) * total / consumed;
|
||||
|
||||
// Damp that jitter with an exponential moving average. Step it once per build advance (keyed on
|
||||
// bytesConsumed) rather than per status-bar redraw, so the smoothing rate doesn't depend on how
|
||||
// often we repaint. As the build nears the end, consumed -> total and raw -> the built count, so
|
||||
// the average settles onto the true count (and finalizeBuild then returns the exact pageCount).
|
||||
constexpr float ALPHA = 0.25f; // weight of each new sample; lower = steadier but slower to settle
|
||||
if (build_->smoothedEstimate <= 0) {
|
||||
build_->smoothedEstimate = static_cast<float>(raw); // seed on the first estimate
|
||||
} else if (consumed != build_->smoothedAtConsumed) {
|
||||
build_->smoothedEstimate += ALPHA * (static_cast<float>(raw) - build_->smoothedEstimate);
|
||||
}
|
||||
build_->smoothedAtConsumed = consumed;
|
||||
|
||||
const uint64_t est = static_cast<uint64_t>(build_->smoothedEstimate + 0.5f);
|
||||
if (est <= pageCount) return pageCount; // never fewer than the pages already available
|
||||
return est > 60000 ? 60000 : static_cast<uint16_t>(est);
|
||||
}
|
||||
|
||||
// Write the LUTs and anchor map into the open tmp .bin, patch the header with the built
|
||||
// page count and table offsets, stamp `version` as the commit point, then swap the tmp
|
||||
// file over filePath. For SECTION_FILE_PARTIAL_VERSION a watermark trailer
|
||||
// (bytesConsumed, totalBytes) is appended after the li LUT so a later open can estimate
|
||||
// the total page count. The parser must still be alive (anchors are read from it).
|
||||
// On failure the tmp is removed and any pre-existing file at filePath is left intact.
|
||||
bool Section::commitBuildFile(const uint8_t version, const uint32_t bytesConsumed, const uint32_t totalBytes) {
|
||||
const bool asPartial = (version == SECTION_FILE_PARTIAL_VERSION);
|
||||
|
||||
const auto failCommit = [this]() {
|
||||
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||
file.close();
|
||||
Storage.remove(binTmpPath().c_str());
|
||||
return false;
|
||||
};
|
||||
|
||||
const uint32_t lutOffset = file.position();
|
||||
for (const auto& entry : build_->lut) {
|
||||
if (entry.fileOffset == 0) {
|
||||
LOG_ERR("SCT", "Failed to write LUT due to invalid page positions");
|
||||
return failCommit();
|
||||
}
|
||||
serialization::writePod(file, entry.fileOffset);
|
||||
}
|
||||
|
||||
// Write anchor-to-page map for fragment navigation (e.g. footnote targets). For a
|
||||
// partial, skip anchors that landed on the incomplete trailing page the suspend drops.
|
||||
const uint32_t anchorMapOffset = file.position();
|
||||
const auto& anchors = build_->parser->getAnchors();
|
||||
uint16_t anchorCount = 0;
|
||||
for (const auto& [anchor, page] : anchors) {
|
||||
if (!asPartial || page < builtPageCount_) anchorCount++;
|
||||
}
|
||||
serialization::writePod(file, anchorCount);
|
||||
for (const auto& [anchor, page] : anchors) {
|
||||
if (asPartial && page >= builtPageCount_) continue;
|
||||
serialization::writeString(file, anchor);
|
||||
serialization::writePod(file, page);
|
||||
}
|
||||
|
||||
const uint32_t paragraphLutOffset = file.position();
|
||||
serialization::writePod(file, static_cast<uint16_t>(build_->lut.size()));
|
||||
for (const auto& entry : build_->lut) {
|
||||
serialization::writePod(file, entry.paragraphIndex);
|
||||
}
|
||||
|
||||
const uint32_t liLutFileOffset = static_cast<uint32_t>(file.position());
|
||||
for (const auto& entry : build_->lut) {
|
||||
serialization::writePod(file, entry.listItemIndex);
|
||||
}
|
||||
|
||||
if (asPartial) {
|
||||
// Watermark trailer, located on load as liLutOffset + pageCount * sizeof(uint16_t).
|
||||
serialization::writePod(file, bytesConsumed);
|
||||
serialization::writePod(file, totalBytes);
|
||||
}
|
||||
|
||||
// Patch header with the built page count and section offsets...
|
||||
file.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(builtPageCount_));
|
||||
serialization::writePod(file, builtPageCount_);
|
||||
serialization::writePod(file, lutOffset);
|
||||
serialization::writePod(file, anchorMapOffset);
|
||||
serialization::writePod(file, paragraphLutOffset);
|
||||
serialization::writePod(file, liLutFileOffset);
|
||||
// ...then commit by overwriting the sentinel version with the real one. Writing the
|
||||
// version last makes it the commit point: a crash before here leaves version 0.
|
||||
file.seek(0);
|
||||
serialization::writePod(file, version);
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
file.close();
|
||||
|
||||
// Swap into place. A crash between remove and rename loses the old file but keeps a
|
||||
// fully-committed tmp; the next build just removes it and rebuilds.
|
||||
if (Storage.exists(filePath.c_str())) {
|
||||
Storage.remove(filePath.c_str());
|
||||
}
|
||||
if (!Storage.rename(binTmpPath().c_str(), filePath.c_str())) {
|
||||
LOG_ERR("SCT", "Failed to move built section into place");
|
||||
Storage.remove(binTmpPath().c_str());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Section::finalizeBuild() {
|
||||
// Flush the trailing page (emits the last page via the completePageFn into the LUT).
|
||||
build_->parser->finishParse();
|
||||
|
||||
if (!build_->reusedHtml) {
|
||||
// Parse succeeded: promote the freshly unzipped HTML to the persistent cache so future
|
||||
// rebuilds skip zip inflation. If promotion fails, drop the temp -- the build still succeeded.
|
||||
if (!Storage.rename(build_->tmpHtmlPath.c_str(), build_->htmlPath.c_str())) {
|
||||
LOG_DBG("SCT", "Failed to promote HTML cache, removing temp");
|
||||
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
const bool committed = commitBuildFile(SECTION_FILE_VERSION, 0, 0);
|
||||
if (build_->cssParser) build_->cssParser->clear();
|
||||
build_.reset();
|
||||
if (!committed) {
|
||||
// commitBuildFile removed filePath before the failed swap, so nothing valid remains.
|
||||
partial_ = false;
|
||||
partialPageCount_ = 0;
|
||||
pageCount = 0;
|
||||
builtPageCount_ = 0;
|
||||
return false;
|
||||
}
|
||||
buildComplete_ = true;
|
||||
partial_ = false;
|
||||
partialPageCount_ = 0;
|
||||
pageCount = builtPageCount_;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Section::suspendBuild() {
|
||||
if (!build_) return;
|
||||
|
||||
// Only worth persisting if this build produced pages a pre-existing partial doesn't
|
||||
// already cover; otherwise keep the older (bigger) partial and just drop the tmp.
|
||||
const bool worthKeeping = builtPageCount_ > 0 && (!partial_ || builtPageCount_ > partialPageCount_);
|
||||
|
||||
bool committed = false;
|
||||
if (worthKeeping) {
|
||||
// Capture the parse watermark and commit before tearing the parser down (the anchor
|
||||
// map is read from it). The incomplete trailing page is intentionally not flushed:
|
||||
// only fully laid-out pages are persisted, and the rebuild re-derives the rest.
|
||||
const uint32_t consumed = static_cast<uint32_t>(build_->parser->parseBytesConsumed());
|
||||
committed = commitBuildFile(SECTION_FILE_PARTIAL_VERSION, consumed, build_->totalBytes);
|
||||
if (committed) {
|
||||
partial_ = true;
|
||||
partialPageCount_ = builtPageCount_;
|
||||
partialBytesConsumed_ = consumed;
|
||||
partialTotalBytes_ = build_->totalBytes;
|
||||
LOG_INF("SCT", "Suspended build: %u pages persisted", builtPageCount_);
|
||||
}
|
||||
}
|
||||
|
||||
if (build_->parser) build_->parser->abortParse();
|
||||
if (build_->cssParser) build_->cssParser->clear();
|
||||
if (!committed && file) {
|
||||
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||
file.close();
|
||||
Storage.remove(binTmpPath().c_str());
|
||||
}
|
||||
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
|
||||
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||
}
|
||||
build_.reset();
|
||||
buildComplete_ = false;
|
||||
pageCount = partial_ ? partialPageCount_ : 0;
|
||||
builtPageCount_ = 0;
|
||||
}
|
||||
|
||||
void Section::abandonBuild() {
|
||||
if (!build_) return;
|
||||
if (build_->parser) build_->parser->abortParse();
|
||||
if (build_->cssParser) build_->cssParser->clear();
|
||||
if (file) {
|
||||
// Explicit close() required before remove (member variable, O_RDWR handle).
|
||||
file.close();
|
||||
Storage.remove(binTmpPath().c_str());
|
||||
}
|
||||
// A parse error would recur against the same HTML, so drop any partial too -- resuming
|
||||
// from it would just re-enter the failing build every open.
|
||||
if (Storage.exists(filePath.c_str())) {
|
||||
Storage.remove(filePath.c_str());
|
||||
}
|
||||
if (!build_->reusedHtml && Storage.exists(build_->tmpHtmlPath.c_str())) {
|
||||
Storage.remove(build_->tmpHtmlPath.c_str());
|
||||
}
|
||||
build_.reset();
|
||||
buildComplete_ = false;
|
||||
partial_ = false;
|
||||
partialPageCount_ = 0;
|
||||
pageCount = 0;
|
||||
builtPageCount_ = 0;
|
||||
}
|
||||
|
||||
std::unique_ptr<Page> Section::loadPageDuringBuild(const int page) {
|
||||
if (!build_ || page < 0 || page >= static_cast<int>(build_->lut.size()) || !file) {
|
||||
return nullptr;
|
||||
}
|
||||
const uint32_t pos = build_->lut[page].fileOffset;
|
||||
if (pos == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
// The .bin is open O_RDWR for the build. Read the already-written page, then restore
|
||||
// the write cursor so the next onPageComplete keeps appending where it left off.
|
||||
const uint32_t writePos = file.position();
|
||||
file.seek(pos);
|
||||
auto p = Page::deserialize(file);
|
||||
file.seek(writePos);
|
||||
return p;
|
||||
}
|
||||
|
||||
// Read a page from the committed file at filePath (finalized section or partial from a
|
||||
// previous session). Uses a local handle so it is safe while a build holds the member
|
||||
// `file` open on the tmp .bin.
|
||||
std::unique_ptr<Page> Section::loadPageAt(const int page) const {
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4);
|
||||
uint32_t lutOffset;
|
||||
serialization::readPod(f, lutOffset);
|
||||
f.seek(lutOffset + sizeof(uint32_t) * page);
|
||||
uint32_t pagePos;
|
||||
serialization::readPod(f, pagePos);
|
||||
f.seek(pagePos);
|
||||
|
||||
return Page::deserialize(f);
|
||||
// No f.close() needed -- DESTRUCTOR_CLOSES_FILE=1 handles it at scope exit
|
||||
}
|
||||
|
||||
std::unique_ptr<Page> Section::loadPage(const int page) {
|
||||
if (page < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
if (build_ && page < static_cast<int>(build_->lut.size())) {
|
||||
return loadPageDuringBuild(page);
|
||||
}
|
||||
// Not (yet) in the active build: serve from the file on disk -- a finalized section,
|
||||
// or a partial from a previous session whose pages the rebuild hasn't reached again.
|
||||
const int onDisk = partial_ ? partialPageCount_ : (build_ ? 0 : pageCount);
|
||||
if (page >= onDisk) {
|
||||
return nullptr;
|
||||
}
|
||||
return loadPageAt(page);
|
||||
}
|
||||
|
||||
std::string Section::getTextFromSectionFile() {
|
||||
std::string fullText;
|
||||
auto p = loadPage(currentPage);
|
||||
if (p) {
|
||||
for (const auto& el : p->elements) {
|
||||
if (el->getTag() == TAG_PageLine) {
|
||||
const auto& line = static_cast<const PageLine&>(*el);
|
||||
if (line.getBlock()) {
|
||||
const auto& block = *line.getBlock();
|
||||
for (uint16_t i = 0; i < block.wordCount(); i++) {
|
||||
if (!fullText.empty()) fullText += " ";
|
||||
fullText += block.wordText(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return fullText;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::getCachedPageCount() const {
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t fileSize = f.size();
|
||||
if (fileSize < HEADER_SIZE) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Only a finalized section's count is the chapter total; a partial's count is just the
|
||||
// suspended build's watermark, which would skew progress mapping. Callers fall back to
|
||||
// their own estimates.
|
||||
uint8_t version;
|
||||
serialization::readPod(f, version);
|
||||
if (version != SECTION_FILE_VERSION) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 4 - sizeof(uint16_t));
|
||||
uint16_t count;
|
||||
serialization::readPod(f, count);
|
||||
return count;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::getPageForAnchor(const std::string& anchor) const {
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t fileSize = f.size();
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 3);
|
||||
uint32_t anchorMapOffset;
|
||||
serialization::readPod(f, anchorMapOffset);
|
||||
if (anchorMapOffset == 0 || anchorMapOffset >= fileSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.seek(anchorMapOffset);
|
||||
uint16_t count;
|
||||
serialization::readPod(f, count);
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
std::string key;
|
||||
uint16_t page;
|
||||
serialization::readString(f, key);
|
||||
serialization::readPod(f, page);
|
||||
if (key == anchor) {
|
||||
return page;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::getPageForParagraphIndex(const uint16_t pIndex) const {
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t fileSize = f.size();
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
|
||||
uint32_t paragraphLutOffset;
|
||||
serialization::readPod(f, paragraphLutOffset);
|
||||
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.seek(paragraphLutOffset);
|
||||
uint16_t count;
|
||||
serialization::readPod(f, count);
|
||||
if (count == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t lutEnd = paragraphLutOffset + sizeof(uint16_t) + count * sizeof(uint16_t);
|
||||
if (lutEnd > fileSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
uint16_t resultPage = count - 1;
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
uint16_t pagePIdx;
|
||||
serialization::readPod(f, pagePIdx);
|
||||
if (pagePIdx >= pIndex) {
|
||||
resultPage = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return resultPage;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::getParagraphIndexForPage(const uint16_t page) const {
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t fileSize = f.size();
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
|
||||
uint32_t paragraphLutOffset;
|
||||
serialization::readPod(f, paragraphLutOffset);
|
||||
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.seek(paragraphLutOffset);
|
||||
uint16_t count;
|
||||
serialization::readPod(f, count);
|
||||
if (count == 0 || page >= count) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t entryEnd = paragraphLutOffset + sizeof(uint16_t) + (page + 1) * sizeof(uint16_t);
|
||||
if (entryEnd > fileSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.seek(paragraphLutOffset + sizeof(uint16_t) + page * sizeof(uint16_t));
|
||||
uint16_t pIdx;
|
||||
serialization::readPod(f, pIdx);
|
||||
return pIdx;
|
||||
}
|
||||
|
||||
std::optional<uint16_t> Section::getPageForListItemIndex(const uint16_t liIndex) const {
|
||||
HalFile f;
|
||||
if (!Storage.openFileForRead("SCT", filePath, f)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t fileSize = f.size();
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t));
|
||||
uint32_t liLutOffset;
|
||||
serialization::readPod(f, liLutOffset);
|
||||
if (liLutOffset == 0 || liLutOffset >= fileSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// The li LUT shares count with the paragraph LUT; read count from paragraphLutOffset
|
||||
f.seek(HEADER_SIZE - sizeof(uint32_t) * 2);
|
||||
uint32_t paragraphLutOffset;
|
||||
serialization::readPod(f, paragraphLutOffset);
|
||||
if (paragraphLutOffset == 0 || paragraphLutOffset >= fileSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.seek(paragraphLutOffset);
|
||||
uint16_t count;
|
||||
serialization::readPod(f, count);
|
||||
if (count == 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint32_t lutEnd = liLutOffset + count * sizeof(uint16_t);
|
||||
if (lutEnd > fileSize) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
f.seek(liLutOffset);
|
||||
uint16_t resultPage = count - 1;
|
||||
for (uint16_t i = 0; i < count; i++) {
|
||||
uint16_t pageLiIdx;
|
||||
serialization::readPod(f, pageLiIdx);
|
||||
if (pageLiIdx >= liIndex) {
|
||||
resultPage = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return resultPage;
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
#pragma once
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub.h"
|
||||
|
||||
class Page;
|
||||
class GfxRenderer;
|
||||
class ChapterHtmlSlimParser;
|
||||
class CssParser;
|
||||
|
||||
class Section {
|
||||
std::shared_ptr<Epub> epub;
|
||||
const int spineIndex;
|
||||
GfxRenderer& renderer;
|
||||
std::string filePath;
|
||||
HalFile file;
|
||||
|
||||
void writeSectionFileHeader(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled,
|
||||
bool embeddedStyle, uint8_t imageRendering, bool focusReadingEnabled);
|
||||
uint32_t onPageComplete(std::unique_ptr<Page> page);
|
||||
|
||||
// Page-offset table entry, kept in RAM while an incremental build is running so
|
||||
// already-built pages can be located in the partially-written .bin.
|
||||
struct PageLutEntry {
|
||||
uint32_t fileOffset;
|
||||
uint16_t paragraphIndex;
|
||||
uint16_t listItemIndex;
|
||||
};
|
||||
// Held only while an incremental build is in progress (see startBuild). Carries the
|
||||
// live parser plus the strings it references (the parser stores them by reference)
|
||||
// and the in-RAM page-offset table.
|
||||
struct BuildContext {
|
||||
std::unique_ptr<ChapterHtmlSlimParser> parser;
|
||||
std::vector<PageLutEntry> lut;
|
||||
std::string parsePath;
|
||||
std::string contentBase;
|
||||
std::string imageBasePath;
|
||||
std::string htmlPath;
|
||||
std::string tmpHtmlPath;
|
||||
bool reusedHtml = false;
|
||||
CssParser* cssParser = nullptr;
|
||||
// HTML byte progress, for estimating the section's total page count while it's still building.
|
||||
uint32_t bytesConsumed = 0;
|
||||
uint32_t totalBytes = 0;
|
||||
// Exponentially-smoothed page-count estimate (0 = not yet seeded) and the bytesConsumed at its
|
||||
// last update. The raw byte-ratio estimate jitters as the build crosses dense/sparse regions;
|
||||
// the EMA is stepped once per build advance (not per redraw) to damp that wobble.
|
||||
float smoothedEstimate = 0;
|
||||
uint32_t smoothedAtConsumed = 0;
|
||||
};
|
||||
std::unique_ptr<BuildContext> build_;
|
||||
bool buildComplete_ = false;
|
||||
// Pages laid out by the active build (== build_->lut.size()). Distinct from pageCount,
|
||||
// which is the pages *available to read* and also counts a loaded partial file's pages.
|
||||
uint16_t builtPageCount_ = 0;
|
||||
// A partial section file (suspended build from a previous session) is loaded at filePath.
|
||||
// Its pages 0..partialPageCount_-1 are readable while a rebuild extends past them.
|
||||
bool partial_ = false;
|
||||
uint16_t partialPageCount_ = 0;
|
||||
// Parse watermark from the partial's trailer, for estimating the total page count.
|
||||
uint32_t partialBytesConsumed_ = 0;
|
||||
uint32_t partialTotalBytes_ = 0;
|
||||
bool finalizeBuild();
|
||||
// Write the LUTs/anchor map (and, for a partial, the watermark trailer), patch the
|
||||
// header, stamp the version byte, and swap the tmp .bin over filePath.
|
||||
bool commitBuildFile(uint8_t version, uint32_t bytesConsumed, uint32_t totalBytes);
|
||||
// Builds write here and are swapped over filePath only on commit, so a prior
|
||||
// partial/finalized file stays readable while a rebuild is in progress.
|
||||
std::string binTmpPath() const { return filePath + ".part"; }
|
||||
std::unique_ptr<Page> loadPageAt(int page) const;
|
||||
// Read a page already laid out by the in-progress build (page < build LUT size), from
|
||||
// the partially-written tmp .bin without disturbing the build's write cursor.
|
||||
std::unique_ptr<Page> loadPageDuringBuild(int page);
|
||||
|
||||
public:
|
||||
uint16_t pageCount = 0;
|
||||
int currentPage = 0;
|
||||
|
||||
// Constructor and destructor are out-of-line: BuildContext holds a unique_ptr to the
|
||||
// forward-declared ChapterHtmlSlimParser, whose full definition is only visible in the .cpp.
|
||||
explicit Section(const std::shared_ptr<Epub>& epub, int spineIndex, GfxRenderer& renderer);
|
||||
~Section();
|
||||
bool loadSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
uint8_t imageRendering, bool focusReadingEnabled);
|
||||
bool clearCache() const;
|
||||
bool createSectionFile(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
uint8_t imageRendering, bool focusReadingEnabled,
|
||||
const std::function<void()>& popupFn = nullptr);
|
||||
|
||||
// Incremental build: lay out the section a few pages at a time so a large chapter
|
||||
// can show its first page immediately and keep the UI responsive while the rest
|
||||
// builds. createSectionFile() above is the one-shot wrapper over these.
|
||||
// if (!startBuild(...)) fail;
|
||||
// each tick: buildSomeMore(N); render up to pageCount; when isBuildComplete() stop.
|
||||
bool startBuild(int fontId, float lineCompression, bool extraParagraphSpacing, uint8_t paragraphAlignment,
|
||||
uint16_t viewportWidth, uint16_t viewportHeight, bool hyphenationEnabled, bool embeddedStyle,
|
||||
uint8_t imageRendering, bool focusReadingEnabled, const std::function<void()>& popupFn = nullptr);
|
||||
// Lay out up to maxPages more pages (maxPages <= 0 = build to completion). Returns
|
||||
// false on error (the build is abandoned). Sets isBuildComplete() when finished.
|
||||
bool buildSomeMore(int maxPages);
|
||||
bool isBuilding() const { return static_cast<bool>(build_); }
|
||||
bool isBuildComplete() const { return buildComplete_; }
|
||||
// Best-known total page count: the exact pageCount once finalized, or a smoothed byte-based
|
||||
// estimate (pages so far scaled by totalBytes/bytesConsumed, damped by an EMA) while a giant spine
|
||||
// is still building, so "page X of Y" / progress don't read off the small build watermark.
|
||||
uint16_t estimatedTotalPages() const;
|
||||
void abandonBuild();
|
||||
// Persist an in-progress build as a partial section file (version sentinel + LUTs +
|
||||
// watermark trailer) instead of discarding it, so the next open of this spine can show
|
||||
// its pages instantly and only rebuild in the background. Called by the destructor, so
|
||||
// any teardown path (exit, sleep, navigation) keeps the work already done. Keeps a
|
||||
// pre-existing partial when it covers more pages than this build reached.
|
||||
void suspendBuild();
|
||||
// True when a partial file was loaded: pageCount is a watermark, not the chapter total.
|
||||
bool isPartial() const { return partial_; }
|
||||
|
||||
// Unified page read: from the active build if it has reached the page, otherwise from
|
||||
// the on-disk file (finalized section, or a partial the rebuild hasn't caught up to).
|
||||
std::unique_ptr<Page> loadPage(int page);
|
||||
|
||||
std::string getTextFromSectionFile();
|
||||
|
||||
// Resolve an anchor from the in-progress build first, then the on-disk anchor map
|
||||
// (covers finalized sections and partials from a previous session).
|
||||
std::optional<uint16_t> findAnchor(const std::string& anchor) const;
|
||||
|
||||
// True if this spine's unzipped HTML is already cached, so a build won't pay the (multi-second on a
|
||||
// giant spine) zip inflation. Lets the reader skip the indexing popup on a fast reopen/rebuild.
|
||||
bool hasHtmlCache() const;
|
||||
|
||||
// Look up the page number for an anchor id from the section cache file.
|
||||
std::optional<uint16_t> getPageForAnchor(const std::string& anchor) const;
|
||||
|
||||
// Look up an anchor among the pages built so far by the in-progress build, so an anchor jump
|
||||
// (TOC / chapter select, usually the chapter top = page 0) can resolve without laying out the
|
||||
// whole chapter. Returns nullopt if the anchor hasn't been reached yet (build more) or no build.
|
||||
std::optional<uint16_t> findAnchorDuringBuild(const std::string& anchor) const;
|
||||
|
||||
// Get the page count from the section cache file without fully loading it.
|
||||
std::optional<uint16_t> getCachedPageCount() const;
|
||||
|
||||
// Look up the page number for a synthetic paragraph index from XPath p[N].
|
||||
std::optional<uint16_t> getPageForParagraphIndex(uint16_t pIndex) const;
|
||||
|
||||
// Look up the page number for a running list-item index from the li LUT.
|
||||
std::optional<uint16_t> getPageForListItemIndex(uint16_t liIndex) const;
|
||||
|
||||
// Look up the synthetic paragraph index for the given rendered page.
|
||||
std::optional<uint16_t> getParagraphIndexForPage(uint16_t page) const;
|
||||
};
|
||||
@@ -1,15 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
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 BlockType getType() = 0;
|
||||
virtual bool isEmpty() = 0;
|
||||
virtual void finish() {}
|
||||
};
|
||||
@@ -1,145 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
|
||||
#include "Epub/css/CssStyle.h"
|
||||
|
||||
/**
|
||||
* BlockStyle - Block-level styling properties
|
||||
*/
|
||||
struct BlockStyle {
|
||||
// Upper bound (in em) for any single side's horizontal margin or padding.
|
||||
// Some EPUBs apply huge em-based insets to chapter-opener classes; without a
|
||||
// cap, effectiveWidth collapses to 1-2 words per line and justification dumps
|
||||
// the remaining space into a single gap.
|
||||
static constexpr float MAX_HORIZONTAL_INSET_EM = 2.0f;
|
||||
|
||||
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
|
||||
bool isRtl = false; // true if resolved direction is RTL
|
||||
bool directionDefined = false; // true if direction was explicitly set in CSS/HTML
|
||||
|
||||
// Set when this block was created by a <br> element. Used by startNewTextBlock to inject
|
||||
// a full line-height gap when the <br> block stays empty (section-break use case).
|
||||
// NOT propagated through getCombinedBlockStyle so it can't leak into sibling blocks.
|
||||
bool fromBrElement = false;
|
||||
|
||||
// Combined 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(); }
|
||||
[[nodiscard]] int16_t topInset() const { return marginTop + paddingTop; }
|
||||
[[nodiscard]] int16_t bottomInset() const { return marginBottom + paddingBottom; }
|
||||
|
||||
// Return a copy with bottom margins/padding zeroed out.
|
||||
[[nodiscard]] BlockStyle withoutBottom() const {
|
||||
BlockStyle result = *this;
|
||||
result.marginBottom = 0;
|
||||
result.paddingBottom = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Return a copy with bottom margins/padding collapsed (max) with the source's.
|
||||
// Uses CSS margin collapsing: adjacent parent-child margins resolve to the larger value.
|
||||
[[nodiscard]] BlockStyle addBottom(const BlockStyle& source) const {
|
||||
BlockStyle result = *this;
|
||||
result.marginBottom = std::max(marginBottom, source.marginBottom);
|
||||
result.paddingBottom = static_cast<int16_t>(paddingBottom + source.paddingBottom);
|
||||
return result;
|
||||
}
|
||||
|
||||
enum class CombineAxis : uint8_t {
|
||||
Horizontal = 1, // margins left/right, padding left/right, text-align, text-indent
|
||||
Vertical = 2, // margins top/bottom, padding top/bottom
|
||||
};
|
||||
|
||||
// Combine this style's properties with a child style along the specified axis.
|
||||
// Properties on the other axis are kept from the child unchanged.
|
||||
[[nodiscard]] BlockStyle getCombinedBlockStyle(const BlockStyle& child, CombineAxis axis) const {
|
||||
BlockStyle result = child;
|
||||
|
||||
if (axis == CombineAxis::Horizontal) {
|
||||
result.marginLeft = static_cast<int16_t>(child.marginLeft + marginLeft);
|
||||
result.marginRight = static_cast<int16_t>(child.marginRight + marginRight);
|
||||
result.paddingLeft = static_cast<int16_t>(child.paddingLeft + paddingLeft);
|
||||
result.paddingRight = static_cast<int16_t>(child.paddingRight + paddingRight);
|
||||
if (!child.textIndentDefined && textIndentDefined) {
|
||||
result.textIndent = textIndent;
|
||||
result.textIndentDefined = true;
|
||||
}
|
||||
if (!child.textAlignDefined && textAlignDefined) {
|
||||
result.alignment = alignment;
|
||||
result.textAlignDefined = true;
|
||||
}
|
||||
} else {
|
||||
result.marginTop = std::max(child.marginTop, marginTop);
|
||||
result.marginBottom = std::max(child.marginBottom, marginBottom);
|
||||
result.paddingTop = static_cast<int16_t>(child.paddingTop + paddingTop);
|
||||
result.paddingBottom = static_cast<int16_t>(child.paddingBottom + paddingBottom);
|
||||
}
|
||||
|
||||
// Direction is not axis-specific. Inherit from parent when child doesn't define it.
|
||||
if (!child.directionDefined && directionDefined) {
|
||||
result.isRtl = isRtl;
|
||||
result.directionDefined = true;
|
||||
}
|
||||
|
||||
// fromBrElement is consumed by startNewTextBlock when an empty <br> block
|
||||
// is merged with the following paragraph; never propagate it further.
|
||||
result.fromBrElement = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
// 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,
|
||||
const uint16_t viewportWidth = 0) {
|
||||
BlockStyle blockStyle;
|
||||
const float vw = viewportWidth;
|
||||
const auto maxHorizontalInsetPx = static_cast<int16_t>(emSize * MAX_HORIZONTAL_INSET_EM);
|
||||
// Resolve all CssLength values to pixels using the current font's em size and viewport width
|
||||
blockStyle.marginTop = cssStyle.marginTop.toPixelsInt16(emSize, vw);
|
||||
blockStyle.marginBottom = cssStyle.marginBottom.toPixelsInt16(emSize, vw);
|
||||
blockStyle.marginLeft = std::min(cssStyle.marginLeft.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
|
||||
blockStyle.marginRight = std::min(cssStyle.marginRight.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
|
||||
|
||||
blockStyle.paddingTop = cssStyle.paddingTop.toPixelsInt16(emSize, vw);
|
||||
blockStyle.paddingBottom = cssStyle.paddingBottom.toPixelsInt16(emSize, vw);
|
||||
blockStyle.paddingLeft = std::min(cssStyle.paddingLeft.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
|
||||
blockStyle.paddingRight = std::min(cssStyle.paddingRight.toPixelsInt16(emSize, vw), maxHorizontalInsetPx);
|
||||
|
||||
// For textIndent: if it's a percentage we can't resolve (no viewport width),
|
||||
// leave textIndentDefined=false so the space-width fallback in resolveFirstLineIndent() is used
|
||||
if (cssStyle.hasTextIndent() && cssStyle.textIndent.isResolvable(vw)) {
|
||||
blockStyle.textIndent = cssStyle.textIndent.toPixelsInt16(emSize, vw);
|
||||
blockStyle.textIndentDefined = true;
|
||||
}
|
||||
blockStyle.textAlignDefined = cssStyle.hasTextAlign();
|
||||
// User setting overrides CSS, unless "Book's Style" alignment setting is selected
|
||||
if (paragraphAlignment == CssTextAlign::None) {
|
||||
blockStyle.alignment = blockStyle.textAlignDefined ? cssStyle.textAlign : CssTextAlign::Justify;
|
||||
} else {
|
||||
blockStyle.alignment = paragraphAlignment;
|
||||
}
|
||||
// RTL direction from CSS/HTML
|
||||
if (cssStyle.hasDirection()) {
|
||||
blockStyle.isRtl = (cssStyle.direction == CssTextDirection::Rtl);
|
||||
blockStyle.directionDefined = true;
|
||||
}
|
||||
return blockStyle;
|
||||
}
|
||||
};
|
||||
@@ -1,218 +0,0 @@
|
||||
#include "ImageBlock.h"
|
||||
|
||||
#include <FontCacheManager.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include "Epub/converters/DirectPixelWriter.h"
|
||||
#include "Epub/converters/ImageDecoderFactory.h"
|
||||
|
||||
// Cache file format:
|
||||
// - uint16_t width
|
||||
// - uint16_t height
|
||||
// - uint8_t pixels[...] - 2 bits per pixel, packed (4 pixels per byte), row-major order
|
||||
|
||||
ImageBlock::ImageBlock(const std::string& imagePath, int16_t width, int16_t height)
|
||||
: imagePath(imagePath), width(width), height(height) {}
|
||||
|
||||
bool ImageBlock::imageExists() const { return Storage.exists(imagePath.c_str()); }
|
||||
|
||||
namespace {
|
||||
|
||||
std::string getCachePath(const std::string& imagePath) {
|
||||
// Replace extension with .pxc (pixel cache)
|
||||
size_t dotPos = imagePath.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
return imagePath.substr(0, dotPos) + ".pxc";
|
||||
}
|
||||
return imagePath + ".pxc";
|
||||
}
|
||||
|
||||
bool renderFromCache(GfxRenderer& renderer, const std::string& cachePath, int x, int y, int expectedWidth,
|
||||
int expectedHeight) {
|
||||
HalFile cacheFile;
|
||||
if (!Storage.openFileForRead("IMG", cachePath, cacheFile)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t cachedWidth, cachedHeight;
|
||||
if (cacheFile.read(&cachedWidth, 2) != 2 || cacheFile.read(&cachedHeight, 2) != 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify dimensions are close (allow 1 pixel tolerance for rounding differences)
|
||||
int widthDiff = abs(cachedWidth - expectedWidth);
|
||||
int heightDiff = abs(cachedHeight - expectedHeight);
|
||||
if (widthDiff > 1 || heightDiff > 1) {
|
||||
LOG_ERR("IMG", "Cache dimension mismatch: %dx%d vs %dx%d", cachedWidth, cachedHeight, expectedWidth,
|
||||
expectedHeight);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use cached dimensions for rendering (they're the actual decoded size)
|
||||
expectedWidth = cachedWidth;
|
||||
expectedHeight = cachedHeight;
|
||||
|
||||
LOG_DBG("IMG", "Loading from cache: %s (%dx%d)", cachePath.c_str(), cachedWidth, cachedHeight);
|
||||
|
||||
// Read several rows per SD access. A full-page image is re-rendered on every
|
||||
// grayscale strip pass (~14x per page), and a one-row-per-read loop here means
|
||||
// cachedHeight (~728) tiny reads through the storage mutex + SdFat each time —
|
||||
// the dominant cost of displaying an image page. Batching rows into a ~4KB
|
||||
// buffer cuts that to ~20 reads per pass without holding the whole image.
|
||||
const int bytesPerRow = (cachedWidth + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
int rowsPerRead = 4096 / bytesPerRow;
|
||||
if (rowsPerRead < 1) rowsPerRead = 1;
|
||||
if (rowsPerRead > cachedHeight) rowsPerRead = cachedHeight;
|
||||
uint8_t* readBuffer = (uint8_t*)malloc((size_t)rowsPerRead * bytesPerRow);
|
||||
if (!readBuffer) {
|
||||
// Fall back to a single-row buffer under memory pressure.
|
||||
rowsPerRead = 1;
|
||||
readBuffer = (uint8_t*)malloc(bytesPerRow);
|
||||
}
|
||||
if (!readBuffer) {
|
||||
LOG_ERR("IMG", "Failed to allocate row buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
DirectPixelWriter pw;
|
||||
pw.init(renderer);
|
||||
|
||||
int rowsInBuffer = 0;
|
||||
int bufferRow = 0;
|
||||
for (int row = 0; row < cachedHeight; row++) {
|
||||
if (bufferRow >= rowsInBuffer) {
|
||||
const int toRead = (cachedHeight - row < rowsPerRead) ? (cachedHeight - row) : rowsPerRead;
|
||||
const size_t bytes = (size_t)toRead * bytesPerRow;
|
||||
if (cacheFile.read(readBuffer, bytes) != static_cast<int>(bytes)) {
|
||||
LOG_ERR("IMG", "Cache read error at row %d", row);
|
||||
free(readBuffer);
|
||||
return false;
|
||||
}
|
||||
rowsInBuffer = toRead;
|
||||
bufferRow = 0;
|
||||
}
|
||||
|
||||
const uint8_t* rowBuffer = readBuffer + (size_t)bufferRow * bytesPerRow;
|
||||
bufferRow++;
|
||||
|
||||
const int destY = y + row;
|
||||
pw.beginRow(destY);
|
||||
// On a grayscale strip pass only a narrow column window of the image is in
|
||||
// the active band; skip the rest instead of unpacking+clipping every pixel.
|
||||
int colStart, colEnd;
|
||||
pw.bandColRange(x, cachedWidth, colStart, colEnd);
|
||||
for (int col = colStart; col < colEnd; col++) {
|
||||
const int byteIdx = col >> 2; // col / 4
|
||||
const int bitShift = 6 - (col & 3) * 2; // MSB first within byte
|
||||
uint8_t pixelValue = (rowBuffer[byteIdx] >> bitShift) & 0x03;
|
||||
|
||||
pw.writePixel(x + col, pixelValue);
|
||||
}
|
||||
}
|
||||
|
||||
free(readBuffer);
|
||||
LOG_DBG("IMG", "Cache render complete");
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ImageBlock::render(GfxRenderer& renderer, const int x, const int y) {
|
||||
// The font-prewarm scan pass only accumulates glyphs; an image contributes
|
||||
// none, and its DirectPixelWriter output bypasses the renderer's scan-mode
|
||||
// suppression, so it would otherwise do a full (discarded) cache render every
|
||||
// page view. Skip it here. The image still draws in the real BW/grayscale
|
||||
// passes; on first view this just moves the one-time decode to the BW pass.
|
||||
FontCacheManager* fcm = renderer.getFontCacheManager();
|
||||
if (fcm && fcm->isScanning()) return;
|
||||
|
||||
LOG_DBG("IMG", "Rendering image at %d,%d: %s (%dx%d)", x, y, imagePath.c_str(), width, height);
|
||||
|
||||
const int screenWidth = renderer.getScreenWidth();
|
||||
const int screenHeight = renderer.getScreenHeight();
|
||||
|
||||
// Bounds check render position using logical screen dimensions
|
||||
if (x < 0 || y < 0 || x + width > screenWidth || y + height > screenHeight) {
|
||||
LOG_ERR("IMG", "Invalid render position: (%d,%d) size (%dx%d) screen (%dx%d)", x, y, width, height, screenWidth,
|
||||
screenHeight);
|
||||
return;
|
||||
}
|
||||
|
||||
// Tiled grayscale (#2190): skip the whole image when it doesn't touch the
|
||||
// active band. The per-pixel writer already clips off-band pixels, but without
|
||||
// this each of the ~7 bands per plane re-ran the full cache load / pixel walk
|
||||
// and discarded the result — the dominant cost of AA on image pages. The check
|
||||
// is orientation-aware and returns true when no strip is active, so the BW
|
||||
// pass and non-tiled controllers render the image exactly as before.
|
||||
if (!renderer.glyphIntersectsStrip(x, y, x + width - 1, y + height - 1)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to render from cache first
|
||||
std::string cachePath = getCachePath(imagePath);
|
||||
if (renderFromCache(renderer, cachePath, x, y, width, height)) {
|
||||
return; // Successfully rendered from cache
|
||||
}
|
||||
|
||||
// No cache - need to decode the image
|
||||
// Check if image file exists
|
||||
HalFile file;
|
||||
if (!Storage.openFileForRead("IMG", imagePath, file)) {
|
||||
LOG_ERR("IMG", "Image file not found: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
size_t fileSize = file.size();
|
||||
file.close();
|
||||
|
||||
if (fileSize == 0) {
|
||||
LOG_ERR("IMG", "Image file is empty: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Decoding and caching: %s", imagePath.c_str());
|
||||
|
||||
RenderConfig config;
|
||||
config.x = x;
|
||||
config.y = y;
|
||||
config.maxWidth = width;
|
||||
config.maxHeight = height;
|
||||
config.useGrayscale = true;
|
||||
config.useDithering = true;
|
||||
config.performanceMode = false;
|
||||
config.useExactDimensions = true; // Use pre-calculated dimensions to avoid rounding mismatches
|
||||
config.cachePath = cachePath; // Enable caching during decode
|
||||
|
||||
ImageToFramebufferDecoder* decoder = ImageDecoderFactory::getDecoder(imagePath);
|
||||
if (!decoder) {
|
||||
LOG_ERR("IMG", "No decoder found for image: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Using %s decoder", decoder->getFormatName());
|
||||
|
||||
bool success = decoder->decodeToFramebuffer(imagePath, renderer, config);
|
||||
if (!success) {
|
||||
LOG_ERR("IMG", "Failed to decode image: %s", imagePath.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Decode successful");
|
||||
}
|
||||
|
||||
bool ImageBlock::serialize(HalFile& file) {
|
||||
serialization::writeString(file, imagePath);
|
||||
serialization::writePod(file, width);
|
||||
serialization::writePod(file, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<ImageBlock> ImageBlock::deserialize(HalFile& file) {
|
||||
std::string path;
|
||||
serialization::readString(file, path);
|
||||
int16_t w, h;
|
||||
serialization::readPod(file, w);
|
||||
serialization::readPod(file, h);
|
||||
return std::unique_ptr<ImageBlock>(new ImageBlock(path, w, h));
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#pragma once
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "Block.h"
|
||||
|
||||
class ImageBlock final : public Block {
|
||||
public:
|
||||
ImageBlock(const std::string& imagePath, int16_t width, int16_t height);
|
||||
~ImageBlock() override = default;
|
||||
|
||||
const std::string& getImagePath() const { return imagePath; }
|
||||
int16_t getWidth() const { return width; }
|
||||
int16_t getHeight() const { return height; }
|
||||
|
||||
bool imageExists() const;
|
||||
|
||||
BlockType getType() override { return IMAGE_BLOCK; }
|
||||
bool isEmpty() override { return false; }
|
||||
|
||||
void render(GfxRenderer& renderer, const int x, const int y);
|
||||
bool serialize(HalFile& file);
|
||||
static std::unique_ptr<ImageBlock> deserialize(HalFile& file);
|
||||
|
||||
private:
|
||||
std::string imagePath;
|
||||
int16_t width;
|
||||
int16_t height;
|
||||
};
|
||||
@@ -1,353 +0,0 @@
|
||||
#include "TextBlock.h"
|
||||
|
||||
#include <BidiUtils.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
#include <Memory.h>
|
||||
#include <Serialization.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
size_t TextBlock::arenaSize(const uint16_t wordCount, const bool hasFocus, const uint16_t textBytes) {
|
||||
// Layout documented in TextBlock.h: 16-bit arrays first, then 8-bit arrays, then text.
|
||||
size_t size = static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(int16_t) + sizeof(uint8_t));
|
||||
if (hasFocus) {
|
||||
size += static_cast<size_t>(wordCount) * (sizeof(uint16_t) + sizeof(uint8_t));
|
||||
}
|
||||
return size + textBytes;
|
||||
}
|
||||
|
||||
void TextBlock::bindArenaPointers() {
|
||||
uint8_t* base = arena.get();
|
||||
const size_t wc = numWords;
|
||||
textOffArr = reinterpret_cast<const uint16_t*>(base);
|
||||
xposArr = reinterpret_cast<const int16_t*>(base + wc * 2);
|
||||
size_t off = wc * 4;
|
||||
if (focusPresent) {
|
||||
focusSuffixXArr = reinterpret_cast<const uint16_t*>(base + off);
|
||||
off += wc * 2;
|
||||
}
|
||||
stylesArr = base + off;
|
||||
off += wc;
|
||||
if (focusPresent) {
|
||||
focusBoundaryArr = base + off;
|
||||
off += wc;
|
||||
}
|
||||
textArr = reinterpret_cast<const char*>(base + off);
|
||||
}
|
||||
|
||||
TextBlock::TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle)
|
||||
: blockStyle(blockStyle) {
|
||||
// Focus annotations are optional: empty vectors mean no word in this block has a split.
|
||||
// When present, they must be sized in lockstep with words[].
|
||||
const bool hasFocus = !focusBoundary.empty();
|
||||
if (words.size() != wordXpos.size() || words.size() != wordStyles.size() || words.size() > 10000 ||
|
||||
(hasFocus && (words.size() != focusBoundary.size() || words.size() != focusSuffixX.size()))) {
|
||||
LOG_ERR("TXB", "Construction failed: size mismatch (words=%u, xpos=%u, styles=%u, boundary=%u, suffixX=%u)",
|
||||
static_cast<uint32_t>(words.size()), static_cast<uint32_t>(wordXpos.size()),
|
||||
static_cast<uint32_t>(wordStyles.size()), static_cast<uint32_t>(focusBoundary.size()),
|
||||
static_cast<uint32_t>(focusSuffixX.size()));
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
numWords = static_cast<uint16_t>(words.size());
|
||||
focusPresent = hasFocus;
|
||||
if (numWords == 0) {
|
||||
return; // valid empty block, no arena
|
||||
}
|
||||
|
||||
// Pass 1: total text size, one NUL per word. A line is at most a physical
|
||||
// row of the page, so uint16_t offsets are ample; reject anything larger.
|
||||
size_t totalText = 0;
|
||||
for (const auto& w : words) totalText += w.size() + 1;
|
||||
if (totalText > UINT16_MAX) {
|
||||
LOG_ERR("TXB", "Construction failed: text size %u exceeds arena limit", static_cast<uint32_t>(totalText));
|
||||
numWords = 0;
|
||||
focusPresent = false;
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
textBytes = static_cast<uint16_t>(totalText);
|
||||
|
||||
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||
arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||
if (!arena) {
|
||||
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||
numWords = 0;
|
||||
textBytes = 0;
|
||||
focusPresent = false;
|
||||
isValid = false;
|
||||
return;
|
||||
}
|
||||
bindArenaPointers();
|
||||
|
||||
// Pass 2: fill. Mutable aliases of the const views bound above.
|
||||
auto* textOff = const_cast<uint16_t*>(textOffArr);
|
||||
auto* xpos = const_cast<int16_t*>(xposArr);
|
||||
auto* styles = const_cast<uint8_t*>(stylesArr);
|
||||
auto* text = const_cast<char*>(textArr);
|
||||
uint16_t off = 0;
|
||||
for (uint16_t i = 0; i < numWords; i++) {
|
||||
textOff[i] = off;
|
||||
xpos[i] = wordXpos[i];
|
||||
styles[i] = static_cast<uint8_t>(wordStyles[i]);
|
||||
memcpy(text + off, words[i].data(), words[i].size());
|
||||
off += static_cast<uint16_t>(words[i].size());
|
||||
text[off++] = '\0';
|
||||
}
|
||||
if (focusPresent) {
|
||||
auto* suffixX = const_cast<uint16_t*>(focusSuffixXArr);
|
||||
auto* boundary = const_cast<uint8_t*>(focusBoundaryArr);
|
||||
for (uint16_t i = 0; i < numWords; i++) {
|
||||
suffixX[i] = focusSuffixX[i];
|
||||
boundary[i] = focusBoundary[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TextBlock::render(const GfxRenderer& renderer, const int fontId, const int x, const int y) const {
|
||||
if (!isValid) {
|
||||
LOG_ERR("TXB", "Render skipped: invalid block");
|
||||
return;
|
||||
}
|
||||
|
||||
const bool scanning = renderer.isFontCacheScanning();
|
||||
const int ascender = renderer.getFontAscenderSize(fontId);
|
||||
|
||||
struct DecorationLineTracker {
|
||||
EpdFontFamily::Style style;
|
||||
int yOffset;
|
||||
int startX = -1;
|
||||
int endX = -1;
|
||||
int yPos = 0;
|
||||
|
||||
bool active() const { return startX != -1; }
|
||||
void reset() {
|
||||
startX = -1;
|
||||
endX = -1;
|
||||
yPos = 0;
|
||||
}
|
||||
};
|
||||
|
||||
DecorationLineTracker decorationLines[] = {
|
||||
{EpdFontFamily::UNDERLINE, ascender + 2},
|
||||
{EpdFontFamily::STRIKETHROUGH, ascender * 4 / 5},
|
||||
};
|
||||
|
||||
const auto flushDecoration = [&](DecorationLineTracker& line) {
|
||||
if (line.active()) {
|
||||
renderer.drawLine(line.startX, line.yPos, line.endX, line.yPos, 2, true);
|
||||
line.reset();
|
||||
}
|
||||
};
|
||||
const auto flushDecorations = [&]() {
|
||||
for (auto& line : decorationLines) {
|
||||
flushDecoration(line);
|
||||
}
|
||||
};
|
||||
|
||||
for (uint16_t i = 0; i < numWords; i++) {
|
||||
const char* word = wordText(i);
|
||||
const int wordX = xposArr[i] + x;
|
||||
const EpdFontFamily::Style currentStyle = wordStyle(i);
|
||||
const auto baseDir =
|
||||
static_cast<BidiUtils::BidiBaseDir>(BidiUtils::detectParagraphLevel(word, blockStyle.isRtl ? 1 : 0));
|
||||
const uint8_t boundary = focusBoundary(i);
|
||||
|
||||
// SUP/SUB shift the baseline passed to drawText; the glyph is also scaled 50% inside
|
||||
// drawText, so these offsets are chosen relative to the full-size ascender:
|
||||
// SUP: raise by 40% of ascender — sits clearly above the cap-height
|
||||
// SUB: lower by 25% of ascender — descends below baseline without clashing with ascenders below
|
||||
int wordY = y;
|
||||
if ((currentStyle & EpdFontFamily::SUP) != 0) {
|
||||
wordY -= ascender * 2 / 5;
|
||||
} else if ((currentStyle & EpdFontFamily::SUB) != 0) {
|
||||
wordY += ascender / 4;
|
||||
}
|
||||
|
||||
if (boundary > 0) {
|
||||
// Focus split: draw bold prefix, then the regular suffix at a pre-computed x offset.
|
||||
// The bold prefix is bounded to 9 codepoints by the clamp on targetBoldChars in
|
||||
// ParsedText::addWord; 9 UTF-8 codepoints occupy at most 9 * 4 = 36 bytes, +1 for null = 37.
|
||||
// suffixX is computed at cache-creation time to avoid font metric lookups at render time.
|
||||
static constexpr size_t MAX_FOCUS_PREFIX_BYTES = 9 * 4 + 1;
|
||||
char boldBuf[40];
|
||||
static_assert(sizeof(boldBuf) >= MAX_FOCUS_PREFIX_BYTES,
|
||||
"boldBuf too small for max focus prefix (9 codepoints * 4 UTF-8 bytes + null)");
|
||||
const auto boldStyle = static_cast<EpdFontFamily::Style>(currentStyle | EpdFontFamily::BOLD);
|
||||
const size_t boldLen =
|
||||
std::min<size_t>({static_cast<size_t>(boundary), static_cast<size_t>(wordTextLen(i)), sizeof(boldBuf) - 1});
|
||||
memcpy(boldBuf, word, boldLen);
|
||||
boldBuf[boldLen] = '\0';
|
||||
renderer.drawText(fontId, wordX, wordY, boldBuf, true, boldStyle, baseDir);
|
||||
const int suffixX = wordX + focusSuffixXArr[i];
|
||||
renderer.drawText(fontId, suffixX, wordY, word + boldLen, true, currentStyle, baseDir);
|
||||
} else {
|
||||
renderer.drawText(fontId, wordX, wordY, word, true, currentStyle, baseDir);
|
||||
}
|
||||
|
||||
if (scanning) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (EpdFontFamily::hasTextDecoration(currentStyle)) {
|
||||
int lineStartX = wordX;
|
||||
int lineWidth = renderer.getTextWidth(fontId, word, currentStyle, baseDir);
|
||||
|
||||
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
||||
lineWidth = (lineWidth + 1) / 2;
|
||||
}
|
||||
|
||||
// Do not decorate the synthetic em-space used for paragraph indentation.
|
||||
if (wordTextLen(i) >= 3 && static_cast<uint8_t>(word[0]) == 0xE2 && static_cast<uint8_t>(word[1]) == 0x80 &&
|
||||
static_cast<uint8_t>(word[2]) == 0x83) {
|
||||
const char* visibleText = word + 3;
|
||||
lineStartX += renderer.getTextAdvanceX(fontId, "\xe2\x80\x83", currentStyle);
|
||||
lineWidth = renderer.getTextWidth(fontId, visibleText, currentStyle, baseDir);
|
||||
if ((currentStyle & (EpdFontFamily::SUP | EpdFontFamily::SUB)) != 0) {
|
||||
lineWidth = (lineWidth + 1) / 2;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& line : decorationLines) {
|
||||
if ((currentStyle & line.style) == 0) {
|
||||
flushDecoration(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
const int lineY = wordY + line.yOffset;
|
||||
if (line.active() && line.yPos != lineY) {
|
||||
flushDecoration(line);
|
||||
}
|
||||
if (!line.active()) {
|
||||
line.startX = lineStartX;
|
||||
line.yPos = lineY;
|
||||
}
|
||||
line.endX = lineStartX + lineWidth;
|
||||
}
|
||||
} else {
|
||||
flushDecorations();
|
||||
}
|
||||
}
|
||||
flushDecorations();
|
||||
}
|
||||
|
||||
bool TextBlock::serialize(HalFile& file) const {
|
||||
if (!isValid) {
|
||||
LOG_ERR("TXB", "Serialization failed: invalid block");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Word data: scalars, then the arena verbatim -- its in-memory layout is
|
||||
// exactly the on-disk layout (see TextBlock.h), so one write covers all
|
||||
// per-word arrays and the text blob.
|
||||
serialization::writePod(file, numWords);
|
||||
serialization::writePod(file, static_cast<uint8_t>(focusPresent ? 1 : 0));
|
||||
serialization::writePod(file, textBytes);
|
||||
if (numWords > 0) {
|
||||
const size_t size = arenaSize(numWords, focusPresent, textBytes);
|
||||
if (file.write(arena.get(), size) != size) {
|
||||
LOG_ERR("TXB", "Serialization failed: arena write (%u bytes)", static_cast<uint32_t>(size));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
serialization::writePod(file, blockStyle.marginRight);
|
||||
serialization::writePod(file, blockStyle.paddingTop);
|
||||
serialization::writePod(file, blockStyle.paddingBottom);
|
||||
serialization::writePod(file, blockStyle.paddingLeft);
|
||||
serialization::writePod(file, blockStyle.paddingRight);
|
||||
serialization::writePod(file, blockStyle.textIndent);
|
||||
serialization::writePod(file, blockStyle.textIndentDefined);
|
||||
serialization::writePod(file, blockStyle.isRtl);
|
||||
serialization::writePod(file, blockStyle.directionDefined);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<TextBlock> TextBlock::deserialize(HalFile& file) {
|
||||
uint16_t wc;
|
||||
uint8_t hasFocus;
|
||||
uint16_t textBytes;
|
||||
serialization::readPod(file, wc);
|
||||
serialization::readPod(file, hasFocus);
|
||||
serialization::readPod(file, textBytes);
|
||||
|
||||
// Sanity checks: cap the arena allocation and reject impossible geometry
|
||||
// (every word carries at least its NUL terminator).
|
||||
if (wc > 10000) {
|
||||
LOG_ERR("TXB", "Deserialization failed: word count %u exceeds maximum", wc);
|
||||
return nullptr;
|
||||
}
|
||||
if ((wc == 0 && textBytes != 0) || (wc > 0 && textBytes < wc)) {
|
||||
LOG_ERR("TXB", "Deserialization failed: bad text size %u for %u words", textBytes, wc);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_ptr<TextBlock> block(new (std::nothrow) TextBlock());
|
||||
if (!block) {
|
||||
LOG_ERR("TXB", "OOM: TextBlock");
|
||||
return nullptr;
|
||||
}
|
||||
block->numWords = wc;
|
||||
block->textBytes = textBytes;
|
||||
block->focusPresent = hasFocus != 0;
|
||||
|
||||
if (wc > 0) {
|
||||
const size_t size = arenaSize(wc, block->focusPresent, textBytes);
|
||||
block->arena = makeUniqueNoThrow<uint8_t[]>(size);
|
||||
if (!block->arena) {
|
||||
LOG_ERR("TXB", "OOM: arena %u bytes", static_cast<uint32_t>(size));
|
||||
return nullptr;
|
||||
}
|
||||
if (file.read(block->arena.get(), size) != size) {
|
||||
LOG_ERR("TXB", "Deserialization failed: arena read (%u bytes)", static_cast<uint32_t>(size));
|
||||
return nullptr;
|
||||
}
|
||||
block->bindArenaPointers();
|
||||
|
||||
// Validate offsets before anything dereferences wordText(): offset 0 first,
|
||||
// strictly increasing, in bounds, and every word NUL-terminated (word i ends
|
||||
// at the byte before offset i+1; the last word at the last text byte).
|
||||
const uint16_t* textOff = block->textOffArr;
|
||||
const char* text = block->textArr;
|
||||
if (textOff[0] != 0 || text[textBytes - 1] != '\0') {
|
||||
LOG_ERR("TXB", "Deserialization failed: corrupt text layout");
|
||||
return nullptr;
|
||||
}
|
||||
for (uint16_t i = 1; i < wc; i++) {
|
||||
if (textOff[i] <= textOff[i - 1] || textOff[i] >= textBytes || text[textOff[i] - 1] != '\0') {
|
||||
LOG_ERR("TXB", "Deserialization failed: corrupt word offset %u", i);
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Style (alignment + margins/padding/indent)
|
||||
BlockStyle& blockStyle = block->blockStyle;
|
||||
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);
|
||||
serialization::readPod(file, blockStyle.marginRight);
|
||||
serialization::readPod(file, blockStyle.paddingTop);
|
||||
serialization::readPod(file, blockStyle.paddingBottom);
|
||||
serialization::readPod(file, blockStyle.paddingLeft);
|
||||
serialization::readPod(file, blockStyle.paddingRight);
|
||||
serialization::readPod(file, blockStyle.textIndent);
|
||||
serialization::readPod(file, blockStyle.textIndentDefined);
|
||||
serialization::readPod(file, blockStyle.isRtl);
|
||||
serialization::readPod(file, blockStyle.directionDefined);
|
||||
|
||||
return block;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
#pragma once
|
||||
#include <EpdFontFamily.h>
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Block.h"
|
||||
#include "BlockStyle.h"
|
||||
|
||||
// Represents a line of text on a page.
|
||||
//
|
||||
// All per-word data lives in ONE flat heap allocation (the arena) instead of
|
||||
// six parallel vectors: a resident page holds ~25-30 of these blocks, and the
|
||||
// vector-of-string layout cost ~250 throwing allocations per page load, which
|
||||
// was the primary driver of heap fragmentation on the ESP32-C3.
|
||||
//
|
||||
// Arena layout, in order (2-byte alignment holds by construction: all 16-bit
|
||||
// arrays come first and the arena base is allocator-aligned; RISC-V faults on
|
||||
// unaligned multi-byte access):
|
||||
// uint16_t textOff[wordCount] byte offset of word i's text in text[]
|
||||
// int16_t xpos[wordCount]
|
||||
// uint16_t focusSuffixX[wordCount] present only when focusPresent
|
||||
// uint8_t styles[wordCount]
|
||||
// uint8_t focusBoundary[wordCount] present only when focusPresent
|
||||
// char text[textBytes] all words back to back, NUL-terminated
|
||||
//
|
||||
// Each word is stored NUL-terminated so render() can hand `text + textOff[i]`
|
||||
// straight to C APIs (drawText) with no std::string materialization.
|
||||
//
|
||||
// Focus split semantics (unchanged from the vector layout): boundary N > 0
|
||||
// means the first N bytes of word i render bold, the remainder in the base
|
||||
// style. N is bounded to 9 codepoints (<= 36 UTF-8 bytes) by the clamp in
|
||||
// ParsedText::addWord. focusSuffixX is the pre-computed pixel offset from the
|
||||
// word start to the regular suffix. Both arrays are omitted from the arena
|
||||
// entirely when no word on the line has a split (zero per-word RAM cost when
|
||||
// focus reading is disabled).
|
||||
class TextBlock final : public Block {
|
||||
private:
|
||||
BlockStyle blockStyle;
|
||||
uint16_t numWords = 0;
|
||||
uint16_t textBytes = 0; // total size of the text region, including NULs
|
||||
bool focusPresent = false;
|
||||
bool isValid = true;
|
||||
// The ONLY allocation: makeUniqueNoThrow, so OOM yields an invalid block
|
||||
// instead of abort() (bare new is not nothrow with -fno-exceptions).
|
||||
std::unique_ptr<uint8_t[]> arena;
|
||||
// Typed views into the arena, bound once after the arena is filled. All
|
||||
// 16-bit bases sit at even offsets, so direct dereference is alignment-safe.
|
||||
const uint16_t* textOffArr = nullptr;
|
||||
const int16_t* xposArr = nullptr;
|
||||
const uint16_t* focusSuffixXArr = nullptr; // null when !focusPresent
|
||||
const uint8_t* stylesArr = nullptr;
|
||||
const uint8_t* focusBoundaryArr = nullptr; // null when !focusPresent
|
||||
const char* textArr = nullptr;
|
||||
|
||||
TextBlock() = default; // deserialize() fills the fields directly
|
||||
static size_t arenaSize(uint16_t wordCount, bool hasFocus, uint16_t textBytes);
|
||||
void bindArenaPointers();
|
||||
|
||||
public:
|
||||
// Flatten-on-construct: copies the layout-time vectors into the arena; the
|
||||
// vectors die with the caller. On arena OOM the block is empty and valid()
|
||||
// is false -- callers must check and fail the line instead of using it.
|
||||
explicit TextBlock(const std::vector<std::string>& words, const std::vector<int16_t>& wordXpos,
|
||||
const std::vector<EpdFontFamily::Style>& wordStyles, const std::vector<uint8_t>& focusBoundary,
|
||||
const std::vector<uint16_t>& focusSuffixX, const BlockStyle& blockStyle = BlockStyle());
|
||||
~TextBlock() override = default;
|
||||
TextBlock(const TextBlock&) = delete;
|
||||
TextBlock& operator=(const TextBlock&) = delete;
|
||||
|
||||
void setBlockStyle(const BlockStyle& blockStyle) { this->blockStyle = blockStyle; }
|
||||
const BlockStyle& getBlockStyle() const { return blockStyle; }
|
||||
bool isEmpty() override { return numWords == 0; }
|
||||
bool valid() const { return isValid; }
|
||||
uint16_t wordCount() const { return numWords; }
|
||||
// NUL-terminated by construction; safe to pass to C APIs directly.
|
||||
const char* wordText(const uint16_t i) const { return textArr + textOffArr[i]; }
|
||||
uint16_t wordTextLen(const uint16_t i) const {
|
||||
const uint16_t end = (i + 1 < numWords) ? textOffArr[i + 1] : textBytes;
|
||||
return end - textOffArr[i] - 1; // exclude the NUL
|
||||
}
|
||||
int16_t wordXpos(const uint16_t i) const { return xposArr[i]; }
|
||||
EpdFontFamily::Style wordStyle(const uint16_t i) const { return static_cast<EpdFontFamily::Style>(stylesArr[i]); }
|
||||
uint8_t focusBoundary(const uint16_t i) const { return focusPresent ? focusBoundaryArr[i] : 0; }
|
||||
uint16_t focusSuffixX(const uint16_t i) const { return focusPresent ? focusSuffixXArr[i] : 0; }
|
||||
|
||||
void render(const GfxRenderer& renderer, int fontId, int x, int y) const;
|
||||
BlockType getType() override { return TEXT_BLOCK; }
|
||||
bool serialize(HalFile& file) const;
|
||||
static std::unique_ptr<TextBlock> deserialize(HalFile& file);
|
||||
};
|
||||
@@ -1,231 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalDisplay.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cassert>
|
||||
|
||||
// Direct framebuffer writer that eliminates per-pixel overhead from the image
|
||||
// rendering hot path. Pre-computes orientation transform as linear coefficients
|
||||
// and caches render-mode state so the inner loop is: one multiply, one add,
|
||||
// one shift, and one AND per pixel — no branches, no method calls.
|
||||
//
|
||||
// Caller is responsible for ensuring (outX, outY) are within screen bounds.
|
||||
// ImageBlock::render() already validates this before entering the pixel loop,
|
||||
// and the JPEG/PNG callbacks pre-clamp destination ranges to screen bounds.
|
||||
struct DirectPixelWriter {
|
||||
uint8_t* fb;
|
||||
GfxRenderer::RenderMode mode;
|
||||
uint16_t displayWidthBytes; // Runtime framebuffer stride (X4: 100, X3: 99)
|
||||
// Active write target: for tiled grayscale, fb is the band scratch, originY is
|
||||
// the band's top physical row, and clipRows is the band height. Off-band
|
||||
// pixels are dropped. With no strip active these collapse to the full frame
|
||||
// (originY 0, clipRows panelHeight) so the clip doubles as a bounds guard.
|
||||
int originY;
|
||||
int clipRows;
|
||||
|
||||
// Orientation is collapsed into a linear transform:
|
||||
// phyX = phyXBase + x * phyXStepX + y * phyXStepY
|
||||
// phyY = phyYBase + x * phyYStepX + y * phyYStepY
|
||||
int phyXBase, phyYBase;
|
||||
int phyXStepX, phyYStepX; // per logical-X step
|
||||
int phyXStepY, phyYStepY; // per logical-Y step
|
||||
|
||||
// Row-precomputed: the Y-dependent portion of the physical coords
|
||||
int rowPhyXBase, rowPhyYBase;
|
||||
|
||||
void init(GfxRenderer& renderer) {
|
||||
fb = renderer.getWriteTarget();
|
||||
originY = renderer.getWriteOriginY();
|
||||
clipRows = renderer.getWriteRows();
|
||||
mode = renderer.getRenderMode();
|
||||
displayWidthBytes = renderer.getDisplayWidthBytes();
|
||||
|
||||
const int phyW = renderer.getDisplayWidth();
|
||||
const int phyH = renderer.getDisplayHeight();
|
||||
|
||||
switch (renderer.getOrientation()) {
|
||||
case GfxRenderer::Portrait:
|
||||
// phyX = y, phyY = (phyH-1) - x
|
||||
phyXBase = 0;
|
||||
phyYBase = phyH - 1;
|
||||
phyXStepX = 0;
|
||||
phyYStepX = -1;
|
||||
phyXStepY = 1;
|
||||
phyYStepY = 0;
|
||||
break;
|
||||
case GfxRenderer::LandscapeClockwise:
|
||||
// phyX = (phyW-1) - x, phyY = (phyH-1) - y
|
||||
phyXBase = phyW - 1;
|
||||
phyYBase = phyH - 1;
|
||||
phyXStepX = -1;
|
||||
phyYStepX = 0;
|
||||
phyXStepY = 0;
|
||||
phyYStepY = -1;
|
||||
break;
|
||||
case GfxRenderer::PortraitInverted:
|
||||
// phyX = (phyW-1) - y, phyY = x
|
||||
phyXBase = phyW - 1;
|
||||
phyYBase = 0;
|
||||
phyXStepX = 0;
|
||||
phyYStepX = 1;
|
||||
phyXStepY = -1;
|
||||
phyYStepY = 0;
|
||||
break;
|
||||
case GfxRenderer::LandscapeCounterClockwise:
|
||||
// phyX = x, phyY = y
|
||||
phyXBase = 0;
|
||||
phyYBase = 0;
|
||||
phyXStepX = 1;
|
||||
phyYStepX = 0;
|
||||
phyXStepY = 0;
|
||||
phyYStepY = 1;
|
||||
break;
|
||||
default:
|
||||
// Fallback to LandscapeCounterClockwise (identity transform)
|
||||
phyXBase = 0;
|
||||
phyYBase = 0;
|
||||
phyXStepX = 1;
|
||||
phyYStepX = 0;
|
||||
phyXStepY = 0;
|
||||
phyYStepY = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Call once per row before the column loop.
|
||||
// Pre-computes the Y-dependent portion so writePixel() only needs the X part.
|
||||
inline void beginRow(int logicalY) {
|
||||
rowPhyXBase = phyXBase + logicalY * phyXStepY;
|
||||
rowPhyYBase = phyYBase + logicalY * phyYStepY;
|
||||
}
|
||||
|
||||
// For the current row (set via beginRow), narrow [colStart, colEnd) to the
|
||||
// columns whose pixels fall inside the active strip band. writePixel() would
|
||||
// clip the rest anyway, but on a strip pass that is most of a full-page image
|
||||
// (only ~one strip-height worth of columns survive in portrait); skipping them
|
||||
// here avoids the per-pixel unpack+transform entirely. For full-frame passes
|
||||
// (clipRows == panel height) the range is unchanged. xBase is the logical X of
|
||||
// column 0; the band test mirrors writePixel(): 0 <= phyY - originY < clipRows.
|
||||
inline void bandColRange(int xBase, int width, int& colStart, int& colEnd) const {
|
||||
// init() only ever sets phyYStepX to 0, +1, or -1; the +1/-1 solve below
|
||||
// relies on that.
|
||||
assert(phyYStepX == 0 || phyYStepX == 1 || phyYStepX == -1);
|
||||
colStart = 0;
|
||||
colEnd = width;
|
||||
if (phyYStepX == 0) {
|
||||
// phyY is constant across the row: the whole row is in-band or out.
|
||||
const int sy = rowPhyYBase - originY;
|
||||
if (static_cast<unsigned>(sy) >= static_cast<unsigned>(clipRows)) colEnd = 0;
|
||||
return;
|
||||
}
|
||||
// phyY = rowPhyYBase + logicalX * phyYStepX (phyYStepX is +1 or -1).
|
||||
// Solve originY <= phyY <= originY + clipRows - 1 for logicalX.
|
||||
const int loY = originY;
|
||||
const int hiY = originY + clipRows - 1;
|
||||
int xLo, xHi;
|
||||
if (phyYStepX > 0) {
|
||||
xLo = loY - rowPhyYBase;
|
||||
xHi = hiY - rowPhyYBase;
|
||||
} else {
|
||||
xLo = rowPhyYBase - hiY;
|
||||
xHi = rowPhyYBase - loY;
|
||||
}
|
||||
const int cs = xLo - xBase;
|
||||
const int ce = xHi - xBase + 1; // exclusive
|
||||
if (cs > colStart) colStart = cs;
|
||||
if (ce < colEnd) colEnd = ce;
|
||||
if (colStart < 0) colStart = 0;
|
||||
if (colEnd > width) colEnd = width;
|
||||
if (colStart > colEnd) colStart = colEnd;
|
||||
}
|
||||
|
||||
// Write a single 2-bit dithered pixel value to the framebuffer.
|
||||
// Must be called after beginRow() for the current row.
|
||||
// No bounds checking — caller guarantees coordinates are valid.
|
||||
inline void writePixel(int logicalX, uint8_t pixelValue) const {
|
||||
// Determine whether to draw based on render mode
|
||||
bool draw;
|
||||
bool state;
|
||||
switch (mode) {
|
||||
case GfxRenderer::BW:
|
||||
draw = (pixelValue < 3);
|
||||
state = true;
|
||||
break;
|
||||
case GfxRenderer::GRAYSCALE_MSB:
|
||||
draw = (pixelValue == 1 || pixelValue == 2);
|
||||
state = false;
|
||||
break;
|
||||
case GfxRenderer::GRAYSCALE_LSB:
|
||||
draw = (pixelValue == 1);
|
||||
state = false;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
if (!draw) return;
|
||||
|
||||
const int phyX = rowPhyXBase + logicalX * phyXStepX;
|
||||
const int phyY = rowPhyYBase + logicalX * phyYStepX;
|
||||
|
||||
// Band-local row. The unsigned compare drops both off-band pixels (strip
|
||||
// mode) and any out-of-frame row (full-frame mode) in one branch.
|
||||
const int sy = phyY - originY;
|
||||
if (static_cast<unsigned>(sy) >= static_cast<unsigned>(clipRows)) return;
|
||||
|
||||
const uint16_t byteIndex = static_cast<uint16_t>(sy * displayWidthBytes + (phyX >> 3));
|
||||
const uint8_t bitMask = 1 << (7 - (phyX & 7));
|
||||
|
||||
if (state) {
|
||||
fb[byteIndex] &= ~bitMask; // Clear bit (draw black)
|
||||
} else {
|
||||
fb[byteIndex] |= bitMask; // Set bit (draw white)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Direct cache writer that eliminates per-pixel overhead from PixelCache::setPixel().
|
||||
// Pre-computes row pointer so the inner loop is just byte index + bit manipulation.
|
||||
//
|
||||
// The cache buffer is a small streaming band (e.g. 16 rows), not the full image,
|
||||
// so a band-relative row/column that lands outside it would corrupt adjacent
|
||||
// heap. This writer therefore bounds-checks every access: beginRow() invalidates
|
||||
// the row when it falls outside the band, and writePixel() drops out-of-range
|
||||
// columns. This path only runs during the single decode that populates the
|
||||
// cache, never on the screen render hot path, so the checks are cheap.
|
||||
struct DirectCacheWriter {
|
||||
uint8_t* buffer;
|
||||
int bytesPerRow;
|
||||
int bandRows;
|
||||
int originX;
|
||||
uint8_t* rowPtr; // Pre-computed for current row; nullptr if row is out of band
|
||||
|
||||
void init(uint8_t* cacheBuffer, int cacheBytesPerRow, int cacheBandRows, int cacheOriginX) {
|
||||
buffer = cacheBuffer;
|
||||
bytesPerRow = cacheBytesPerRow;
|
||||
bandRows = cacheBandRows;
|
||||
originX = cacheOriginX;
|
||||
rowPtr = nullptr;
|
||||
}
|
||||
|
||||
// Call once per row before the column loop. Drops rows outside the band.
|
||||
inline void beginRow(int screenY, int cacheOriginY) {
|
||||
const int localRow = screenY - cacheOriginY;
|
||||
rowPtr = (static_cast<unsigned>(localRow) < static_cast<unsigned>(bandRows))
|
||||
? buffer + (size_t)localRow * bytesPerRow
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
// Write a 2-bit pixel value. Drops the write if the row is out of band or the
|
||||
// column is out of range.
|
||||
inline void writePixel(int screenX, uint8_t value) const {
|
||||
if (!rowPtr) return;
|
||||
const int localX = screenX - originX;
|
||||
const int byteIdx = localX >> 2; // localX / 4
|
||||
if (static_cast<unsigned>(byteIdx) >= static_cast<unsigned>(bytesPerRow)) return;
|
||||
const int bitShift = 6 - (localX & 3) * 2; // MSB first: pixel 0 at bits 6-7
|
||||
rowPtr[byteIdx] = (rowPtr[byteIdx] & ~(0x03 << bitShift)) | ((value & 0x03) << bitShift);
|
||||
}
|
||||
};
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
// 4x4 Bayer matrix for ordered dithering
|
||||
inline const uint8_t bayer4x4[4][4] = {
|
||||
{0, 8, 2, 10},
|
||||
{12, 4, 14, 6},
|
||||
{3, 11, 1, 9},
|
||||
{15, 7, 13, 5},
|
||||
};
|
||||
|
||||
// Apply Bayer dithering and quantize to 4 levels (0-3)
|
||||
// Stateless - works correctly with any pixel processing order
|
||||
inline uint8_t applyBayerDither4Level(uint8_t gray, int x, int y) {
|
||||
int bayer = bayer4x4[y & 3][x & 3];
|
||||
int dither = (bayer - 8) * 5; // Scale to +/-40 (half of quantization step 85)
|
||||
|
||||
int adjusted = gray + dither;
|
||||
if (adjusted < 0) adjusted = 0;
|
||||
if (adjusted > 255) adjusted = 255;
|
||||
|
||||
if (adjusted < 64) return 0;
|
||||
if (adjusted < 128) return 1;
|
||||
if (adjusted < 192) return 2;
|
||||
return 3;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#include "ImageDecoderFactory.h"
|
||||
|
||||
#include <Logging.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
std::unique_ptr<JpegToFramebufferConverter> ImageDecoderFactory::jpegDecoder = nullptr;
|
||||
std::unique_ptr<PngToFramebufferConverter> ImageDecoderFactory::pngDecoder = nullptr;
|
||||
|
||||
ImageToFramebufferDecoder* ImageDecoderFactory::getDecoder(const std::string& imagePath) {
|
||||
std::string ext = imagePath;
|
||||
size_t dotPos = ext.rfind('.');
|
||||
if (dotPos != std::string::npos) {
|
||||
ext = ext.substr(dotPos);
|
||||
for (auto& c : ext) {
|
||||
c = tolower(c);
|
||||
}
|
||||
} else {
|
||||
ext = "";
|
||||
}
|
||||
|
||||
if (JpegToFramebufferConverter::supportsFormat(ext)) {
|
||||
if (!jpegDecoder) {
|
||||
jpegDecoder.reset(new JpegToFramebufferConverter());
|
||||
}
|
||||
return jpegDecoder.get();
|
||||
} else if (PngToFramebufferConverter::supportsFormat(ext)) {
|
||||
if (!pngDecoder) {
|
||||
pngDecoder.reset(new PngToFramebufferConverter());
|
||||
}
|
||||
return pngDecoder.get();
|
||||
}
|
||||
|
||||
LOG_ERR("DEC", "No decoder found for image: %s", imagePath.c_str());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ImageDecoderFactory::isFormatSupported(const std::string& imagePath) { return getDecoder(imagePath) != nullptr; }
|
||||
@@ -1,20 +0,0 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
class JpegToFramebufferConverter;
|
||||
class PngToFramebufferConverter;
|
||||
|
||||
class ImageDecoderFactory {
|
||||
public:
|
||||
// Returns non-owning pointer - factory owns the decoder lifetime
|
||||
static ImageToFramebufferDecoder* getDecoder(const std::string& imagePath);
|
||||
static bool isFormatSupported(const std::string& imagePath);
|
||||
|
||||
private:
|
||||
static std::unique_ptr<JpegToFramebufferConverter> jpegDecoder;
|
||||
static std::unique_ptr<PngToFramebufferConverter> pngDecoder;
|
||||
};
|
||||
@@ -1,17 +0,0 @@
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
#include <Logging.h>
|
||||
|
||||
bool ImageToFramebufferDecoder::validateImageDimensions(int width, int height, const std::string& format) {
|
||||
if (width * height > MAX_SOURCE_PIXELS) {
|
||||
LOG_ERR("IMG", "Image too large (%dx%d = %d pixels %s), max supported: %d pixels", width, height, width * height,
|
||||
format.c_str(), MAX_SOURCE_PIXELS);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImageToFramebufferDecoder::warnUnsupportedFeature(const std::string& feature, const std::string& imagePath) {
|
||||
LOG_ERR("IMG", "Warning: Unsupported feature '%s' in image '%s'. Image may not display correctly.", feature.c_str(),
|
||||
imagePath.c_str());
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class GfxRenderer;
|
||||
|
||||
struct ImageDimensions {
|
||||
int16_t width;
|
||||
int16_t height;
|
||||
};
|
||||
|
||||
struct RenderConfig {
|
||||
int x, y;
|
||||
int maxWidth, maxHeight;
|
||||
bool useGrayscale = true;
|
||||
bool useDithering = true;
|
||||
bool performanceMode = false;
|
||||
bool useExactDimensions = false; // If true, use maxWidth/maxHeight as exact output size (no recalculation)
|
||||
std::string cachePath; // If non-empty, decoder will write pixel cache to this path
|
||||
};
|
||||
|
||||
class ImageToFramebufferDecoder {
|
||||
public:
|
||||
virtual ~ImageToFramebufferDecoder() = default;
|
||||
|
||||
virtual bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) = 0;
|
||||
|
||||
virtual bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const = 0;
|
||||
|
||||
virtual const char* getFormatName() const = 0;
|
||||
|
||||
protected:
|
||||
// Size validation helpers
|
||||
static constexpr int MAX_SOURCE_PIXELS = 3145728; // 2048 * 1536
|
||||
|
||||
bool validateImageDimensions(int width, int height, const std::string& format);
|
||||
void warnUnsupportedFeature(const std::string& feature, const std::string& imagePath);
|
||||
};
|
||||
@@ -1,519 +0,0 @@
|
||||
#include "JpegToFramebufferConverter.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JPEGDEC.h>
|
||||
#include <Logging.h>
|
||||
#include <Memory.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
#include "DirectPixelWriter.h"
|
||||
#include "DitherUtils.h"
|
||||
#include "PixelCache.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Context struct passed through JPEGDEC callbacks to avoid global mutable state.
|
||||
// The draw callback receives this via pDraw->pUser (set by setUserPointer()).
|
||||
// The file I/O callbacks receive the HalFile* via pFile->fHandle (set by jpegOpen()).
|
||||
struct JpegContext {
|
||||
GfxRenderer* renderer{nullptr};
|
||||
const RenderConfig* config{nullptr};
|
||||
int screenWidth{0};
|
||||
int screenHeight{0};
|
||||
|
||||
// Source dimensions after JPEGDEC's built-in scaling
|
||||
int scaledSrcWidth{0};
|
||||
int scaledSrcHeight{0};
|
||||
|
||||
// Final output dimensions
|
||||
int dstWidth{0};
|
||||
int dstHeight{0};
|
||||
|
||||
// Fine scale in 16.16 fixed-point (ESP32-C3 has no FPU).
|
||||
// X and Y axes use separate scale factors: the aspect ratio of the output (dstWidth/dstHeight)
|
||||
// may differ from the source (srcWidth/srcHeight) due to integer rounding of displayHeight.
|
||||
// Using a single (X-based) scale for both axes causes the wrong srcRow to be skipped
|
||||
// during nearest-neighbor downscaling, potentially losing critical image content.
|
||||
int32_t fineScaleFPX{1 << 16}; // X: src -> dst column mapping
|
||||
int32_t invScaleFPX{1 << 16}; // X: dst -> src column mapping
|
||||
int32_t fineScaleFPY{1 << 16}; // Y: src -> dst row mapping
|
||||
int32_t invScaleFPY{1 << 16}; // Y: dst -> src row mapping
|
||||
|
||||
PixelCache cache;
|
||||
bool caching{false};
|
||||
};
|
||||
|
||||
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
||||
// avoiding the need for global file state.
|
||||
void* jpegOpen(const char* filename, int32_t* size) {
|
||||
HalFile* f = new HalFile();
|
||||
if (!Storage.openFileForRead("JPG", std::string(filename), *f)) {
|
||||
delete f;
|
||||
return nullptr;
|
||||
}
|
||||
*size = f->size();
|
||||
return f;
|
||||
}
|
||||
|
||||
void jpegClose(void* handle) {
|
||||
HalFile* f = reinterpret_cast<HalFile*>(handle);
|
||||
if (f) {
|
||||
f->close();
|
||||
delete f;
|
||||
}
|
||||
}
|
||||
|
||||
// JPEGDEC tracks file position via pFile->iPos internally (e.g. JPEGGetMoreData
|
||||
// checks iPos < iSize to decide whether more data is available). The callbacks
|
||||
// MUST maintain iPos to match the actual file position, otherwise progressive
|
||||
// JPEGs with large headers fail during parsing.
|
||||
int32_t jpegRead(JPEGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||
HalFile* f = reinterpret_cast<HalFile*>(pFile->fHandle);
|
||||
if (!f) return 0;
|
||||
int32_t bytesRead = f->read(pBuf, len);
|
||||
if (bytesRead < 0) return 0;
|
||||
pFile->iPos += bytesRead;
|
||||
return bytesRead;
|
||||
}
|
||||
|
||||
int32_t jpegSeek(JPEGFILE* pFile, int32_t pos) {
|
||||
HalFile* f = reinterpret_cast<HalFile*>(pFile->fHandle);
|
||||
if (!f) return -1;
|
||||
if (!f->seek(pos)) return -1;
|
||||
pFile->iPos = pos;
|
||||
return pos;
|
||||
}
|
||||
|
||||
// JPEGDEC object is ~17 KB due to internal decode buffers.
|
||||
// Heap-allocate on demand so memory is only used during active decode.
|
||||
constexpr size_t JPEG_DECODER_APPROX_SIZE = 20 * 1024;
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_JPEG = JPEG_DECODER_APPROX_SIZE + 16 * 1024;
|
||||
|
||||
// Choose JPEGDEC's built-in scale factor for coarse downscaling.
|
||||
// Returns the scale denominator (1, 2, 4, or 8) and sets jpegScaleOption.
|
||||
int chooseJpegScale(float targetScale, int& jpegScaleOption) {
|
||||
if (targetScale <= 0.125f) {
|
||||
jpegScaleOption = JPEG_SCALE_EIGHTH;
|
||||
return 8;
|
||||
}
|
||||
if (targetScale <= 0.25f) {
|
||||
jpegScaleOption = JPEG_SCALE_QUARTER;
|
||||
return 4;
|
||||
}
|
||||
if (targetScale <= 0.5f) {
|
||||
jpegScaleOption = JPEG_SCALE_HALF;
|
||||
return 2;
|
||||
}
|
||||
jpegScaleOption = 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Fixed-point 16.16 arithmetic avoids software float emulation on ESP32-C3 (no FPU).
|
||||
constexpr int FP_SHIFT = 16;
|
||||
constexpr int32_t FP_ONE = 1 << FP_SHIFT;
|
||||
constexpr int32_t FP_MASK = FP_ONE - 1;
|
||||
|
||||
int jpegDrawCallback(JPEGDRAW* pDraw) {
|
||||
JpegContext* ctx = reinterpret_cast<JpegContext*>(pDraw->pUser);
|
||||
if (!ctx || !ctx->config || !ctx->renderer) return 0;
|
||||
|
||||
// In EIGHT_BIT_GRAYSCALE mode, pPixels contains 8-bit grayscale values
|
||||
// Buffer is densely packed: stride = pDraw->iWidth, valid columns = pDraw->iWidthUsed
|
||||
uint8_t* pixels = reinterpret_cast<uint8_t*>(pDraw->pPixels);
|
||||
const int stride = pDraw->iWidth;
|
||||
const int validW = pDraw->iWidthUsed;
|
||||
const int blockH = pDraw->iHeight;
|
||||
|
||||
if (stride <= 0 || blockH <= 0 || validW <= 0) return 1;
|
||||
|
||||
const bool useDithering = ctx->config->useDithering;
|
||||
bool caching = ctx->caching;
|
||||
const int32_t fineScaleFPX = ctx->fineScaleFPX;
|
||||
const int32_t invScaleFPX = ctx->invScaleFPX;
|
||||
const int32_t fineScaleFPY = ctx->fineScaleFPY;
|
||||
const int32_t invScaleFPY = ctx->invScaleFPY;
|
||||
GfxRenderer& renderer = *ctx->renderer;
|
||||
const int cfgX = ctx->config->x;
|
||||
const int cfgY = ctx->config->y;
|
||||
const int blockX = pDraw->x;
|
||||
const int blockY = pDraw->y;
|
||||
|
||||
// Determine destination pixel range covered by this source block
|
||||
const int srcYEnd = blockY + blockH;
|
||||
const int srcXEnd = blockX + validW;
|
||||
|
||||
int dstYStart = (int)((int64_t)blockY * fineScaleFPY >> FP_SHIFT);
|
||||
int dstYEnd = (srcYEnd >= ctx->scaledSrcHeight) ? ctx->dstHeight : (int)((int64_t)srcYEnd * fineScaleFPY >> FP_SHIFT);
|
||||
int dstXStart = (int)((int64_t)blockX * fineScaleFPX >> FP_SHIFT);
|
||||
int dstXEnd = (srcXEnd >= ctx->scaledSrcWidth) ? ctx->dstWidth : (int)((int64_t)srcXEnd * fineScaleFPX >> FP_SHIFT);
|
||||
|
||||
// Pre-clamp destination ranges to screen bounds (eliminates per-pixel screen checks)
|
||||
int clampYMax = ctx->dstHeight;
|
||||
if (ctx->screenHeight - cfgY < clampYMax) clampYMax = ctx->screenHeight - cfgY;
|
||||
if (dstYStart < -cfgY) dstYStart = -cfgY;
|
||||
if (dstYEnd > clampYMax) dstYEnd = clampYMax;
|
||||
|
||||
int clampXMax = ctx->dstWidth;
|
||||
if (ctx->screenWidth - cfgX < clampXMax) clampXMax = ctx->screenWidth - cfgX;
|
||||
if (dstXStart < -cfgX) dstXStart = -cfgX;
|
||||
if (dstXEnd > clampXMax) dstXEnd = clampXMax;
|
||||
|
||||
if (dstYStart >= dstYEnd || dstXStart >= dstXEnd) return 1;
|
||||
|
||||
// Pre-compute orientation and render-mode state once per callback invocation
|
||||
DirectPixelWriter pw;
|
||||
pw.init(renderer);
|
||||
|
||||
// The cache streams to disk one MCU-row band at a time. Flushing rows below
|
||||
// this block (raster order guarantees they are final) repositions the band;
|
||||
// cacheOriginY then maps screen rows to the band-local buffer rows. If a flush
|
||||
// write fails, stop caching for the rest of this decode (and let finalize drop
|
||||
// the partial file) rather than writing past the band buffer.
|
||||
DirectCacheWriter cw;
|
||||
int cacheOriginY = 0;
|
||||
if (caching) {
|
||||
if (!ctx->cache.advanceTo(dstYStart)) {
|
||||
caching = false;
|
||||
ctx->caching = false;
|
||||
} else {
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||
cacheOriginY = ctx->config->y + ctx->cache.bandStart;
|
||||
}
|
||||
}
|
||||
|
||||
// === 1:1 fast path: no scaling math ===
|
||||
if (fineScaleFPX == FP_ONE && fineScaleFPY == FP_ONE) {
|
||||
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||
const int outY = cfgY + dstY;
|
||||
pw.beginRow(outY);
|
||||
if (caching) cw.beginRow(outY, cacheOriginY);
|
||||
const uint8_t* row = &pixels[(dstY - blockY) * stride];
|
||||
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
|
||||
const int outX = cfgX + dstX;
|
||||
uint8_t gray = row[dstX - blockX];
|
||||
uint8_t dithered;
|
||||
if (useDithering) {
|
||||
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// === Bilinear interpolation (upscale: fineScale > 1.0) ===
|
||||
// Smooths block boundaries that would otherwise create visible banding
|
||||
// on progressive JPEG DC-only decode (1/8 resolution upscaled to target).
|
||||
if (fineScaleFPX > FP_ONE && fineScaleFPY > FP_ONE) {
|
||||
// Pre-compute safe X range where lx0 and lx0+1 are both in [0, validW-1].
|
||||
// Only the left/right edge pixels (typically 0-2 and 1-8 respectively) need clamping.
|
||||
int safeXStart = (int)(((int64_t)blockX * fineScaleFPX + FP_MASK) >> FP_SHIFT);
|
||||
int safeXEnd = (int)((int64_t)(blockX + validW - 1) * fineScaleFPX >> FP_SHIFT);
|
||||
if (safeXStart < dstXStart) safeXStart = dstXStart;
|
||||
if (safeXEnd > dstXEnd) safeXEnd = dstXEnd;
|
||||
if (safeXStart > safeXEnd) safeXEnd = safeXStart;
|
||||
|
||||
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||
const int outY = cfgY + dstY;
|
||||
pw.beginRow(outY);
|
||||
if (caching) cw.beginRow(outY, cacheOriginY);
|
||||
const int32_t srcFyFP = dstY * invScaleFPY;
|
||||
const int32_t fy = srcFyFP & FP_MASK;
|
||||
const int32_t fyInv = FP_ONE - fy;
|
||||
int ly0 = (srcFyFP >> FP_SHIFT) - blockY;
|
||||
int ly1 = ly0 + 1;
|
||||
if (ly0 < 0) ly0 = 0;
|
||||
if (ly0 >= blockH) ly0 = blockH - 1;
|
||||
if (ly1 >= blockH) ly1 = blockH - 1;
|
||||
|
||||
const uint8_t* row0 = &pixels[ly0 * stride];
|
||||
const uint8_t* row1 = &pixels[ly1 * stride];
|
||||
|
||||
// Left edge (with X boundary clamping)
|
||||
for (int dstX = dstXStart; dstX < safeXStart; dstX++) {
|
||||
const int outX = cfgX + dstX;
|
||||
const int32_t srcFxFP = dstX * invScaleFPX;
|
||||
const int32_t fx = srcFxFP & FP_MASK;
|
||||
const int32_t fxInv = FP_ONE - fx;
|
||||
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
|
||||
int lx1 = lx0 + 1;
|
||||
if (lx0 < 0) lx0 = 0;
|
||||
if (lx1 < 0) lx1 = 0;
|
||||
if (lx0 >= validW) lx0 = validW - 1;
|
||||
if (lx1 >= validW) lx1 = validW - 1;
|
||||
|
||||
int top = ((int)row0[lx0] * fxInv + (int)row0[lx1] * fx) >> FP_SHIFT;
|
||||
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
|
||||
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
|
||||
|
||||
uint8_t dithered;
|
||||
if (useDithering) {
|
||||
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
|
||||
// Interior (no X boundary checks — lx0 and lx0+1 guaranteed in bounds)
|
||||
for (int dstX = safeXStart; dstX < safeXEnd; dstX++) {
|
||||
const int outX = cfgX + dstX;
|
||||
const int32_t srcFxFP = dstX * invScaleFPX;
|
||||
const int32_t fx = srcFxFP & FP_MASK;
|
||||
const int32_t fxInv = FP_ONE - fx;
|
||||
const int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
|
||||
|
||||
int top = ((int)row0[lx0] * fxInv + (int)row0[lx0 + 1] * fx) >> FP_SHIFT;
|
||||
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx0 + 1] * fx) >> FP_SHIFT;
|
||||
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
|
||||
|
||||
uint8_t dithered;
|
||||
if (useDithering) {
|
||||
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
|
||||
// Right edge (with X boundary clamping)
|
||||
for (int dstX = safeXEnd; dstX < dstXEnd; dstX++) {
|
||||
const int outX = cfgX + dstX;
|
||||
const int32_t srcFxFP = dstX * invScaleFPX;
|
||||
const int32_t fx = srcFxFP & FP_MASK;
|
||||
const int32_t fxInv = FP_ONE - fx;
|
||||
int lx0 = (srcFxFP >> FP_SHIFT) - blockX;
|
||||
int lx1 = lx0 + 1;
|
||||
if (lx0 >= validW) lx0 = validW - 1;
|
||||
if (lx1 >= validW) lx1 = validW - 1;
|
||||
|
||||
int top = ((int)row0[lx0] * fxInv + (int)row0[lx1] * fx) >> FP_SHIFT;
|
||||
int bot = ((int)row1[lx0] * fxInv + (int)row1[lx1] * fx) >> FP_SHIFT;
|
||||
uint8_t gray = (uint8_t)((top * fyInv + bot * fy) >> FP_SHIFT);
|
||||
|
||||
uint8_t dithered;
|
||||
if (useDithering) {
|
||||
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// === Nearest-neighbor (downscale: fineScale < 1.0) ===
|
||||
for (int dstY = dstYStart; dstY < dstYEnd; dstY++) {
|
||||
const int outY = cfgY + dstY;
|
||||
pw.beginRow(outY);
|
||||
if (caching) cw.beginRow(outY, cacheOriginY);
|
||||
const int32_t srcFyFP = dstY * invScaleFPY;
|
||||
int ly = (srcFyFP >> FP_SHIFT) - blockY;
|
||||
if (ly < 0) ly = 0;
|
||||
if (ly >= blockH) ly = blockH - 1;
|
||||
const uint8_t* row = &pixels[ly * stride];
|
||||
|
||||
for (int dstX = dstXStart; dstX < dstXEnd; dstX++) {
|
||||
const int outX = cfgX + dstX;
|
||||
const int32_t srcFxFP = dstX * invScaleFPX;
|
||||
int lx = (srcFxFP >> FP_SHIFT) - blockX;
|
||||
if (lx < 0) lx = 0;
|
||||
if (lx >= validW) lx = validW - 1;
|
||||
uint8_t gray = row[lx];
|
||||
|
||||
uint8_t dithered;
|
||||
if (useDithering) {
|
||||
dithered = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
dithered = gray / 85;
|
||||
if (dithered > 3) dithered = 3;
|
||||
}
|
||||
pw.writePixel(outX, dithered);
|
||||
if (caching) cw.writePixel(outX, dithered);
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool JpegToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_JPEG) {
|
||||
LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_JPEG);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<JPEGDEC> jpeg(new (std::nothrow) JPEGDEC());
|
||||
if (!jpeg) {
|
||||
LOG_ERR("JPG", "Failed to allocate JPEG decoder for dimensions");
|
||||
return false;
|
||||
}
|
||||
|
||||
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, nullptr);
|
||||
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
|
||||
if (rc != 1) {
|
||||
LOG_ERR("JPG", "Failed to open JPEG for dimensions (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = jpeg->getWidth();
|
||||
out.height = jpeg->getHeight();
|
||||
LOG_DBG("JPG", "Image dimensions: %dx%d", out.width, out.height);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JpegToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||
const RenderConfig& config) {
|
||||
LOG_DBG("JPG", "Decoding JPEG: %s", imagePath.c_str());
|
||||
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_JPEG) {
|
||||
LOG_ERR("JPG", "Not enough heap for JPEG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_JPEG);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<JPEGDEC> jpeg(new (std::nothrow) JPEGDEC());
|
||||
if (!jpeg) {
|
||||
LOG_ERR("JPG", "Failed to allocate JPEG decoder");
|
||||
return false;
|
||||
}
|
||||
|
||||
JpegContext ctx;
|
||||
ctx.renderer = &renderer;
|
||||
ctx.config = &config;
|
||||
ctx.screenWidth = renderer.getScreenWidth();
|
||||
ctx.screenHeight = renderer.getScreenHeight();
|
||||
|
||||
int rc = jpeg->open(imagePath.c_str(), jpegOpen, jpegClose, jpegRead, jpegSeek, jpegDrawCallback);
|
||||
const ScopedCleanup cleanup{[&jpeg]() { jpeg->close(); }};
|
||||
if (rc != 1) {
|
||||
LOG_ERR("JPG", "Failed to open JPEG (err=%d): %s", jpeg->getLastError(), imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
int srcWidth = jpeg->getWidth();
|
||||
int srcHeight = jpeg->getHeight();
|
||||
|
||||
if (srcWidth <= 0 || srcHeight <= 0) {
|
||||
LOG_ERR("JPG", "Invalid JPEG dimensions: %dx%d", srcWidth, srcHeight);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateImageDimensions(srcWidth, srcHeight, "JPEG")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isProgressive = jpeg->getJPEGType() == JPEG_MODE_PROGRESSIVE;
|
||||
if (isProgressive) {
|
||||
LOG_INF("JPG", "Progressive JPEG detected - decoding DC coefficients only (lower quality)");
|
||||
}
|
||||
|
||||
// Calculate overall target scale
|
||||
float targetScale;
|
||||
int destWidth, destHeight;
|
||||
|
||||
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||
destWidth = config.maxWidth;
|
||||
destHeight = config.maxHeight;
|
||||
targetScale = (float)destWidth / srcWidth;
|
||||
} else {
|
||||
float scaleX = (config.maxWidth > 0 && srcWidth > config.maxWidth) ? (float)config.maxWidth / srcWidth : 1.0f;
|
||||
float scaleY = (config.maxHeight > 0 && srcHeight > config.maxHeight) ? (float)config.maxHeight / srcHeight : 1.0f;
|
||||
targetScale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (targetScale > 1.0f) targetScale = 1.0f;
|
||||
|
||||
destWidth = (int)(srcWidth * targetScale);
|
||||
destHeight = (int)(srcHeight * targetScale);
|
||||
}
|
||||
|
||||
// Choose JPEGDEC built-in scaling for coarse downscaling.
|
||||
// Progressive JPEGs: JPEGDEC forces JPEG_SCALE_EIGHTH internally (DC-only
|
||||
// decode produces 1/8 resolution). We must match this to avoid the if/else
|
||||
// priority chain in DecodeJPEG selecting a different scale.
|
||||
int jpegScaleOption;
|
||||
int jpegScaleDenom;
|
||||
if (isProgressive) {
|
||||
jpegScaleOption = JPEG_SCALE_EIGHTH;
|
||||
jpegScaleDenom = 8;
|
||||
} else {
|
||||
jpegScaleDenom = chooseJpegScale(targetScale, jpegScaleOption);
|
||||
}
|
||||
|
||||
if (destWidth <= 0 || destHeight <= 0) {
|
||||
LOG_ERR("JPG", "Degenerate output dimensions %dx%d for %s, skipping render", destWidth, destHeight,
|
||||
imagePath.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx.scaledSrcWidth = (srcWidth + jpegScaleDenom - 1) / jpegScaleDenom;
|
||||
ctx.scaledSrcHeight = (srcHeight + jpegScaleDenom - 1) / jpegScaleDenom;
|
||||
ctx.dstWidth = destWidth;
|
||||
ctx.dstHeight = destHeight;
|
||||
ctx.fineScaleFPX = (int32_t)((int64_t)destWidth * FP_ONE / ctx.scaledSrcWidth);
|
||||
ctx.invScaleFPX = (int32_t)((int64_t)ctx.scaledSrcWidth * FP_ONE / destWidth);
|
||||
ctx.fineScaleFPY = (int32_t)((int64_t)destHeight * FP_ONE / ctx.scaledSrcHeight);
|
||||
ctx.invScaleFPY = (int32_t)((int64_t)ctx.scaledSrcHeight * FP_ONE / destHeight);
|
||||
|
||||
LOG_DBG("JPG", "JPEG %dx%d -> %dx%d (scale %.2f, jpegScale 1/%d, fineScale %.2f)%s", srcWidth, srcHeight, destWidth,
|
||||
destHeight, targetScale, jpegScaleDenom, (float)destWidth / ctx.scaledSrcWidth,
|
||||
isProgressive ? " [progressive]" : "");
|
||||
|
||||
// Set pixel type to 8-bit grayscale (must be after open())
|
||||
jpeg->setPixelType(EIGHT_BIT_GRAYSCALE);
|
||||
jpeg->setUserPointer(&ctx);
|
||||
|
||||
// Start streaming the pixel cache to disk. The band only needs to hold the
|
||||
// tallest single decode block: a JPEGDEC MCU cell is at most 16 scaled-source
|
||||
// rows tall, which our fine scale maps to this many output rows.
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
if (ctx.caching) {
|
||||
const int maxBlockDstRows = (int)(((int64_t)16 * ctx.fineScaleFPY) >> FP_SHIFT) + 2;
|
||||
if (!ctx.cache.begin(config.cachePath, destWidth, destHeight, config.x, config.y, maxBlockDstRows)) {
|
||||
LOG_ERR("JPG", "Failed to start cache stream, continuing without caching");
|
||||
ctx.caching = false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long decodeStart = millis();
|
||||
rc = jpeg->decode(0, 0, jpegScaleOption);
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
if (rc != 1) {
|
||||
LOG_ERR("JPG", "Decode failed (rc=%d, lastError=%d)", rc, jpeg->getLastError());
|
||||
if (ctx.caching) ctx.cache.abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("JPG", "JPEG decoding complete - render time: %lu ms", decodeTime);
|
||||
|
||||
// Finalize the streamed cache file. Note: a flush failure mid-decode clears
|
||||
// ctx.caching (the partial file is dropped), so re-read the flag here.
|
||||
if (ctx.caching) {
|
||||
ctx.cache.finalize();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool JpegToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||
return FsHelpers::hasJpgExtension(extension);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
class JpegToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||
public:
|
||||
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||
|
||||
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||
|
||||
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||
return getDimensionsStatic(imagePath, dims);
|
||||
}
|
||||
|
||||
static bool supportsFormat(const std::string& extension);
|
||||
const char* getFormatName() const override { return "JPEG"; }
|
||||
};
|
||||
@@ -1,187 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
// Streaming cache writer for 2-bit pixels (4 levels). Packs 4 pixels per byte,
|
||||
// MSB first.
|
||||
//
|
||||
// The .pxc file is written incrementally in small row bands rather than holding
|
||||
// the whole decoded image in one heap buffer. A full-page image (e.g. 482x728)
|
||||
// needs ~88KB packed, which will not fit alongside the ~20KB JPEG decoder on a
|
||||
// fragmented 380KB heap (free heap is routinely ~55KB on an image page). When
|
||||
// the cache cannot be written, every render pass re-decodes the JPEG from
|
||||
// scratch; an anti-aliased image page renders ~14 times (BW + AA restore + two
|
||||
// grayscale planes x ~6 strips), so a 2s decode becomes a ~30s freeze / watchdog
|
||||
// reset. Streaming keeps the working set to a single MCU-row band, so caching
|
||||
// succeeds and the image is decoded exactly once.
|
||||
//
|
||||
// Correctness relies on JPEGDEC delivering blocks in raster MCU order (outer
|
||||
// loop over y, inner over x: see jpeg.inl DecodeJPEG). Consecutive MCU rows map
|
||||
// to contiguous, non-overlapping destination row ranges, so once a block whose
|
||||
// top row is Y arrives, every output row < Y is final and is flushed to disk.
|
||||
struct PixelCache {
|
||||
uint8_t* buffer; // band buffer: (bandRows + 1) rows; last row kept zeroed
|
||||
uint8_t* zeroRow; // points at the spare zeroed row, for gap/clip fill
|
||||
int width;
|
||||
int height;
|
||||
int bytesPerRow;
|
||||
int originX; // config.x - to convert screen coords to cache coords
|
||||
int originY; // config.y
|
||||
int bandRows; // rows held in the band buffer
|
||||
int bandStart; // image-local row index of band buffer row 0
|
||||
int flushedRows; // image-local rows already written to file
|
||||
HalFile file;
|
||||
std::string cachePathStr;
|
||||
bool ok;
|
||||
|
||||
PixelCache()
|
||||
: buffer(nullptr),
|
||||
zeroRow(nullptr),
|
||||
width(0),
|
||||
height(0),
|
||||
bytesPerRow(0),
|
||||
originX(0),
|
||||
originY(0),
|
||||
bandRows(0),
|
||||
bandStart(0),
|
||||
flushedRows(0),
|
||||
ok(false) {}
|
||||
PixelCache(const PixelCache&) = delete;
|
||||
PixelCache& operator=(const PixelCache&) = delete;
|
||||
|
||||
static constexpr int MIN_BAND_ROWS = 16;
|
||||
static constexpr size_t MAX_BAND_BYTES = 24 * 1024; // band working-set ceiling
|
||||
|
||||
// Open the cache file, write the header, and allocate a band buffer big enough
|
||||
// to hold the tallest single decode block (maxBlockDstRows output rows).
|
||||
bool begin(const std::string& cachePath, int w, int h, int ox, int oy, int maxBlockDstRows) {
|
||||
width = w;
|
||||
height = h;
|
||||
originX = ox;
|
||||
originY = oy;
|
||||
bytesPerRow = (w + 3) / 4; // 2 bits per pixel, 4 pixels per byte
|
||||
bandStart = 0;
|
||||
flushedRows = 0;
|
||||
ok = false;
|
||||
|
||||
int wantRows = maxBlockDstRows + 2;
|
||||
if (wantRows < MIN_BAND_ROWS) wantRows = MIN_BAND_ROWS;
|
||||
if (wantRows > h) wantRows = h;
|
||||
|
||||
size_t maxRowsByMem = MAX_BAND_BYTES / (size_t)bytesPerRow;
|
||||
if (maxRowsByMem < 1) maxRowsByMem = 1;
|
||||
if ((size_t)wantRows > maxRowsByMem) wantRows = (int)maxRowsByMem;
|
||||
|
||||
// A single decode block must fit inside the band, otherwise streaming would
|
||||
// drop rows. This only fails for pathological upscales that could not be
|
||||
// cached at all; fall back to the no-cache path.
|
||||
if (wantRows < maxBlockDstRows) {
|
||||
LOG_ERR("IMG", "Cache band too small (%d < %d rows) for %dx%d", wantRows, maxBlockDstRows, w, h);
|
||||
return false;
|
||||
}
|
||||
bandRows = wantRows;
|
||||
|
||||
const size_t bufSize = (size_t)(bandRows + 1) * bytesPerRow; // +1 spare zero row
|
||||
buffer = (uint8_t*)malloc(bufSize);
|
||||
if (!buffer) {
|
||||
LOG_ERR("IMG", "OOM cache band: %u bytes", (unsigned)bufSize);
|
||||
return false;
|
||||
}
|
||||
memset(buffer, 0, bufSize);
|
||||
zeroRow = buffer + (size_t)bandRows * bytesPerRow;
|
||||
|
||||
if (!Storage.openFileForWrite("IMG", cachePath, file)) {
|
||||
LOG_ERR("IMG", "Failed to open cache file for writing: %s", cachePath.c_str());
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
return false;
|
||||
}
|
||||
cachePathStr = cachePath;
|
||||
|
||||
uint16_t w16 = (uint16_t)w;
|
||||
uint16_t h16 = (uint16_t)h;
|
||||
if (file.write(&w16, 2) != 2 || file.write(&h16, 2) != 2) {
|
||||
LOG_ERR("IMG", "Failed to write cache header: %s", cachePath.c_str());
|
||||
abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("IMG", "Cache stream started: %s (%dx%d, band %d rows)", cachePath.c_str(), w, h, bandRows);
|
||||
ok = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flush every output row below newTopRow (they are final in raster order) and
|
||||
// reposition the band to start at newTopRow. Returns false if a write failed,
|
||||
// in which case the caller must stop caching for the rest of the decode.
|
||||
bool advanceTo(int newTopRow) {
|
||||
if (!ok) return false;
|
||||
if (newTopRow <= bandStart) return true;
|
||||
if (newTopRow > height) newTopRow = height;
|
||||
|
||||
for (int r = bandStart; r < newTopRow; ++r) {
|
||||
const int idx = r - bandStart;
|
||||
const uint8_t* rowPtr = (idx < bandRows) ? (buffer + (size_t)idx * bytesPerRow) : zeroRow;
|
||||
if (file.write(rowPtr, (size_t)bytesPerRow) != (size_t)bytesPerRow) {
|
||||
LOG_ERR("IMG", "Cache write error at row %d", r);
|
||||
ok = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
flushedRows = newTopRow;
|
||||
bandStart = newTopRow;
|
||||
memset(buffer, 0, (size_t)bandRows * bytesPerRow); // fresh band (gaps stay black)
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flush the final band and zero-fill any rows never covered (image clipped by
|
||||
// the screen), then close the file.
|
||||
bool finalize() {
|
||||
if (!ok) {
|
||||
abort();
|
||||
return false;
|
||||
}
|
||||
for (int r = flushedRows; r < height; ++r) {
|
||||
const int idx = r - bandStart;
|
||||
const uint8_t* rowPtr = (idx >= 0 && idx < bandRows) ? (buffer + (size_t)idx * bytesPerRow) : zeroRow;
|
||||
if (file.write(rowPtr, (size_t)bytesPerRow) != (size_t)bytesPerRow) {
|
||||
LOG_ERR("IMG", "Cache write error at row %d", r);
|
||||
abort();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
LOG_DBG("IMG", "Cache written: %s (%dx%d, %d bytes)", cachePathStr.c_str(), width, height,
|
||||
4 + bytesPerRow * height);
|
||||
ok = false; // file handed off; nothing left to clean up
|
||||
return true;
|
||||
}
|
||||
|
||||
// Drop a partial/failed cache so a later decode re-creates it cleanly.
|
||||
void abort() {
|
||||
if (file.isOpen()) file.close();
|
||||
if (!cachePathStr.empty()) {
|
||||
Storage.remove(cachePathStr.c_str());
|
||||
}
|
||||
ok = false;
|
||||
}
|
||||
|
||||
~PixelCache() {
|
||||
if (file.isOpen()) {
|
||||
// The file is still open, so neither finalize() nor abort() ran, or a
|
||||
// mid-stream write failed (advanceTo() cleared ok but left the file open).
|
||||
// Drop the partial cache so we leave no corrupt file behind.
|
||||
abort();
|
||||
}
|
||||
if (buffer) {
|
||||
free(buffer);
|
||||
buffer = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,399 +0,0 @@
|
||||
#include "PngToFramebufferConverter.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Memory.h>
|
||||
#include <PNGdec.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
#include "DirectPixelWriter.h"
|
||||
#include "DitherUtils.h"
|
||||
#include "PixelCache.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Context struct passed through PNGdec callbacks to avoid global mutable state.
|
||||
// The draw callback receives this via pDraw->pUser (set by png.decode()).
|
||||
// The file I/O callbacks receive the HalFile* via pFile->fHandle (set by pngOpen()).
|
||||
struct PngContext {
|
||||
GfxRenderer* renderer{nullptr};
|
||||
const RenderConfig* config{nullptr};
|
||||
int screenWidth{0};
|
||||
int screenHeight{0};
|
||||
|
||||
// Scaling state
|
||||
float scale{1.f};
|
||||
int srcWidth{0};
|
||||
int srcHeight{0};
|
||||
int dstWidth{0};
|
||||
int dstHeight{0};
|
||||
int lastDstY{-1}; // Track last rendered destination Y to avoid duplicates
|
||||
|
||||
PixelCache cache;
|
||||
bool caching{false};
|
||||
|
||||
uint8_t* grayLineBuffer{nullptr};
|
||||
};
|
||||
|
||||
// File I/O callbacks use pFile->fHandle to access the HalFile*,
|
||||
// avoiding the need for global file state.
|
||||
void* pngOpenWithHandle(const char* filename, int32_t* size) {
|
||||
HalFile* f = new HalFile();
|
||||
if (!Storage.openFileForRead("PNG", std::string(filename), *f)) {
|
||||
delete f;
|
||||
return nullptr;
|
||||
}
|
||||
*size = f->size();
|
||||
return f;
|
||||
}
|
||||
|
||||
void pngCloseWithHandle(void* handle) {
|
||||
HalFile* f = reinterpret_cast<HalFile*>(handle);
|
||||
if (f) {
|
||||
f->close();
|
||||
delete f;
|
||||
}
|
||||
}
|
||||
|
||||
int32_t pngReadWithHandle(PNGFILE* pFile, uint8_t* pBuf, int32_t len) {
|
||||
HalFile* f = reinterpret_cast<HalFile*>(pFile->fHandle);
|
||||
if (!f) return 0;
|
||||
return f->read(pBuf, len);
|
||||
}
|
||||
|
||||
int32_t pngSeekWithHandle(PNGFILE* pFile, int32_t pos) {
|
||||
HalFile* f = reinterpret_cast<HalFile*>(pFile->fHandle);
|
||||
if (!f) return -1;
|
||||
return f->seek(pos);
|
||||
}
|
||||
|
||||
// The PNG decoder (PNGdec) is ~42 KB due to internal zlib decompression buffers.
|
||||
// We heap-allocate it on demand rather than using a static instance, so this memory
|
||||
// is only consumed while actually decoding/querying PNG images. This is critical on
|
||||
// the ESP32-C3 where total RAM is ~320 KB.
|
||||
constexpr size_t PNG_DECODER_APPROX_SIZE = 44 * 1024; // ~42 KB + overhead
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_PNG = PNG_DECODER_APPROX_SIZE + 16 * 1024; // decoder + 16 KB headroom
|
||||
|
||||
// PNGdec keeps TWO scanlines in its internal ucPixels buffer (current + previous)
|
||||
// and each scanline includes a leading filter byte.
|
||||
// Required storage is therefore approximately: 2 * (pitch + 1) + alignment slack.
|
||||
// If PNG_MAX_BUFFERED_PIXELS is smaller than this requirement for a given image,
|
||||
// PNGdec can overrun its internal buffer before our draw callback executes.
|
||||
int bytesPerPixelFromType(int pixelType) {
|
||||
switch (pixelType) {
|
||||
case PNG_PIXEL_TRUECOLOR:
|
||||
return 3;
|
||||
case PNG_PIXEL_GRAY_ALPHA:
|
||||
return 2;
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||
return 4;
|
||||
case PNG_PIXEL_GRAYSCALE:
|
||||
case PNG_PIXEL_INDEXED:
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
int requiredPngInternalBufferBytes(int srcWidth, int pixelType) {
|
||||
// +1 filter byte per scanline, *2 for current+previous lines, +32 for alignment margin.
|
||||
int pitch = srcWidth * bytesPerPixelFromType(pixelType);
|
||||
return ((pitch + 1) * 2) + 32;
|
||||
}
|
||||
|
||||
// Convert entire source line to grayscale with alpha blending to white background.
|
||||
// For indexed PNGs with tRNS chunk, alpha values are stored at palette[768] onwards.
|
||||
// Processing the whole line at once improves cache locality and reduces per-pixel overhead.
|
||||
void convertLineToGray(uint8_t* pPixels, uint8_t* grayLine, int width, int pixelType, uint8_t* palette, int hasAlpha) {
|
||||
switch (pixelType) {
|
||||
case PNG_PIXEL_GRAYSCALE:
|
||||
memcpy(grayLine, pPixels, width);
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_INDEXED:
|
||||
if (palette) {
|
||||
if (hasAlpha) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t idx = pPixels[x];
|
||||
uint8_t* p = &palette[idx * 3];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = palette[768 + idx];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
} else {
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &palette[pPixels[x] * 3];
|
||||
grayLine[x] = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
memcpy(grayLine, pPixels, width);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_GRAY_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t gray = pPixels[x * 2];
|
||||
uint8_t alpha = pPixels[x * 2 + 1];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
break;
|
||||
|
||||
case PNG_PIXEL_TRUECOLOR_ALPHA:
|
||||
for (int x = 0; x < width; x++) {
|
||||
uint8_t* p = &pPixels[x * 4];
|
||||
uint8_t gray = (uint8_t)((p[0] * 77 + p[1] * 150 + p[2] * 29) >> 8);
|
||||
uint8_t alpha = p[3];
|
||||
grayLine[x] = (uint8_t)((gray * alpha + 255 * (255 - alpha)) / 255);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
memset(grayLine, 128, width);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int pngDrawCallback(PNGDRAW* pDraw) {
|
||||
PngContext* ctx = reinterpret_cast<PngContext*>(pDraw->pUser);
|
||||
if (!ctx || !ctx->config || !ctx->renderer || !ctx->grayLineBuffer) return 0;
|
||||
|
||||
int srcY = pDraw->y;
|
||||
int srcWidth = ctx->srcWidth;
|
||||
|
||||
// Calculate destination Y with scaling
|
||||
int dstY = (int)(srcY * ctx->scale);
|
||||
|
||||
// Skip if we already rendered this destination row (multiple source rows map to same dest)
|
||||
if (dstY == ctx->lastDstY) return 1;
|
||||
ctx->lastDstY = dstY;
|
||||
|
||||
// Check bounds
|
||||
if (dstY >= ctx->dstHeight) return 1;
|
||||
|
||||
int outY = ctx->config->y + dstY;
|
||||
if (outY >= ctx->screenHeight) return 1;
|
||||
|
||||
// Convert entire source line to grayscale (improves cache locality)
|
||||
convertLineToGray(pDraw->pPixels, ctx->grayLineBuffer, srcWidth, pDraw->iPixelType, pDraw->pPalette,
|
||||
pDraw->iHasAlpha);
|
||||
|
||||
// Render scaled row using Bresenham-style integer stepping (no floating-point division)
|
||||
int dstWidth = ctx->dstWidth;
|
||||
int outXBase = ctx->config->x;
|
||||
int screenWidth = ctx->screenWidth;
|
||||
bool useDithering = ctx->config->useDithering;
|
||||
bool caching = ctx->caching;
|
||||
|
||||
// Pre-compute orientation and render-mode state once per row
|
||||
DirectPixelWriter pw;
|
||||
pw.init(*ctx->renderer);
|
||||
pw.beginRow(outY);
|
||||
|
||||
// The cache streams to disk one row at a time. Flushing rows below this one
|
||||
// (PNGdec delivers scanlines top to bottom) repositions the single-row band.
|
||||
// A flush failure stops caching for the rest of the decode so we never write
|
||||
// past the band buffer; finalize() then drops the partial file.
|
||||
DirectCacheWriter cw;
|
||||
if (caching) {
|
||||
if (!ctx->cache.advanceTo(dstY)) {
|
||||
caching = false;
|
||||
ctx->caching = false;
|
||||
} else {
|
||||
cw.init(ctx->cache.buffer, ctx->cache.bytesPerRow, ctx->cache.bandRows, ctx->cache.originX);
|
||||
cw.beginRow(outY, ctx->config->y + ctx->cache.bandStart);
|
||||
}
|
||||
}
|
||||
|
||||
int srcX = 0;
|
||||
int error = 0;
|
||||
|
||||
for (int dstX = 0; dstX < dstWidth; dstX++) {
|
||||
int outX = outXBase + dstX;
|
||||
if (outX < screenWidth) {
|
||||
uint8_t gray = ctx->grayLineBuffer[srcX];
|
||||
|
||||
uint8_t ditheredGray;
|
||||
if (useDithering) {
|
||||
ditheredGray = applyBayerDither4Level(gray, outX, outY);
|
||||
} else {
|
||||
ditheredGray = gray / 85;
|
||||
if (ditheredGray > 3) ditheredGray = 3;
|
||||
}
|
||||
pw.writePixel(outX, ditheredGray);
|
||||
if (caching) cw.writePixel(outX, ditheredGray);
|
||||
}
|
||||
|
||||
// Bresenham-style stepping: advance srcX based on ratio srcWidth/dstWidth
|
||||
error += srcWidth;
|
||||
while (error >= dstWidth) {
|
||||
error -= dstWidth;
|
||||
srcX++;
|
||||
}
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PngToFramebufferConverter::getDimensionsStatic(const std::string& imagePath, ImageDimensions& out) {
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<PNG> png(new (std::nothrow) PNG());
|
||||
if (!png) {
|
||||
LOG_ERR("PNG", "Failed to allocate PNG decoder for dimensions");
|
||||
return false;
|
||||
}
|
||||
|
||||
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||
nullptr);
|
||||
const ScopedCleanup cleanup{[&png]() { png->close(); }};
|
||||
|
||||
if (rc != 0) {
|
||||
LOG_ERR("PNG", "Failed to open PNG for dimensions: %d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
out.width = png->getWidth();
|
||||
out.height = png->getHeight();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PngToFramebufferConverter::decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer,
|
||||
const RenderConfig& config) {
|
||||
LOG_DBG("PNG", "Decoding PNG: %s", imagePath.c_str());
|
||||
|
||||
size_t freeHeap = ESP.getFreeHeap();
|
||||
if (freeHeap < MIN_FREE_HEAP_FOR_PNG) {
|
||||
LOG_ERR("PNG", "Not enough heap for PNG decoder (%u free, need %u)", freeHeap, MIN_FREE_HEAP_FOR_PNG);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Heap-allocate PNG decoder (~42 KB) - freed at end of function
|
||||
std::unique_ptr<PNG> png(new (std::nothrow) PNG());
|
||||
if (!png) {
|
||||
LOG_ERR("PNG", "Failed to allocate PNG decoder");
|
||||
return false;
|
||||
}
|
||||
|
||||
PngContext ctx;
|
||||
ctx.renderer = &renderer;
|
||||
ctx.config = &config;
|
||||
ctx.screenWidth = renderer.getScreenWidth();
|
||||
ctx.screenHeight = renderer.getScreenHeight();
|
||||
|
||||
int rc = png->open(imagePath.c_str(), pngOpenWithHandle, pngCloseWithHandle, pngReadWithHandle, pngSeekWithHandle,
|
||||
pngDrawCallback);
|
||||
const ScopedCleanup cleanup{[&png]() { png->close(); }};
|
||||
if (rc != PNG_SUCCESS) {
|
||||
LOG_ERR("PNG", "Failed to open PNG: %d", rc);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!validateImageDimensions(png->getWidth(), png->getHeight(), "PNG")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Calculate output dimensions
|
||||
ctx.srcWidth = png->getWidth();
|
||||
ctx.srcHeight = png->getHeight();
|
||||
|
||||
if (config.useExactDimensions && config.maxWidth > 0 && config.maxHeight > 0) {
|
||||
// Use exact dimensions as specified (avoids rounding mismatches with pre-calculated sizes)
|
||||
ctx.dstWidth = config.maxWidth;
|
||||
ctx.dstHeight = config.maxHeight;
|
||||
ctx.scale = (float)ctx.dstWidth / ctx.srcWidth;
|
||||
} else {
|
||||
// Calculate scale factor to fit within maxWidth/maxHeight
|
||||
float scaleX = (float)config.maxWidth / ctx.srcWidth;
|
||||
float scaleY = (float)config.maxHeight / ctx.srcHeight;
|
||||
ctx.scale = (scaleX < scaleY) ? scaleX : scaleY;
|
||||
if (ctx.scale > 1.0f) ctx.scale = 1.0f; // Don't upscale
|
||||
|
||||
ctx.dstWidth = (int)(ctx.srcWidth * ctx.scale);
|
||||
ctx.dstHeight = (int)(ctx.srcHeight * ctx.scale);
|
||||
}
|
||||
ctx.lastDstY = -1; // Reset row tracking
|
||||
|
||||
LOG_DBG("PNG", "PNG %dx%d -> %dx%d (scale %.2f), bpp: %d", ctx.srcWidth, ctx.srcHeight, ctx.dstWidth, ctx.dstHeight,
|
||||
ctx.scale, png->getBpp());
|
||||
|
||||
const int pixelType = png->getPixelType();
|
||||
const int requiredInternal = requiredPngInternalBufferBytes(ctx.srcWidth, pixelType);
|
||||
if (requiredInternal > PNG_MAX_BUFFERED_PIXELS) {
|
||||
LOG_ERR("PNG",
|
||||
"PNG row buffer too small: need %d bytes for width=%d type=%d, configured PNG_MAX_BUFFERED_PIXELS=%d",
|
||||
requiredInternal, ctx.srcWidth, pixelType, PNG_MAX_BUFFERED_PIXELS);
|
||||
LOG_ERR("PNG", "Aborting decode to avoid PNGdec internal buffer overflow");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (png->getBpp() != 8) {
|
||||
warnUnsupportedFeature("bit depth (" + std::to_string(png->getBpp()) + "bpp)", imagePath);
|
||||
}
|
||||
|
||||
// Allocate grayscale line buffer on demand (~3.2 KB) - freed after decode
|
||||
const size_t grayBufSize = PNG_MAX_BUFFERED_PIXELS / 2;
|
||||
ctx.grayLineBuffer = static_cast<uint8_t*>(malloc(grayBufSize));
|
||||
if (!ctx.grayLineBuffer) {
|
||||
LOG_ERR("PNG", "Failed to allocate gray line buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Stream the pixel cache to disk. PNGdec delivers source scanlines top to
|
||||
// bottom and we emit at most one (downscaled) output row per callback, so the
|
||||
// band only needs a single row. Streaming keeps the working set tiny, so
|
||||
// unlike the old full-image buffer it neither competes with the ~44KB decoder
|
||||
// nor forces larger images to skip caching - which previously meant a full
|
||||
// re-decode on every one of an image page's ~14 render passes.
|
||||
ctx.caching = !config.cachePath.empty();
|
||||
if (ctx.caching) {
|
||||
if (!ctx.cache.begin(config.cachePath, ctx.dstWidth, ctx.dstHeight, config.x, config.y, 1)) {
|
||||
LOG_ERR("PNG", "Failed to start cache stream, continuing without caching");
|
||||
ctx.caching = false;
|
||||
}
|
||||
}
|
||||
|
||||
unsigned long decodeStart = millis();
|
||||
rc = png->decode(&ctx, 0);
|
||||
unsigned long decodeTime = millis() - decodeStart;
|
||||
|
||||
free(ctx.grayLineBuffer);
|
||||
ctx.grayLineBuffer = nullptr;
|
||||
|
||||
if (rc != PNG_SUCCESS) {
|
||||
LOG_ERR("PNG", "Decode failed: %d", rc);
|
||||
if (ctx.caching) ctx.cache.abort();
|
||||
return false;
|
||||
}
|
||||
|
||||
LOG_DBG("PNG", "PNG decoding complete - render time: %lu ms", decodeTime);
|
||||
|
||||
// Finalize the streamed cache (caching may have been cleared on a flush error).
|
||||
if (ctx.caching) {
|
||||
ctx.cache.finalize();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PngToFramebufferConverter::supportsFormat(const std::string& extension) {
|
||||
return FsHelpers::hasPngExtension(extension);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "ImageToFramebufferDecoder.h"
|
||||
|
||||
class PngToFramebufferConverter final : public ImageToFramebufferDecoder {
|
||||
public:
|
||||
static bool getDimensionsStatic(const std::string& imagePath, ImageDimensions& out);
|
||||
|
||||
bool decodeToFramebuffer(const std::string& imagePath, GfxRenderer& renderer, const RenderConfig& config) override;
|
||||
|
||||
bool getDimensions(const std::string& imagePath, ImageDimensions& dims) const override {
|
||||
return getDimensionsStatic(imagePath, dims);
|
||||
}
|
||||
|
||||
static bool supportsFormat(const std::string& extension);
|
||||
const char* getFormatName() const override { return "PNG"; }
|
||||
};
|
||||
@@ -1,948 +0,0 @@
|
||||
#include "CssParser.h"
|
||||
|
||||
#include <Arduino.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
// Stack-allocated string buffer to avoid heap reallocations during parsing
|
||||
// Provides string-like interface with fixed capacity
|
||||
struct StackBuffer {
|
||||
static constexpr size_t CAPACITY = 1024;
|
||||
char data[CAPACITY];
|
||||
size_t len = 0;
|
||||
|
||||
void push_back(char c) {
|
||||
if (len < CAPACITY - 1) {
|
||||
data[len++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
void clear() { len = 0; }
|
||||
bool empty() const { return len == 0; }
|
||||
size_t size() const { return len; }
|
||||
|
||||
// Get string view of current content (zero-copy)
|
||||
std::string_view view() const { return std::string_view(data, len); }
|
||||
operator std::string_view() const noexcept { return view(); }
|
||||
};
|
||||
|
||||
// Buffer size for reading CSS files
|
||||
constexpr size_t READ_BUFFER_SIZE = 512;
|
||||
|
||||
// Maximum number of CSS rules to store in the selector map
|
||||
// Prevents unbounded memory growth from pathological CSS files
|
||||
constexpr size_t MAX_RULES = 1500;
|
||||
|
||||
// Minimum free heap required to apply CSS during rendering
|
||||
// If below this threshold, we skip CSS to avoid display artifacts.
|
||||
constexpr size_t MIN_FREE_HEAP_FOR_CSS = 48 * 1024;
|
||||
|
||||
// Maximum length for a single selector string
|
||||
// Prevents parsing of extremely long or malformed selectors
|
||||
constexpr size_t MAX_SELECTOR_LENGTH = 256;
|
||||
|
||||
// Check if character is CSS whitespace
|
||||
constexpr bool isCssWhitespace(const char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'; }
|
||||
|
||||
constexpr std::string_view trimCssWhitespace(std::string_view s) {
|
||||
while (!s.empty() && isCssWhitespace(s.front())) s.remove_prefix(1);
|
||||
while (!s.empty() && isCssWhitespace(s.back())) s.remove_suffix(1);
|
||||
return s;
|
||||
}
|
||||
|
||||
constexpr char asciiToLower(const char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + 32) : c; }
|
||||
|
||||
// Case-insensitive equality on ASCII. lowercaseKeyword MUST already be
|
||||
// lowercase; CSS keywords are ASCII by spec so byte-wise tolower is safe.
|
||||
constexpr bool iequalsAscii(std::string_view value, std::string_view lowercaseKeyword) {
|
||||
return std::equal(value.begin(), value.end(), lowercaseKeyword.begin(), lowercaseKeyword.end(),
|
||||
[](char a, char b) { return asciiToLower(a) == b; });
|
||||
}
|
||||
|
||||
// Walk s and invoke fn(token) for each non-empty run between delimiters.
|
||||
// Tokens are boundary-trimmed and yielded as string_views into s; no
|
||||
// allocation. Runs of consecutive delimiters coalesce — no empty tokens are
|
||||
// emitted. `isDelimiter` is invoked once per character.
|
||||
template <typename Pred, typename F>
|
||||
void forEachDelimitedToken(std::string_view s, Pred isDelimiter, F&& fn) {
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= s.size(); ++i) {
|
||||
if (i == s.size() || isDelimiter(s[i])) {
|
||||
const std::string_view trimmed = trimCssWhitespace(s.substr(start, i - start));
|
||||
if (!trimmed.empty()) {
|
||||
fn(trimmed);
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FNV-1a per Fowler/Noll/Vo, sized to match size_t on the target. The firmware
|
||||
// runs on a 32-bit core where size_t is 32 bits, so naively using the 64-bit
|
||||
// constants would silently truncate FNV_PRIME to a non-prime and wreck hash
|
||||
// distribution. The selection below picks the canonical 32- or 64-bit
|
||||
// constants at compile time so the same source works in a 64-bit host
|
||||
// simulator. `fnv1aMix` is the per-byte mix step; callers apply any
|
||||
// byte-level transform (e.g. asciiToLower) first.
|
||||
static_assert(sizeof(size_t) == 4 || sizeof(size_t) == 8, "FNV constants are only defined for 32- or 64-bit size_t");
|
||||
constexpr size_t FNV_OFFSET_BASIS =
|
||||
sizeof(size_t) == 8 ? static_cast<size_t>(14695981039346656037ULL) : static_cast<size_t>(2166136261U);
|
||||
constexpr size_t FNV_PRIME =
|
||||
sizeof(size_t) == 8 ? static_cast<size_t>(1099511628211ULL) : static_cast<size_t>(16777619U);
|
||||
|
||||
constexpr size_t fnv1aMix(size_t hash, unsigned char byte) { return (hash ^ byte) * FNV_PRIME; }
|
||||
|
||||
// Parse the entirety of s as a number into `out`. Accepts an optional leading
|
||||
// '+' (which std::from_chars rejects by spec) so callers can pass CSS-style
|
||||
// signed numbers without manual trimming. Returns false on empty input, a
|
||||
// non-numeric suffix, or any from_chars error.
|
||||
template <typename T>
|
||||
bool tryParseNumber(std::string_view s, T& out) {
|
||||
const char* begin = s.data();
|
||||
const char* end = s.data() + s.size();
|
||||
if (begin < end && *begin == '+') ++begin;
|
||||
const auto r = std::from_chars(begin, end, out);
|
||||
return r.ec == std::errc{} && r.ptr == end;
|
||||
}
|
||||
|
||||
// Collect up to 4 whitespace-separated tokens for a CSS edge-value shorthand
|
||||
// (margin, padding, and the border-* family). Returns the number of tokens
|
||||
// written; extras are silently dropped. Callers apply the 1/2/3/4-value
|
||||
// fallback rule using the returned count.
|
||||
size_t collectEdgeValueTokens(std::string_view s, std::string_view (&out)[4]) {
|
||||
size_t count = 0;
|
||||
forEachDelimitedToken(s, isCssWhitespace, [&](std::string_view tok) {
|
||||
if (count < 4) out[count++] = tok;
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
std::string_view stripTrailingImportant(std::string_view value) {
|
||||
constexpr std::string_view IMPORTANT = "!important";
|
||||
|
||||
while (!value.empty() && isCssWhitespace(value.back())) {
|
||||
value.remove_suffix(1);
|
||||
}
|
||||
|
||||
if (value.size() < IMPORTANT.size()) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const size_t suffixPos = value.size() - IMPORTANT.size();
|
||||
if (!iequalsAscii(value.substr(suffixPos), IMPORTANT)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
value.remove_suffix(IMPORTANT.size());
|
||||
while (!value.empty() && isCssWhitespace(value.back())) {
|
||||
value.remove_suffix(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// Transparent case-insensitive hash/equal. Bodies live here (rather than
|
||||
// inline in the header) so they can share the anonymous-namespace asciiToLower
|
||||
// with the other ASCII helpers in this translation unit.
|
||||
|
||||
size_t CssParser::SvHash::operator()(std::string_view sv) const noexcept {
|
||||
size_t h = FNV_OFFSET_BASIS;
|
||||
for (char c : sv) h = fnv1aMix(h, asciiToLower(c));
|
||||
return h;
|
||||
}
|
||||
|
||||
size_t CssParser::SvHash::operator()(const std::string& s) const noexcept { return operator()(std::string_view(s)); }
|
||||
|
||||
size_t CssParser::SvHash::operator()(CompositeKey k) const noexcept {
|
||||
// Hash the case-folded concatenation of every piece without materializing
|
||||
// it — the running hash continues across pieces as if they were one buffer.
|
||||
size_t h = FNV_OFFSET_BASIS;
|
||||
for (std::string_view piece : k.pieces) {
|
||||
for (char c : piece) h = fnv1aMix(h, asciiToLower(c));
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
bool CssParser::SvEqual::operator()(std::string_view a, std::string_view b) const noexcept {
|
||||
if (a.size() != b.size()) return false;
|
||||
for (size_t i = 0; i < a.size(); ++i) {
|
||||
if (asciiToLower(a[i]) != asciiToLower(b[i])) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CssParser::SvEqual::operator()(const std::string& a, std::string_view b) const noexcept {
|
||||
return operator()(std::string_view(a), b);
|
||||
}
|
||||
|
||||
bool CssParser::SvEqual::operator()(std::string_view a, const std::string& b) const noexcept {
|
||||
return operator()(a, std::string_view(b));
|
||||
}
|
||||
|
||||
bool CssParser::SvEqual::operator()(const std::string& a, const std::string& b) const noexcept {
|
||||
return operator()(std::string_view(a), std::string_view(b));
|
||||
}
|
||||
|
||||
bool CssParser::SvEqual::operator()(CompositeKey k, std::string_view sv) const noexcept {
|
||||
size_t total = 0;
|
||||
for (std::string_view piece : k.pieces) total += piece.size();
|
||||
if (total != sv.size()) return false;
|
||||
size_t i = 0;
|
||||
for (std::string_view piece : k.pieces) {
|
||||
for (char c : piece) {
|
||||
if (asciiToLower(c) != asciiToLower(sv[i++])) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CssParser::SvEqual::operator()(std::string_view sv, CompositeKey k) const noexcept { return operator()(k, sv); }
|
||||
|
||||
// Property value interpreters
|
||||
|
||||
CssTextAlign CssParser::interpretAlignment(std::string_view val) {
|
||||
val = trimCssWhitespace(val);
|
||||
|
||||
if (iequalsAscii(val, "left") || iequalsAscii(val, "start")) return CssTextAlign::Left;
|
||||
if (iequalsAscii(val, "right") || iequalsAscii(val, "end")) return CssTextAlign::Right;
|
||||
if (iequalsAscii(val, "center")) return CssTextAlign::Center;
|
||||
if (iequalsAscii(val, "justify")) return CssTextAlign::Justify;
|
||||
|
||||
return CssTextAlign::Left;
|
||||
}
|
||||
|
||||
CssFontStyle CssParser::interpretFontStyle(std::string_view val) {
|
||||
val = trimCssWhitespace(val);
|
||||
|
||||
if (iequalsAscii(val, "italic") || iequalsAscii(val, "oblique")) return CssFontStyle::Italic;
|
||||
return CssFontStyle::Normal;
|
||||
}
|
||||
|
||||
CssFontWeight CssParser::interpretFontWeight(std::string_view val) {
|
||||
val = trimCssWhitespace(val);
|
||||
|
||||
// Named values
|
||||
if (iequalsAscii(val, "bold") || iequalsAscii(val, "bolder")) return CssFontWeight::Bold;
|
||||
if (iequalsAscii(val, "normal") || iequalsAscii(val, "lighter")) return CssFontWeight::Normal;
|
||||
|
||||
// Numeric values: 100-900
|
||||
// CSS spec: 400 = normal, 700 = bold
|
||||
// We use: 0-400 = normal, 700+ = bold, 500-600 = normal (conservative)
|
||||
long numericWeight = 0;
|
||||
if (tryParseNumber(val, numericWeight)) {
|
||||
return numericWeight >= 700 ? CssFontWeight::Bold : CssFontWeight::Normal;
|
||||
}
|
||||
return CssFontWeight::Normal;
|
||||
}
|
||||
|
||||
CssTextDecoration CssParser::interpretDecoration(std::string_view val) {
|
||||
// text-decoration can have multiple space-separated values. Compare whole tokens
|
||||
// so malformed values like "notunderline" do not accidentally enable a line.
|
||||
CssTextDecoration result = CssTextDecoration::None;
|
||||
bool explicitNone = false;
|
||||
forEachDelimitedToken(val, isCssWhitespace, [&](const std::string_view token) {
|
||||
if (iequalsAscii(token, "none")) {
|
||||
explicitNone = true;
|
||||
} else if (iequalsAscii(token, "underline")) {
|
||||
result = result | CssTextDecoration::Underline;
|
||||
} else if (iequalsAscii(token, "line-through")) {
|
||||
result = result | CssTextDecoration::LineThrough;
|
||||
}
|
||||
});
|
||||
return explicitNone ? CssTextDecoration::None : result;
|
||||
}
|
||||
|
||||
CssLength CssParser::interpretLength(std::string_view val) {
|
||||
CssLength result;
|
||||
tryInterpretLength(val, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool CssParser::tryInterpretLength(std::string_view val, CssLength& out) {
|
||||
val = trimCssWhitespace(val);
|
||||
if (val.empty()) {
|
||||
out = CssLength{};
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t unitStart = val.size();
|
||||
for (size_t i = 0; i < val.size(); ++i) {
|
||||
const char c = val[i];
|
||||
if (!std::isdigit(c) && c != '.' && c != '-' && c != '+') {
|
||||
unitStart = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float numericValue;
|
||||
if (!tryParseNumber(val.substr(0, unitStart), numericValue)) {
|
||||
out = CssLength{};
|
||||
return false; // No number parsed (e.g. auto, inherit, initial)
|
||||
}
|
||||
|
||||
const std::string_view unitPart = val.substr(unitStart);
|
||||
auto unit = CssUnit::Pixels;
|
||||
if (iequalsAscii(unitPart, "em")) {
|
||||
unit = CssUnit::Em;
|
||||
} else if (iequalsAscii(unitPart, "rem")) {
|
||||
unit = CssUnit::Rem;
|
||||
} else if (iequalsAscii(unitPart, "pt")) {
|
||||
unit = CssUnit::Points;
|
||||
} else if (unitPart == "%") {
|
||||
unit = CssUnit::Percent;
|
||||
}
|
||||
|
||||
out = CssLength{numericValue, unit};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Declaration parsing
|
||||
|
||||
void CssParser::parseDeclarationIntoStyle(std::string_view decl, CssStyle& style) {
|
||||
const size_t colonPos = decl.find(':');
|
||||
if (colonPos == std::string_view::npos || colonPos == 0) return;
|
||||
|
||||
const std::string_view name = trimCssWhitespace(decl.substr(0, colonPos));
|
||||
const std::string_view value = trimCssWhitespace(decl.substr(colonPos + 1));
|
||||
|
||||
if (name.empty() || value.empty()) return;
|
||||
|
||||
if (iequalsAscii(name, "text-align")) {
|
||||
style.textAlign = interpretAlignment(value);
|
||||
style.defined.textAlign = 1;
|
||||
} else if (iequalsAscii(name, "font-style")) {
|
||||
style.fontStyle = interpretFontStyle(value);
|
||||
style.defined.fontStyle = 1;
|
||||
} else if (iequalsAscii(name, "font-weight")) {
|
||||
style.fontWeight = interpretFontWeight(value);
|
||||
style.defined.fontWeight = 1;
|
||||
} else if (iequalsAscii(name, "text-decoration") || iequalsAscii(name, "text-decoration-line")) {
|
||||
style.textDecoration = interpretDecoration(value);
|
||||
style.defined.textDecoration = 1;
|
||||
} else if (iequalsAscii(name, "text-indent")) {
|
||||
style.textIndent = interpretLength(value);
|
||||
style.defined.textIndent = 1;
|
||||
} else if (iequalsAscii(name, "margin-top")) {
|
||||
style.marginTop = interpretLength(value);
|
||||
style.defined.marginTop = 1;
|
||||
} else if (iequalsAscii(name, "margin-bottom")) {
|
||||
style.marginBottom = interpretLength(value);
|
||||
style.defined.marginBottom = 1;
|
||||
} else if (iequalsAscii(name, "margin-left")) {
|
||||
style.marginLeft = interpretLength(value);
|
||||
style.defined.marginLeft = 1;
|
||||
} else if (iequalsAscii(name, "margin-right")) {
|
||||
style.marginRight = interpretLength(value);
|
||||
style.defined.marginRight = 1;
|
||||
} else if (iequalsAscii(name, "margin")) {
|
||||
std::string_view margins[4];
|
||||
const size_t count = collectEdgeValueTokens(value, margins);
|
||||
if (count > 0) {
|
||||
style.marginTop = interpretLength(margins[0]);
|
||||
style.marginRight = count >= 2 ? interpretLength(margins[1]) : style.marginTop;
|
||||
style.marginBottom = count >= 3 ? interpretLength(margins[2]) : style.marginTop;
|
||||
style.marginLeft = count >= 4 ? interpretLength(margins[3]) : style.marginRight;
|
||||
style.defined.marginTop = style.defined.marginRight = style.defined.marginBottom = style.defined.marginLeft = 1;
|
||||
}
|
||||
} else if (iequalsAscii(name, "padding-top")) {
|
||||
style.paddingTop = interpretLength(value);
|
||||
style.defined.paddingTop = 1;
|
||||
} else if (iequalsAscii(name, "padding-bottom")) {
|
||||
style.paddingBottom = interpretLength(value);
|
||||
style.defined.paddingBottom = 1;
|
||||
} else if (iequalsAscii(name, "padding-left")) {
|
||||
style.paddingLeft = interpretLength(value);
|
||||
style.defined.paddingLeft = 1;
|
||||
} else if (iequalsAscii(name, "padding-right")) {
|
||||
style.paddingRight = interpretLength(value);
|
||||
style.defined.paddingRight = 1;
|
||||
} else if (iequalsAscii(name, "padding")) {
|
||||
std::string_view paddings[4];
|
||||
const size_t count = collectEdgeValueTokens(value, paddings);
|
||||
if (count > 0) {
|
||||
style.paddingTop = interpretLength(paddings[0]);
|
||||
style.paddingRight = count >= 2 ? interpretLength(paddings[1]) : style.paddingTop;
|
||||
style.paddingBottom = count >= 3 ? interpretLength(paddings[2]) : style.paddingTop;
|
||||
style.paddingLeft = count >= 4 ? interpretLength(paddings[3]) : style.paddingRight;
|
||||
style.defined.paddingTop = style.defined.paddingRight = style.defined.paddingBottom = style.defined.paddingLeft =
|
||||
1;
|
||||
}
|
||||
} else if (iequalsAscii(name, "height")) {
|
||||
CssLength len;
|
||||
if (tryInterpretLength(value, len)) {
|
||||
style.imageHeight = len;
|
||||
style.defined.imageHeight = 1;
|
||||
}
|
||||
} else if (iequalsAscii(name, "width")) {
|
||||
CssLength len;
|
||||
if (tryInterpretLength(value, len)) {
|
||||
style.imageWidth = len;
|
||||
style.defined.imageWidth = 1;
|
||||
}
|
||||
} else if (iequalsAscii(name, "display")) {
|
||||
const std::string_view displayValue = stripTrailingImportant(value);
|
||||
style.display = iequalsAscii(displayValue, "none") ? CssDisplay::None : CssDisplay::Block;
|
||||
style.defined.display = 1;
|
||||
} else if (iequalsAscii(name, "direction")) {
|
||||
const std::string_view directionValue = stripTrailingImportant(value);
|
||||
if (iequalsAscii(directionValue, "rtl")) {
|
||||
style.direction = CssTextDirection::Rtl;
|
||||
style.defined.direction = 1;
|
||||
} else if (iequalsAscii(directionValue, "ltr")) {
|
||||
style.direction = CssTextDirection::Ltr;
|
||||
style.defined.direction = 1;
|
||||
}
|
||||
} else if (iequalsAscii(name, "vertical-align")) {
|
||||
if (iequalsAscii(value, "super")) {
|
||||
style.verticalAlign = CssVerticalAlign::Super;
|
||||
style.defined.verticalAlign = 1;
|
||||
} else if (iequalsAscii(value, "sub")) {
|
||||
style.verticalAlign = CssVerticalAlign::Sub;
|
||||
style.defined.verticalAlign = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CssStyle CssParser::parseDeclarations(std::string_view declBlock) {
|
||||
CssStyle style;
|
||||
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i <= declBlock.size(); ++i) {
|
||||
if (i == declBlock.size() || declBlock[i] == ';') {
|
||||
if (i > start) {
|
||||
parseDeclarationIntoStyle(declBlock.substr(start, i - start), style);
|
||||
}
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
// Rule processing
|
||||
|
||||
void CssParser::processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style) {
|
||||
// Check if we've reached the rule limit before processing
|
||||
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||
LOG_DBG("CSS", "Reached max rules limit (%zu), stopping CSS parsing", MAX_RULES);
|
||||
return;
|
||||
}
|
||||
|
||||
// Walk comma-separated selectors in place — no vector allocation. Selectors
|
||||
// with unsupported syntax (combinators, attributes, pseudo, etc.) are skipped
|
||||
// silently; the only heap allocation per kept selector is the std::string
|
||||
// map key, which is unavoidable since the map owns its keys.
|
||||
bool limitReached = false;
|
||||
forEachDelimitedToken(
|
||||
selectorGroup, [](char c) { return c == ','; },
|
||||
[&](std::string_view sel) {
|
||||
if (limitReached) return;
|
||||
|
||||
if (sel.size() > MAX_SELECTOR_LENGTH) {
|
||||
LOG_DBG("CSS", "Selector too long (%zu > %zu), skipping", sel.size(), MAX_SELECTOR_LENGTH);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Support richer CSS selector syntax in the future. For now we only
|
||||
// handle `tag`, `.class`, or `tag.class`. Reject anything containing a
|
||||
// character that introduces unsupported syntax:
|
||||
// '+' adjacent sibling combinator
|
||||
// '>' child combinator
|
||||
// '[' attribute selector
|
||||
// ':' pseudo class/element
|
||||
// '#' ID selector
|
||||
// '~' general sibling combinator
|
||||
// '*' wildcard
|
||||
// ' ' descendant combinator
|
||||
// Single-pass scan via find_first_of instead of eight sequential find() calls.
|
||||
constexpr std::string_view kUnsupportedSelectorChars = "+>[:#~* ";
|
||||
if (sel.find_first_of(kUnsupportedSelectorChars) != std::string_view::npos) return;
|
||||
|
||||
// Skip if this would exceed the rule limit
|
||||
if (rulesBySelector_.size() >= MAX_RULES) {
|
||||
LOG_DBG("CSS", "Reached max rules limit, stopping selector processing");
|
||||
limitReached = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Store or merge with existing. Hash/equal are case-insensitive, so two
|
||||
// selectors that differ only in ASCII case collide on insert and merge.
|
||||
auto it = rulesBySelector_.find(sel);
|
||||
if (it != rulesBySelector_.end()) {
|
||||
it->second.applyOver(style);
|
||||
} else {
|
||||
rulesBySelector_.emplace(std::string(sel), style);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Main parsing entry point
|
||||
|
||||
bool CssParser::loadFromStream(HalFile& source) {
|
||||
if (!source) {
|
||||
LOG_ERR("CSS", "Cannot read from invalid file");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t totalRead = 0;
|
||||
|
||||
// Use stack-allocated buffers for parsing to avoid heap reallocations
|
||||
StackBuffer selector;
|
||||
StackBuffer declBuffer;
|
||||
|
||||
bool inComment = false;
|
||||
bool maybeSlash = false;
|
||||
bool prevStar = false;
|
||||
|
||||
bool inAtRule = false;
|
||||
int atDepth = 0;
|
||||
|
||||
int bodyDepth = 0;
|
||||
bool skippingRule = false;
|
||||
CssStyle currentStyle;
|
||||
|
||||
auto handleChar = [&](const char c) {
|
||||
if (inAtRule) {
|
||||
if (c == '{') {
|
||||
++atDepth;
|
||||
} else if (c == '}') {
|
||||
if (atDepth > 0) --atDepth;
|
||||
if (atDepth == 0) inAtRule = false;
|
||||
} else if (c == ';' && atDepth == 0) {
|
||||
inAtRule = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bodyDepth == 0) {
|
||||
if (selector.empty() && isCssWhitespace(c)) {
|
||||
return;
|
||||
}
|
||||
if (c == '@' && selector.empty()) {
|
||||
inAtRule = true;
|
||||
atDepth = 0;
|
||||
return;
|
||||
}
|
||||
if (c == '{') {
|
||||
bodyDepth = 1;
|
||||
currentStyle = CssStyle{};
|
||||
declBuffer.clear();
|
||||
if (selector.size() > MAX_SELECTOR_LENGTH * 4) {
|
||||
skippingRule = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
selector.push_back(c);
|
||||
return;
|
||||
}
|
||||
|
||||
// bodyDepth > 0
|
||||
if (c == '{') {
|
||||
++bodyDepth;
|
||||
return;
|
||||
}
|
||||
if (c == '}') {
|
||||
--bodyDepth;
|
||||
if (bodyDepth == 0) {
|
||||
if (!skippingRule && !declBuffer.empty()) {
|
||||
parseDeclarationIntoStyle(declBuffer, currentStyle);
|
||||
}
|
||||
if (!skippingRule) {
|
||||
processRuleBlockWithStyle(selector, currentStyle);
|
||||
}
|
||||
selector.clear();
|
||||
declBuffer.clear();
|
||||
skippingRule = false;
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (bodyDepth > 1) {
|
||||
return;
|
||||
}
|
||||
if (!skippingRule) {
|
||||
if (c == ';') {
|
||||
if (!declBuffer.empty()) {
|
||||
parseDeclarationIntoStyle(declBuffer, currentStyle);
|
||||
declBuffer.clear();
|
||||
}
|
||||
} else {
|
||||
declBuffer.push_back(c);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
char buffer[READ_BUFFER_SIZE];
|
||||
while (source.available()) {
|
||||
int bytesRead = source.read(buffer, sizeof(buffer));
|
||||
if (bytesRead <= 0) break;
|
||||
|
||||
totalRead += static_cast<size_t>(bytesRead);
|
||||
|
||||
for (int i = 0; i < bytesRead; ++i) {
|
||||
const char c = buffer[i];
|
||||
|
||||
if (inComment) {
|
||||
if (prevStar && c == '/') {
|
||||
inComment = false;
|
||||
prevStar = false;
|
||||
continue;
|
||||
}
|
||||
prevStar = c == '*';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (maybeSlash) {
|
||||
if (c == '*') {
|
||||
inComment = true;
|
||||
maybeSlash = false;
|
||||
prevStar = false;
|
||||
continue;
|
||||
}
|
||||
handleChar('/');
|
||||
maybeSlash = false;
|
||||
// fall through to process current char
|
||||
}
|
||||
|
||||
if (c == '/') {
|
||||
maybeSlash = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
handleChar(c);
|
||||
}
|
||||
}
|
||||
|
||||
if (maybeSlash) {
|
||||
handleChar('/');
|
||||
}
|
||||
|
||||
LOG_DBG("CSS", "Parsed %zu rules from %zu bytes", rulesBySelector_.size(), totalRead);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Style resolution
|
||||
|
||||
CssStyle CssParser::resolveStyle(std::string_view tagName, std::string_view classAttr) const {
|
||||
static bool lowHeapWarningLogged = false;
|
||||
if (ESP.getFreeHeap() < MIN_FREE_HEAP_FOR_CSS) {
|
||||
if (!lowHeapWarningLogged) {
|
||||
lowHeapWarningLogged = true;
|
||||
LOG_DBG("CSS", "Warning: low heap (%u bytes) below MIN_FREE_HEAP_FOR_CSS (%u), returning empty style",
|
||||
ESP.getFreeHeap(), static_cast<unsigned>(MIN_FREE_HEAP_FOR_CSS));
|
||||
}
|
||||
return CssStyle{};
|
||||
}
|
||||
|
||||
CssStyle result;
|
||||
|
||||
// 1. Apply element-level style (lowest priority). The map's hash/equal are
|
||||
// case-insensitive, so the raw tagName view can be used as the lookup key.
|
||||
if (auto it = rulesBySelector_.find(tagName); it != rulesBySelector_.end()) {
|
||||
result.applyOver(it->second);
|
||||
}
|
||||
|
||||
if (classAttr.empty()) return result;
|
||||
|
||||
// TODO: Support combinations of classes (e.g. style on .class1.class2)
|
||||
// 2. Apply class styles (medium priority). The transparent hash/equal accept
|
||||
// a CompositeKey, so we never materialize the concatenation.
|
||||
forEachDelimitedToken(classAttr, isCssWhitespace, [&](std::string_view cls) {
|
||||
if (auto it = rulesBySelector_.find(CompositeKey{".", cls}); it != rulesBySelector_.end()) {
|
||||
result.applyOver(it->second);
|
||||
}
|
||||
});
|
||||
|
||||
// TODO: Support combinations of classes (e.g. style on p.class1.class2)
|
||||
// 3. Apply element.class styles (higher priority).
|
||||
forEachDelimitedToken(classAttr, isCssWhitespace, [&](std::string_view cls) {
|
||||
if (auto it = rulesBySelector_.find(CompositeKey{tagName, ".", cls}); it != rulesBySelector_.end()) {
|
||||
result.applyOver(it->second);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Inline style parsing (static - doesn't need rule database)
|
||||
|
||||
CssStyle CssParser::parseInlineStyle(std::string_view styleValue) { return parseDeclarations(styleValue); }
|
||||
|
||||
// Cache serialization
|
||||
|
||||
// Cache file name (version is CssParser::CSS_CACHE_VERSION)
|
||||
constexpr char rulesCache[] = "/css_rules.cache";
|
||||
|
||||
bool CssParser::hasCache() const { return Storage.exists((cachePath + rulesCache).c_str()); }
|
||||
|
||||
void CssParser::deleteCache() const {
|
||||
if (hasCache()) Storage.remove((cachePath + rulesCache).c_str());
|
||||
}
|
||||
|
||||
bool CssParser::saveToCache() const {
|
||||
if (cachePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HalFile file;
|
||||
if (!Storage.openFileForWrite("CSS", cachePath + rulesCache, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Write version
|
||||
file.write(CssParser::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));
|
||||
file.write(static_cast<uint8_t>(style.direction));
|
||||
|
||||
// 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);
|
||||
writeLength(style.imageHeight);
|
||||
writeLength(style.imageWidth);
|
||||
file.write(static_cast<uint8_t>(style.display));
|
||||
file.write(static_cast<uint8_t>(style.verticalAlign));
|
||||
|
||||
// Write defined flags as uint32_t
|
||||
uint32_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;
|
||||
if (style.defined.imageHeight) definedBits |= 1 << 13;
|
||||
if (style.defined.imageWidth) definedBits |= 1 << 14;
|
||||
if (style.defined.display) definedBits |= 1 << 15;
|
||||
if (style.defined.direction) definedBits |= 1 << 16;
|
||||
if (style.defined.verticalAlign) definedBits |= 1 << 17;
|
||||
file.write(reinterpret_cast<const uint8_t*>(&definedBits), sizeof(definedBits));
|
||||
}
|
||||
|
||||
LOG_DBG("CSS", "Saved %u rules to cache", ruleCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CssParser::loadFromCache() {
|
||||
if (cachePath.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
HalFile file;
|
||||
if (!Storage.openFileForRead("CSS", cachePath + rulesCache, file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Clear existing rules
|
||||
clear();
|
||||
|
||||
// Read and verify version
|
||||
uint8_t version = 0;
|
||||
if (file.read(&version, 1) != 1 || version != CssParser::CSS_CACHE_VERSION) {
|
||||
LOG_DBG("CSS", "Cache version mismatch (got %u, expected %u), removing stale cache for rebuild", version,
|
||||
CssParser::CSS_CACHE_VERSION);
|
||||
// Explicitly close() file before calling Storage.remove()
|
||||
file.close();
|
||||
Storage.remove((cachePath + rulesCache).c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read rule count
|
||||
uint16_t ruleCount = 0;
|
||||
if (file.read(&ruleCount, sizeof(ruleCount)) != sizeof(ruleCount)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ruleCount > MAX_RULES) {
|
||||
LOG_DBG("CSS", "Invalid cache rule count (%u > %zu)", ruleCount, MAX_RULES);
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
auto hasRemainingBytes = [&file](const size_t neededBytes) -> bool {
|
||||
return static_cast<size_t>(file.available()) >= neededBytes;
|
||||
};
|
||||
|
||||
constexpr size_t CSS_LENGTH_FIELD_COUNT = 11;
|
||||
constexpr size_t CSS_LENGTH_BYTES = sizeof(float) + sizeof(uint8_t);
|
||||
constexpr size_t CSS_FIXED_STYLE_BYTES =
|
||||
5 * sizeof(uint8_t) + (CSS_LENGTH_FIELD_COUNT * CSS_LENGTH_BYTES) + sizeof(uint8_t) + sizeof(uint32_t);
|
||||
|
||||
// Read each rule
|
||||
for (uint16_t i = 0; i < ruleCount; ++i) {
|
||||
// Read selector string
|
||||
uint16_t selectorLen = 0;
|
||||
if (!hasRemainingBytes(sizeof(selectorLen))) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
if (file.read(&selectorLen, sizeof(selectorLen)) != sizeof(selectorLen)) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (selectorLen == 0 || selectorLen > MAX_SELECTOR_LENGTH || !hasRemainingBytes(selectorLen)) {
|
||||
LOG_DBG("CSS", "Invalid selector length in cache: %u", selectorLen);
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string selector;
|
||||
selector.resize(selectorLen);
|
||||
if (file.read(&selector[0], selectorLen) != selectorLen) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasRemainingBytes(CSS_FIXED_STYLE_BYTES)) {
|
||||
LOG_DBG("CSS", "Truncated CSS cache while reading style payload");
|
||||
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 & CSS_TEXT_DECORATION_MASK);
|
||||
|
||||
if (file.read(&enumVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.direction = static_cast<CssTextDirection>(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) ||
|
||||
!readLength(style.imageHeight) || !readLength(style.imageWidth)) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read display value
|
||||
uint8_t displayVal;
|
||||
if (file.read(&displayVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.display = static_cast<CssDisplay>(displayVal);
|
||||
|
||||
// Read verticalAlign value
|
||||
uint8_t verticalAlignVal;
|
||||
if (file.read(&verticalAlignVal, 1) != 1) {
|
||||
rulesBySelector_.clear();
|
||||
return false;
|
||||
}
|
||||
style.verticalAlign = static_cast<CssVerticalAlign>(verticalAlignVal);
|
||||
|
||||
// Read defined flags
|
||||
uint32_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;
|
||||
style.defined.imageHeight = (definedBits & 1 << 13) != 0;
|
||||
style.defined.imageWidth = (definedBits & 1 << 14) != 0;
|
||||
style.defined.display = (definedBits & 1 << 15) != 0;
|
||||
style.defined.direction = (definedBits & 1 << 16) != 0;
|
||||
style.defined.verticalAlign = (definedBits & 1 << 17) != 0;
|
||||
|
||||
rulesBySelector_[selector] = style;
|
||||
}
|
||||
|
||||
LOG_DBG("CSS", "Loaded %u rules from cache", ruleCount);
|
||||
return true;
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "CssStyle.h"
|
||||
|
||||
/**
|
||||
* Lightweight CSS parser for EPUB stylesheets
|
||||
*
|
||||
* Parses CSS files and extracts styling information relevant for e-ink display.
|
||||
* Uses a two-phase approach: first tokenizes the CSS content, then builds
|
||||
* a rule database that can be queried during HTML parsing.
|
||||
*
|
||||
* Supported selectors:
|
||||
* - Element selectors: p, div, h1, etc.
|
||||
* - Class selectors: .classname
|
||||
* - Combined: element.classname
|
||||
* - Grouped: selector1, selector2 { }
|
||||
*
|
||||
* Not supported (silently ignored):
|
||||
* - Descendant/child selectors
|
||||
* - Pseudo-classes and pseudo-elements
|
||||
* - Media queries (content is skipped)
|
||||
* - @import, @font-face, etc.
|
||||
*/
|
||||
class CssParser {
|
||||
public:
|
||||
// Bump when CSS cache format or rules change; section caches are invalidated when this changes
|
||||
static constexpr uint8_t CSS_CACHE_VERSION = 7;
|
||||
|
||||
explicit CssParser(std::string cachePath) : cachePath(std::move(cachePath)) {}
|
||||
~CssParser() = default;
|
||||
|
||||
// Non-copyable
|
||||
CssParser(const CssParser&) = delete;
|
||||
CssParser& operator=(const CssParser&) = delete;
|
||||
|
||||
/**
|
||||
* Load and parse CSS from a file stream.
|
||||
* Can be called multiple times to accumulate rules from multiple stylesheets.
|
||||
* @param source Open file handle to read from
|
||||
* @return true if parsing completed (even if no rules found)
|
||||
*/
|
||||
bool loadFromStream(HalFile& source);
|
||||
|
||||
/**
|
||||
* Look up the style for an HTML element, considering tag name and class attributes.
|
||||
* Applies CSS cascade: element style < class style < element.class style
|
||||
*
|
||||
* @param tagName The HTML element name (e.g., "p", "div")
|
||||
* @param classAttr The class attribute value (may contain multiple space-separated classes)
|
||||
* @return Combined style with all applicable rules merged
|
||||
*/
|
||||
[[nodiscard]] CssStyle resolveStyle(std::string_view tagName, std::string_view classAttr) const;
|
||||
|
||||
/**
|
||||
* Parse an inline style attribute string.
|
||||
* @param styleValue The value of a style="" attribute
|
||||
* @return Parsed style properties
|
||||
*/
|
||||
[[nodiscard]] static CssStyle parseInlineStyle(std::string_view styleValue);
|
||||
|
||||
/**
|
||||
* Check if any rules have been loaded
|
||||
*/
|
||||
[[nodiscard]] bool empty() const { return rulesBySelector_.empty(); }
|
||||
|
||||
/**
|
||||
* Get count of loaded rule sets
|
||||
*/
|
||||
[[nodiscard]] size_t ruleCount() const { return rulesBySelector_.size(); }
|
||||
|
||||
/**
|
||||
* Clear all loaded rules
|
||||
*/
|
||||
void clear() { rulesBySelector_.clear(); }
|
||||
|
||||
/**
|
||||
* Check if CSS rules cache file exists
|
||||
*/
|
||||
bool hasCache() const;
|
||||
|
||||
/**
|
||||
* Delete CSS rules cache file exists
|
||||
*/
|
||||
void deleteCache() const;
|
||||
|
||||
/**
|
||||
* Save parsed CSS rules to a cache file.
|
||||
* @return true if cache was written successfully
|
||||
*/
|
||||
bool saveToCache() const;
|
||||
|
||||
/**
|
||||
* Load CSS rules from a cache file.
|
||||
* Clears any existing rules before loading.
|
||||
* @return true if cache was loaded successfully
|
||||
*/
|
||||
bool loadFromCache();
|
||||
|
||||
private:
|
||||
// Lookup key for a multi-piece selector. The pieces are hashed and compared
|
||||
// as if concatenated, so callers can look up composite keys without
|
||||
// materializing the concatenation in a scratch buffer. Constructed from a
|
||||
// braced list of any arity, e.g. `CompositeKey{tagName, ".", cls}` or
|
||||
// `CompositeKey{".", cls}`. The initializer_list's backing array lives for
|
||||
// the full expression, which covers the lifetime of the find() call.
|
||||
struct CompositeKey {
|
||||
std::initializer_list<std::string_view> pieces;
|
||||
CompositeKey(std::initializer_list<std::string_view> p) noexcept : pieces(p) {}
|
||||
};
|
||||
|
||||
// ASCII-case-insensitive transparent hash/equal. Stored selectors and lookup
|
||||
// keys are compared without regard to case, so callers may insert and look up
|
||||
// using whatever case the CSS source or HTML element name happens to use.
|
||||
// Bodies live in CssParser.cpp so they can share the file-local asciiToLower.
|
||||
struct SvHash {
|
||||
using is_transparent = void;
|
||||
size_t operator()(std::string_view sv) const noexcept;
|
||||
size_t operator()(const std::string& s) const noexcept;
|
||||
size_t operator()(CompositeKey k) const noexcept;
|
||||
};
|
||||
struct SvEqual {
|
||||
using is_transparent = void;
|
||||
bool operator()(std::string_view a, std::string_view b) const noexcept;
|
||||
bool operator()(const std::string& a, std::string_view b) const noexcept;
|
||||
bool operator()(std::string_view a, const std::string& b) const noexcept;
|
||||
bool operator()(const std::string& a, const std::string& b) const noexcept;
|
||||
bool operator()(CompositeKey a, std::string_view b) const noexcept;
|
||||
bool operator()(std::string_view a, CompositeKey b) const noexcept;
|
||||
};
|
||||
|
||||
// Storage: maps selector -> style properties. Hash/equal are case-insensitive.
|
||||
std::unordered_map<std::string, CssStyle, SvHash, SvEqual> rulesBySelector_;
|
||||
|
||||
std::string cachePath;
|
||||
|
||||
// Internal parsing helpers
|
||||
void processRuleBlockWithStyle(std::string_view selectorGroup, const CssStyle& style);
|
||||
static CssStyle parseDeclarations(std::string_view declBlock);
|
||||
static void parseDeclarationIntoStyle(std::string_view decl, CssStyle& style);
|
||||
|
||||
// Individual property value parsers
|
||||
static CssTextAlign interpretAlignment(std::string_view val);
|
||||
static CssFontStyle interpretFontStyle(std::string_view val);
|
||||
static CssFontWeight interpretFontWeight(std::string_view val);
|
||||
static CssTextDecoration interpretDecoration(std::string_view val);
|
||||
static CssLength interpretLength(std::string_view val);
|
||||
/** Returns true only when a numeric length was parsed (e.g. 2em, 50%). False for auto/inherit/initial. */
|
||||
static bool tryInterpretLength(std::string_view val, CssLength& out);
|
||||
};
|
||||
@@ -1,270 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Matches order of PARAGRAPH_ALIGNMENT in CrossPointSettings
|
||||
enum class CssTextAlign : uint8_t { Justify = 0, Left = 1, Center = 2, Right = 3, None = 4 };
|
||||
enum class CssUnit : uint8_t { Pixels = 0, Em = 1, Rem = 2, Points = 3, Percent = 4 };
|
||||
enum class CssTextDirection : uint8_t { Ltr = 0, Rtl = 1 };
|
||||
|
||||
// Represents a CSS length value with its unit, allowing deferred resolution to pixels
|
||||
struct CssLength {
|
||||
float value = 0.0f;
|
||||
CssUnit unit = CssUnit::Pixels;
|
||||
|
||||
CssLength() = default;
|
||||
CssLength(const float v, const CssUnit u) : value(v), unit(u) {}
|
||||
|
||||
// Convenience constructor for pixel values (most common case)
|
||||
explicit CssLength(const float pixels) : value(pixels) {}
|
||||
|
||||
// Returns true if this length can be resolved to pixels with the given context.
|
||||
// Percentage units require a non-zero containerWidth to resolve.
|
||||
[[nodiscard]] bool isResolvable(const float containerWidth = 0) const {
|
||||
return unit != CssUnit::Percent || containerWidth > 0;
|
||||
}
|
||||
|
||||
// Resolve to pixels given the current em size (font line height)
|
||||
// containerWidth is needed for percentage units (e.g. viewport width)
|
||||
[[nodiscard]] float toPixels(const float emSize, const float containerWidth = 0) const {
|
||||
switch (unit) {
|
||||
case CssUnit::Em:
|
||||
case CssUnit::Rem:
|
||||
return value * emSize;
|
||||
case CssUnit::Points:
|
||||
return value * 1.33f; // Approximate pt to px conversion
|
||||
case CssUnit::Percent:
|
||||
return value * containerWidth / 100.0f;
|
||||
default:
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve to int16_t pixels (for BlockStyle fields)
|
||||
[[nodiscard]] int16_t toPixelsInt16(const float emSize, const float containerWidth = 0) const {
|
||||
return static_cast<int16_t>(toPixels(emSize, containerWidth));
|
||||
}
|
||||
};
|
||||
|
||||
// Font style options matching CSS font-style property
|
||||
enum class CssFontStyle : uint8_t { Normal = 0, Italic = 1 };
|
||||
|
||||
// Font weight options - CSS supports 100-900, we simplify to normal/bold
|
||||
enum class CssFontWeight : uint8_t { Normal = 0, Bold = 1 };
|
||||
|
||||
// Text decoration options. Values are bit flags so CSS can combine multiple line decorations.
|
||||
enum class CssTextDecoration : uint8_t { None = 0, Underline = 1, LineThrough = 2 };
|
||||
|
||||
constexpr CssTextDecoration operator|(const CssTextDecoration a, const CssTextDecoration b) {
|
||||
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) | static_cast<uint8_t>(b));
|
||||
}
|
||||
|
||||
constexpr CssTextDecoration operator&(const CssTextDecoration a, const CssTextDecoration b) {
|
||||
return static_cast<CssTextDecoration>(static_cast<uint8_t>(a) & static_cast<uint8_t>(b));
|
||||
}
|
||||
|
||||
constexpr uint8_t CSS_TEXT_DECORATION_MASK =
|
||||
static_cast<uint8_t>(CssTextDecoration::Underline) | static_cast<uint8_t>(CssTextDecoration::LineThrough);
|
||||
|
||||
// Display options - only None and Block are relevant for e-ink rendering
|
||||
enum class CssDisplay : uint8_t { Block = 0, None = 1 };
|
||||
|
||||
// Vertical alignment options for inline elements (e.g. superscript/subscript)
|
||||
enum class CssVerticalAlign : uint8_t { Baseline = 0, Super = 1, Sub = 2 };
|
||||
|
||||
// Bitmask for tracking which properties have been explicitly set
|
||||
struct CssPropertyFlags {
|
||||
uint16_t textAlign : 1;
|
||||
uint16_t fontStyle : 1;
|
||||
uint16_t fontWeight : 1;
|
||||
uint16_t textDecoration : 1;
|
||||
uint16_t textIndent : 1;
|
||||
uint16_t marginTop : 1;
|
||||
uint16_t marginBottom : 1;
|
||||
uint16_t marginLeft : 1;
|
||||
uint16_t marginRight : 1;
|
||||
uint16_t paddingTop : 1;
|
||||
uint16_t paddingBottom : 1;
|
||||
uint16_t paddingLeft : 1;
|
||||
uint16_t paddingRight : 1;
|
||||
uint16_t imageHeight : 1;
|
||||
uint16_t imageWidth : 1;
|
||||
uint16_t display : 1;
|
||||
uint16_t direction : 1;
|
||||
uint16_t verticalAlign : 1;
|
||||
|
||||
CssPropertyFlags()
|
||||
: textAlign(0),
|
||||
fontStyle(0),
|
||||
fontWeight(0),
|
||||
textDecoration(0),
|
||||
textIndent(0),
|
||||
marginTop(0),
|
||||
marginBottom(0),
|
||||
marginLeft(0),
|
||||
marginRight(0),
|
||||
paddingTop(0),
|
||||
paddingBottom(0),
|
||||
paddingLeft(0),
|
||||
paddingRight(0),
|
||||
imageHeight(0),
|
||||
imageWidth(0),
|
||||
display(0),
|
||||
direction(0),
|
||||
verticalAlign(0) {}
|
||||
|
||||
[[nodiscard]] bool anySet() const {
|
||||
return textAlign || fontStyle || fontWeight || textDecoration || textIndent || marginTop || marginBottom ||
|
||||
marginLeft || marginRight || paddingTop || paddingBottom || paddingLeft || paddingRight || imageHeight ||
|
||||
imageWidth || display || direction || verticalAlign;
|
||||
}
|
||||
|
||||
void clearAll() {
|
||||
textAlign = fontStyle = fontWeight = textDecoration = textIndent = 0;
|
||||
marginTop = marginBottom = marginLeft = marginRight = 0;
|
||||
paddingTop = paddingBottom = paddingLeft = paddingRight = 0;
|
||||
imageHeight = imageWidth = display = direction = verticalAlign = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// Cache serializes defined flags as uint32_t with bit indices 0..17.
|
||||
static_assert(sizeof(CssPropertyFlags) <= sizeof(uint32_t),
|
||||
"CssPropertyFlags exceeds 32 bits; update cache read/write in CssParser.cpp");
|
||||
|
||||
// Represents a collection of CSS style properties
|
||||
// Only stores properties relevant to e-ink text rendering
|
||||
// Length values are stored as CssLength (value + unit) for deferred resolution
|
||||
struct CssStyle {
|
||||
CssTextAlign textAlign = CssTextAlign::Left;
|
||||
CssFontStyle fontStyle = CssFontStyle::Normal;
|
||||
CssFontWeight fontWeight = CssFontWeight::Normal;
|
||||
CssTextDecoration textDecoration = CssTextDecoration::None;
|
||||
CssTextDirection direction = CssTextDirection::Ltr;
|
||||
|
||||
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
|
||||
CssLength marginRight; // Horizontal spacing right of block
|
||||
CssLength paddingTop; // Padding before
|
||||
CssLength paddingBottom; // Padding after
|
||||
CssLength paddingLeft; // Padding left
|
||||
CssLength paddingRight; // Padding right
|
||||
CssLength imageHeight; // Height for img (e.g. 2em) – width derived from aspect ratio when only height set
|
||||
CssLength imageWidth; // Width for img when both or only width set
|
||||
CssDisplay display = CssDisplay::Block; // display property (Block or None)
|
||||
CssVerticalAlign verticalAlign = CssVerticalAlign::Baseline; // vertical-align (super/sub positioning)
|
||||
|
||||
CssPropertyFlags defined; // Tracks which properties were explicitly set
|
||||
|
||||
// Apply properties from another style, only overwriting if the other style
|
||||
// has that property explicitly defined
|
||||
void applyOver(const CssStyle& base) {
|
||||
if (base.hasTextAlign()) {
|
||||
textAlign = base.textAlign;
|
||||
defined.textAlign = 1;
|
||||
}
|
||||
if (base.hasFontStyle()) {
|
||||
fontStyle = base.fontStyle;
|
||||
defined.fontStyle = 1;
|
||||
}
|
||||
if (base.hasFontWeight()) {
|
||||
fontWeight = base.fontWeight;
|
||||
defined.fontWeight = 1;
|
||||
}
|
||||
if (base.hasTextDecoration()) {
|
||||
textDecoration = base.textDecoration;
|
||||
defined.textDecoration = 1;
|
||||
}
|
||||
if (base.hasTextIndent()) {
|
||||
textIndent = base.textIndent;
|
||||
defined.textIndent = 1;
|
||||
}
|
||||
if (base.hasMarginTop()) {
|
||||
marginTop = base.marginTop;
|
||||
defined.marginTop = 1;
|
||||
}
|
||||
if (base.hasMarginBottom()) {
|
||||
marginBottom = base.marginBottom;
|
||||
defined.marginBottom = 1;
|
||||
}
|
||||
if (base.hasMarginLeft()) {
|
||||
marginLeft = base.marginLeft;
|
||||
defined.marginLeft = 1;
|
||||
}
|
||||
if (base.hasMarginRight()) {
|
||||
marginRight = base.marginRight;
|
||||
defined.marginRight = 1;
|
||||
}
|
||||
if (base.hasPaddingTop()) {
|
||||
paddingTop = base.paddingTop;
|
||||
defined.paddingTop = 1;
|
||||
}
|
||||
if (base.hasPaddingBottom()) {
|
||||
paddingBottom = base.paddingBottom;
|
||||
defined.paddingBottom = 1;
|
||||
}
|
||||
if (base.hasPaddingLeft()) {
|
||||
paddingLeft = base.paddingLeft;
|
||||
defined.paddingLeft = 1;
|
||||
}
|
||||
if (base.hasPaddingRight()) {
|
||||
paddingRight = base.paddingRight;
|
||||
defined.paddingRight = 1;
|
||||
}
|
||||
if (base.hasImageHeight()) {
|
||||
imageHeight = base.imageHeight;
|
||||
defined.imageHeight = 1;
|
||||
}
|
||||
if (base.hasImageWidth()) {
|
||||
imageWidth = base.imageWidth;
|
||||
defined.imageWidth = 1;
|
||||
}
|
||||
if (base.hasDisplay()) {
|
||||
display = base.display;
|
||||
defined.display = 1;
|
||||
}
|
||||
if (base.hasDirection()) {
|
||||
direction = base.direction;
|
||||
defined.direction = 1;
|
||||
}
|
||||
if (base.hasVerticalAlign()) {
|
||||
verticalAlign = base.verticalAlign;
|
||||
defined.verticalAlign = 1;
|
||||
}
|
||||
}
|
||||
|
||||
[[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.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; }
|
||||
[[nodiscard]] bool hasMarginRight() const { return defined.marginRight; }
|
||||
[[nodiscard]] bool hasPaddingTop() const { return defined.paddingTop; }
|
||||
[[nodiscard]] bool hasPaddingBottom() const { return defined.paddingBottom; }
|
||||
[[nodiscard]] bool hasPaddingLeft() const { return defined.paddingLeft; }
|
||||
[[nodiscard]] bool hasPaddingRight() const { return defined.paddingRight; }
|
||||
[[nodiscard]] bool hasImageHeight() const { return defined.imageHeight; }
|
||||
[[nodiscard]] bool hasImageWidth() const { return defined.imageWidth; }
|
||||
[[nodiscard]] bool hasDisplay() const { return defined.display; }
|
||||
[[nodiscard]] bool hasDirection() const { return defined.direction; }
|
||||
[[nodiscard]] bool hasVerticalAlign() const { return defined.verticalAlign; }
|
||||
|
||||
void reset() {
|
||||
textAlign = CssTextAlign::Left;
|
||||
fontStyle = CssFontStyle::Normal;
|
||||
fontWeight = CssFontWeight::Normal;
|
||||
textDecoration = CssTextDecoration::None;
|
||||
direction = CssTextDirection::Ltr;
|
||||
textIndent = CssLength{};
|
||||
marginTop = marginBottom = marginLeft = marginRight = CssLength{};
|
||||
paddingTop = paddingBottom = paddingLeft = paddingRight = CssLength{};
|
||||
imageHeight = imageWidth = CssLength{};
|
||||
display = CssDisplay::Block;
|
||||
verticalAlign = CssVerticalAlign::Baseline;
|
||||
defined.clearAll();
|
||||
}
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
// based on
|
||||
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||
|
||||
#include "htmlEntities.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <iterator>
|
||||
|
||||
struct EntityPair {
|
||||
const char* key;
|
||||
const char* value;
|
||||
};
|
||||
|
||||
// Sorted lexicographically by key to allow binary search.
|
||||
static constexpr EntityPair ENTITY_LOOKUP[] = {
|
||||
{"Æ", "Æ"}, {"Á", "Á"}, {"Â", "Â"}, {"À", "À"}, {"Α", "Α"},
|
||||
{"Å", "Å"}, {"Ã", "Ã"}, {"Ä", "Ä"}, {"Β", "Β"}, {"Ç", "Ç"},
|
||||
{"Χ", "Χ"}, {"‡", "‡"}, {"Δ", "Δ"}, {"Ð", "Ð"}, {"É", "É"},
|
||||
{"Ê", "Ê"}, {"È", "È"}, {"Ε", "Ε"}, {"Η", "Η"}, {"Ë", "Ë"},
|
||||
{"Γ", "Γ"}, {"Í", "Í"}, {"Î", "Î"}, {"Ì", "Ì"}, {"Ι", "Ι"},
|
||||
{"Ï", "Ï"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"}, {"Ñ", "Ñ"},
|
||||
{"Ν", "Ν"}, {"Œ", "Œ"}, {"Ó", "Ó"}, {"Ô", "Ô"}, {"Ò", "Ò"},
|
||||
{"Ω", "Ω"}, {"Ο", "Ο"}, {"Ø", "Ø"}, {"Õ", "Õ"}, {"Ö", "Ö"},
|
||||
{"Φ", "Φ"}, {"Π", "Π"}, {"″", "″"}, {"Ψ", "Ψ"}, {"Ρ", "Ρ"},
|
||||
{"Š", "Š"}, {"Σ", "Σ"}, {"Þ", "Þ"}, {"Τ", "Τ"}, {"Θ", "Θ"},
|
||||
{"Ú", "Ú"}, {"Û", "Û"}, {"Ù", "Ù"}, {"Υ", "Υ"}, {"Ü", "Ü"},
|
||||
{"Ξ", "Ξ"}, {"Ý", "Ý"}, {"Ÿ", "Ÿ"}, {"Ζ", "Ζ"}, {"á", "á"},
|
||||
{"â", "â"}, {"´", "´"}, {"æ", "æ"}, {"à", "à"}, {"ℵ", "ℵ"},
|
||||
{"α", "α"}, {"&", "&"}, {"∧", "∧"}, {"∠", "∠"}, {"å", "å"},
|
||||
{"≈", "≈"}, {"ã", "ã"}, {"ä", "ä"}, {"„", "„"}, {"β", "β"},
|
||||
{"¦", "¦"}, {"•", "•"}, {"∩", "∩"}, {"ç", "ç"}, {"¸", "¸"},
|
||||
{"¢", "¢"}, {"χ", "χ"}, {"ˆ", "ˆ"}, {"♣", "♣"}, {"≅", "≅"},
|
||||
{"©", "©"}, {"↵", "↵"}, {"∪", "∪"}, {"¤", "¤"}, {"⇓", "⇓"},
|
||||
{"†", "†"}, {"↓", "↓"}, {"°", "°"}, {"δ", "δ"}, {"♦", "♦"},
|
||||
{"÷", "÷"}, {"é", "é"}, {"ê", "ê"}, {"è", "è"}, {"∅", "∅"},
|
||||
{" ", " "}, {" ", " "}, {"ε", "ε"}, {"≡", "≡"}, {"η", "η"},
|
||||
{"ð", "ð"}, {"ë", "ë"}, {"€", "€"}, {"∃", "∃"}, {"ƒ", "ƒ"},
|
||||
{"∀", "∀"}, {"½", "½"}, {"¼", "¼"}, {"¾", "¾"}, {"⁄", "⁄"},
|
||||
{"γ", "γ"}, {"≥", "≥"}, {">", ">"}, {"⇔", "⇔"}, {"↔", "↔"},
|
||||
{"♥", "♥"}, {"…", "…"}, {"í", "í"}, {"î", "î"}, {"¡", "¡"},
|
||||
{"ì", "ì"}, {"ℑ", "ℑ"}, {"∞", "∞"}, {"∫", "∫"}, {"ι", "ι"},
|
||||
{"¿", "¿"}, {"∈", "∈"}, {"ï", "ï"}, {"κ", "κ"}, {"⇐", "⇐"},
|
||||
{"λ", "λ"}, {"⟨", "〈"}, {"«", "«"}, {"←", "←"}, {"⌈", "⌈"},
|
||||
{"“", "\u201C"}, {"≤", "≤"}, {"⌊", "⌊"}, {"∗", "∗"}, {"◊", "◊"},
|
||||
{"‎", "\u200E"}, {"‹", "‹"}, {"‘", "\u2018"}, {"<", "<"}, {"¯", "¯"},
|
||||
{"—", "—"}, {"µ", "µ"}, {"·", "·"}, {"−", "−"}, {"μ", "μ"},
|
||||
{"∇", "∇"}, {" ", "\xC2\xA0"}, {"–", "–"}, {"≠", "≠"}, {"∋", "∋"},
|
||||
{"¬", "¬"}, {"∉", "∉"}, {"⊄", "⊄"}, {"ñ", "ñ"}, {"ν", "ν"},
|
||||
{"ó", "ó"}, {"ô", "ô"}, {"œ", "œ"}, {"ò", "ò"}, {"‾", "‾"},
|
||||
{"ω", "ω"}, {"ο", "ο"}, {"⊕", "⊕"}, {"∨", "∨"}, {"ª", "ª"},
|
||||
{"º", "º"}, {"ø", "ø"}, {"õ", "õ"}, {"⊗", "⊗"}, {"ö", "ö"},
|
||||
{"¶", "¶"}, {"∂", "∂"}, {"‰", "‰"}, {"⊥", "⊥"}, {"φ", "φ"},
|
||||
{"π", "π"}, {"ϖ", "ϖ"}, {"±", "±"}, {"£", "£"}, {"′", "′"},
|
||||
{"∏", "∏"}, {"∝", "∝"}, {"ψ", "ψ"}, {""", "\""}, {"⇒", "⇒"},
|
||||
{"√", "√"}, {"⟩", "〉"}, {"»", "»"}, {"→", "→"}, {"⌉", "⌉"},
|
||||
{"”", "\u201D"}, {"ℜ", "\u211C"}, {"®", "®"}, {"⌋", "⌋"}, {"ρ", "ρ"},
|
||||
{"‏", "\u200F"}, {"›", "›"}, {"’", "\u2019"}, {"‚", "‚"}, {"š", "š"},
|
||||
{"⋅", "⋅"}, {"§", "§"}, {"­", "\xC2\xAD"}, {"σ", "σ"}, {"ς", "ς"},
|
||||
{"∼", "∼"}, {"♠", "♠"}, {"⊂", "⊂"}, {"⊆", "⊆"}, {"∑", "∑"},
|
||||
{"¹", "¹"}, {"²", "²"}, {"³", "³"}, {"⊃", "⊃"}, {"⊇", "⊇"},
|
||||
{"ß", "ß"}, {"τ", "τ"}, {"∴", "∴"}, {"θ", "θ"}, {"ϑ", "ϑ"},
|
||||
{" ", " "}, {"þ", "þ"}, {"˜", "˜"}, {"×", "×"}, {"™", "™"},
|
||||
{"⇑", "⇑"}, {"ú", "ú"}, {"↑", "↑"}, {"û", "û"}, {"ù", "ù"},
|
||||
{"¨", "¨"}, {"ϒ", "ϒ"}, {"υ", "υ"}, {"ü", "ü"}, {"℘", "℘"},
|
||||
{"ξ", "ξ"}, {"ý", "ý"}, {"¥", "¥"}, {"ÿ", "ÿ"}, {"ζ", "ζ"},
|
||||
{"‍", "\u200D"}, {"‌", "\u200C"},
|
||||
};
|
||||
|
||||
// Verify the table is sorted at compile time.
|
||||
static constexpr int constexprStrcmp(const char* a, const char* b) {
|
||||
for (size_t i = 0;; i++) {
|
||||
if (a[i] != b[i]) return (unsigned char)a[i] < (unsigned char)b[i] ? -1 : 1;
|
||||
if (a[i] == '\0') return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static constexpr bool isTableSorted() {
|
||||
for (size_t i = 1; i < std::size(ENTITY_LOOKUP); i++) {
|
||||
if (constexprStrcmp(ENTITY_LOOKUP[i - 1].key, ENTITY_LOOKUP[i].key) >= 0) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
static_assert(isTableSorted(), "ENTITY_LOOKUP must be sorted lexicographically by key");
|
||||
|
||||
// Lookup a single HTML entity and return its UTF-8 value.
|
||||
const char* lookupHtmlEntity(const char* entity, size_t len) {
|
||||
if (entity == nullptr || len == 0) return nullptr;
|
||||
|
||||
size_t lo = 0;
|
||||
size_t hi = std::size(ENTITY_LOOKUP);
|
||||
|
||||
while (lo < hi) {
|
||||
const size_t mid = lo + (hi - lo) / 2;
|
||||
const char* key = ENTITY_LOOKUP[mid].key;
|
||||
const size_t keyLen = strlen(key);
|
||||
const size_t cmpLen = (len < keyLen) ? len : keyLen;
|
||||
int cmp = memcmp(entity, key, cmpLen);
|
||||
if (cmp == 0) {
|
||||
// safety net: if prefix equal, shorter string is considered smaller
|
||||
if (len < keyLen)
|
||||
cmp = -1;
|
||||
else if (len > keyLen)
|
||||
cmp = 1;
|
||||
else
|
||||
cmp = 0;
|
||||
}
|
||||
|
||||
if (cmp == 0) return ENTITY_LOOKUP[mid].value;
|
||||
if (cmp < 0)
|
||||
hi = mid;
|
||||
else
|
||||
lo = mid + 1;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// based on
|
||||
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||
|
||||
#pragma once
|
||||
#include <string>
|
||||
|
||||
// Lookup a single HTML entity (including & and ;) and return its UTF-8 value
|
||||
// Returns nullptr if entity is not found
|
||||
const char* lookupHtmlEntity(const char* entity, size_t len);
|
||||
@@ -1,471 +0,0 @@
|
||||
#include "HyphenationCommon.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
namespace {
|
||||
|
||||
// Convert Latin uppercase letters (ASCII plus Latin-1 supplement) to lowercase
|
||||
uint32_t toLowerLatinImpl(const uint32_t cp) {
|
||||
if (cp >= 'A' && cp <= 'Z') {
|
||||
return cp - 'A' + 'a';
|
||||
}
|
||||
if ((cp >= 0x00C0 && cp <= 0x00D6) || (cp >= 0x00D8 && cp <= 0x00DE)) {
|
||||
return cp + 0x20;
|
||||
}
|
||||
|
||||
// Latin Extended-A (U+0100..U+017E): uppercase letters are paired with
|
||||
// lowercase at cp+1. Two sub-ranges have different alignment:
|
||||
// U+0100..U+0137: uppercase on EVEN codepoints
|
||||
// U+0139..U+0148: uppercase on ODD codepoints
|
||||
// U+014A..U+0177: uppercase on EVEN codepoints
|
||||
// U+0179..U+017E: uppercase on ODD codepoints
|
||||
// Covers Polish (Ą/ą, Ć/ć, Ę/ę, Ł/ł, Ń/ń, Ś/ś, Ź/ź, Ż/ż), Czech, Hungarian, Turkish, etc.
|
||||
if ((cp >= 0x0100 && cp <= 0x0137 && (cp % 2 == 0)) || (cp >= 0x0139 && cp <= 0x0148 && (cp % 2 == 1)) ||
|
||||
(cp >= 0x014A && cp <= 0x0177 && (cp % 2 == 0)) || (cp >= 0x0179 && cp <= 0x017E && (cp % 2 == 1))) {
|
||||
return cp + 1;
|
||||
}
|
||||
|
||||
switch (cp) {
|
||||
case 0x0178: // Ÿ
|
||||
return 0x00FF; // ÿ
|
||||
case 0x1E9E: // ẞ
|
||||
return 0x00DF; // ß
|
||||
default:
|
||||
return cp;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Cyrillic uppercase letters to lowercase
|
||||
// Cyrillic uppercase range 0x0410-0x042F maps to lowercase by adding 0x20
|
||||
// Special case: Cyrillic capital IO (0x0401) maps to lowercase io (0x0451)
|
||||
uint32_t toLowerCyrillicImpl(const uint32_t cp) {
|
||||
if (cp >= 0x0410 && cp <= 0x042F) {
|
||||
return cp + 0x20;
|
||||
}
|
||||
if (cp == 0x0401) {
|
||||
return 0x0451;
|
||||
}
|
||||
return cp;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t toLowerLatin(const uint32_t cp) { return toLowerLatinImpl(cp); }
|
||||
|
||||
uint32_t toLowerCyrillic(const uint32_t cp) { return toLowerCyrillicImpl(cp); }
|
||||
|
||||
bool isLatinLetter(const uint32_t cp) {
|
||||
if ((cp >= 'A' && cp <= 'Z') || (cp >= 'a' && cp <= 'z')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (((cp >= 0x00C0 && cp <= 0x00D6) || (cp >= 0x00D8 && cp <= 0x00F6) || (cp >= 0x00F8 && cp <= 0x00FF)) &&
|
||||
cp != 0x00D7 && cp != 0x00F7) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Latin Extended-A (U+0100..U+017F): Polish, Czech, Hungarian, Turkish, etc.
|
||||
if (cp >= 0x0100 && cp <= 0x017F) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (cp) {
|
||||
case 0x0152: // Œ
|
||||
case 0x0153: // œ
|
||||
case 0x0178: // Ÿ
|
||||
case 0x1E9E: // ẞ
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isCyrillicLetter(const uint32_t cp) { return (cp >= 0x0400 && cp <= 0x052F); }
|
||||
|
||||
bool isAlphabetic(const uint32_t cp) { return isLatinLetter(cp) || isCyrillicLetter(cp); }
|
||||
|
||||
bool isPunctuation(const uint32_t cp) {
|
||||
switch (cp) {
|
||||
case '-':
|
||||
case '.':
|
||||
case ',':
|
||||
case '!':
|
||||
case '?':
|
||||
case ';':
|
||||
case ':':
|
||||
case '"':
|
||||
case '\'':
|
||||
case ')':
|
||||
case '(':
|
||||
case 0x00AB: // «
|
||||
case 0x00BB: // »
|
||||
case 0x2018: // ‘
|
||||
case 0x2019: // ’
|
||||
case 0x201A: // ‚
|
||||
case 0x201C: // “
|
||||
case 0x201D: // ”
|
||||
case 0x201E: // „
|
||||
case 0x00A0: // no-break space
|
||||
case '{':
|
||||
case '}':
|
||||
case '[':
|
||||
case ']':
|
||||
case '/':
|
||||
case 0x2039: // ‹
|
||||
case 0x203A: // ›
|
||||
case 0x2026: // …
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isAsciiDigit(const uint32_t cp) { return cp >= '0' && cp <= '9'; }
|
||||
|
||||
bool isApostrophe(const uint32_t cp) {
|
||||
switch (cp) {
|
||||
case '\'':
|
||||
case 0x2018: // left single quotation mark
|
||||
case 0x2019: // right single quotation mark
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isExplicitHyphen(const uint32_t cp) {
|
||||
switch (cp) {
|
||||
case '-':
|
||||
case 0x00AD: // soft hyphen
|
||||
case 0x058A: // Armenian hyphen
|
||||
case 0x2010: // hyphen
|
||||
case 0x2011: // non-breaking hyphen
|
||||
case 0x2012: // figure dash
|
||||
case 0x2013: // en dash
|
||||
case 0x2014: // em dash
|
||||
case 0x2015: // horizontal bar
|
||||
case 0x2043: // hyphen bullet
|
||||
case 0x207B: // superscript minus
|
||||
case 0x208B: // subscript minus
|
||||
case 0x2212: // minus sign
|
||||
case 0x2E17: // double oblique hyphen
|
||||
case 0x2E3A: // two-em dash
|
||||
case 0x2E3B: // three-em dash
|
||||
case 0xFE58: // small em dash
|
||||
case 0xFE63: // small hyphen-minus
|
||||
case 0xFF0D: // fullwidth hyphen-minus
|
||||
case 0x005F: // Underscore
|
||||
case 0x2026: // Ellipsis
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool isSoftHyphen(const uint32_t cp) { return cp == 0x00AD; }
|
||||
|
||||
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps) {
|
||||
if (cps.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove trailing footnote references like [12], even if punctuation trails after the closing bracket.
|
||||
if (cps.size() >= 3) {
|
||||
int end = static_cast<int>(cps.size()) - 1;
|
||||
while (end >= 0 && isPunctuation(cps[end].value)) {
|
||||
--end;
|
||||
}
|
||||
int pos = end;
|
||||
if (pos >= 0 && isAsciiDigit(cps[pos].value)) {
|
||||
while (pos >= 0 && isAsciiDigit(cps[pos].value)) {
|
||||
--pos;
|
||||
}
|
||||
if (pos >= 0 && cps[pos].value == '[' && end - pos > 1) {
|
||||
cps.erase(cps.begin() + pos, cps.end());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while (!cps.empty() && isPunctuation(cps.front().value)) {
|
||||
cps.erase(cps.begin());
|
||||
}
|
||||
while (!cps.empty() && isPunctuation(cps.back().value)) {
|
||||
cps.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<CodepointInfo> collectCodepoints(const std::string& word) {
|
||||
std::vector<CodepointInfo> cps;
|
||||
cps.reserve(word.size());
|
||||
|
||||
const unsigned char* base = reinterpret_cast<const unsigned char*>(word.c_str());
|
||||
const unsigned char* ptr = base;
|
||||
while (*ptr != 0) {
|
||||
const unsigned char* current = ptr;
|
||||
const uint32_t cp = utf8NextCodepoint(&ptr);
|
||||
// If this is a combining diacritic (e.g., U+0301 = acute) and there's
|
||||
// a previous base character that can be composed into a single
|
||||
// precomposed Unicode scalar (Latin-1 / Latin-Extended), do that
|
||||
// composition here. This provides lightweight NFC-like behavior for
|
||||
// common Western European diacritics (acute, grave, circumflex, tilde,
|
||||
// diaeresis, cedilla) without pulling in a full Unicode normalization
|
||||
// library.
|
||||
if (!cps.empty()) {
|
||||
uint32_t prev = cps.back().value;
|
||||
uint32_t composed = 0;
|
||||
switch (cp) {
|
||||
case 0x0300: // grave
|
||||
switch (prev) {
|
||||
case 0x0041:
|
||||
composed = 0x00C0;
|
||||
break; // A -> À
|
||||
case 0x0061:
|
||||
composed = 0x00E0;
|
||||
break; // a -> à
|
||||
case 0x0045:
|
||||
composed = 0x00C8;
|
||||
break; // E -> È
|
||||
case 0x0065:
|
||||
composed = 0x00E8;
|
||||
break; // e -> è
|
||||
case 0x0049:
|
||||
composed = 0x00CC;
|
||||
break; // I -> Ì
|
||||
case 0x0069:
|
||||
composed = 0x00EC;
|
||||
break; // i -> ì
|
||||
case 0x004F:
|
||||
composed = 0x00D2;
|
||||
break; // O -> Ò
|
||||
case 0x006F:
|
||||
composed = 0x00F2;
|
||||
break; // o -> ò
|
||||
case 0x0055:
|
||||
composed = 0x00D9;
|
||||
break; // U -> Ù
|
||||
case 0x0075:
|
||||
composed = 0x00F9;
|
||||
break; // u -> ù
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x0301: // acute
|
||||
switch (prev) {
|
||||
case 0x0041:
|
||||
composed = 0x00C1;
|
||||
break; // A -> Á
|
||||
case 0x0061:
|
||||
composed = 0x00E1;
|
||||
break; // a -> á
|
||||
case 0x0045:
|
||||
composed = 0x00C9;
|
||||
break; // E -> É
|
||||
case 0x0065:
|
||||
composed = 0x00E9;
|
||||
break; // e -> é
|
||||
case 0x0049:
|
||||
composed = 0x00CD;
|
||||
break; // I -> Í
|
||||
case 0x0069:
|
||||
composed = 0x00ED;
|
||||
break; // i -> í
|
||||
case 0x004F:
|
||||
composed = 0x00D3;
|
||||
break; // O -> Ó
|
||||
case 0x006F:
|
||||
composed = 0x00F3;
|
||||
break; // o -> ó
|
||||
case 0x0055:
|
||||
composed = 0x00DA;
|
||||
break; // U -> Ú
|
||||
case 0x0075:
|
||||
composed = 0x00FA;
|
||||
break; // u -> ú
|
||||
case 0x0059:
|
||||
composed = 0x00DD;
|
||||
break; // Y -> Ý
|
||||
case 0x0079:
|
||||
composed = 0x00FD;
|
||||
break; // y -> ý
|
||||
case 0x0043:
|
||||
composed = 0x0106;
|
||||
break; // C -> Ć
|
||||
case 0x0063:
|
||||
composed = 0x0107;
|
||||
break; // c -> ć
|
||||
case 0x004E:
|
||||
composed = 0x0143;
|
||||
break; // N -> Ń
|
||||
case 0x006E:
|
||||
composed = 0x0144;
|
||||
break; // n -> ń
|
||||
case 0x0053:
|
||||
composed = 0x015A;
|
||||
break; // S -> Ś
|
||||
case 0x0073:
|
||||
composed = 0x015B;
|
||||
break; // s -> ś
|
||||
case 0x005A:
|
||||
composed = 0x0179;
|
||||
break; // Z -> Ź
|
||||
case 0x007A:
|
||||
composed = 0x017A;
|
||||
break; // z -> ź
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x0302: // circumflex
|
||||
switch (prev) {
|
||||
case 0x0041:
|
||||
composed = 0x00C2;
|
||||
break; // A -> Â
|
||||
case 0x0061:
|
||||
composed = 0x00E2;
|
||||
break; // a -> â
|
||||
case 0x0045:
|
||||
composed = 0x00CA;
|
||||
break; // E -> Ê
|
||||
case 0x0065:
|
||||
composed = 0x00EA;
|
||||
break; // e -> ê
|
||||
case 0x0049:
|
||||
composed = 0x00CE;
|
||||
break; // I -> Î
|
||||
case 0x0069:
|
||||
composed = 0x00EE;
|
||||
break; // i -> î
|
||||
case 0x004F:
|
||||
composed = 0x00D4;
|
||||
break; // O -> Ô
|
||||
case 0x006F:
|
||||
composed = 0x00F4;
|
||||
break; // o -> ô
|
||||
case 0x0055:
|
||||
composed = 0x00DB;
|
||||
break; // U -> Û
|
||||
case 0x0075:
|
||||
composed = 0x00FB;
|
||||
break; // u -> û
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x0303: // tilde
|
||||
switch (prev) {
|
||||
case 0x0041:
|
||||
composed = 0x00C3;
|
||||
break; // A -> Ã
|
||||
case 0x0061:
|
||||
composed = 0x00E3;
|
||||
break; // a -> ã
|
||||
case 0x004E:
|
||||
composed = 0x00D1;
|
||||
break; // N -> Ñ
|
||||
case 0x006E:
|
||||
composed = 0x00F1;
|
||||
break; // n -> ñ
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x0308: // diaeresis/umlaut
|
||||
switch (prev) {
|
||||
case 0x0041:
|
||||
composed = 0x00C4;
|
||||
break; // A -> Ä
|
||||
case 0x0061:
|
||||
composed = 0x00E4;
|
||||
break; // a -> ä
|
||||
case 0x0045:
|
||||
composed = 0x00CB;
|
||||
break; // E -> Ë
|
||||
case 0x0065:
|
||||
composed = 0x00EB;
|
||||
break; // e -> ë
|
||||
case 0x0049:
|
||||
composed = 0x00CF;
|
||||
break; // I -> Ï
|
||||
case 0x0069:
|
||||
composed = 0x00EF;
|
||||
break; // i -> ï
|
||||
case 0x004F:
|
||||
composed = 0x00D6;
|
||||
break; // O -> Ö
|
||||
case 0x006F:
|
||||
composed = 0x00F6;
|
||||
break; // o -> ö
|
||||
case 0x0055:
|
||||
composed = 0x00DC;
|
||||
break; // U -> Ü
|
||||
case 0x0075:
|
||||
composed = 0x00FC;
|
||||
break; // u -> ü
|
||||
case 0x0059:
|
||||
composed = 0x0178;
|
||||
break; // Y -> Ÿ
|
||||
case 0x0079:
|
||||
composed = 0x00FF;
|
||||
break; // y -> ÿ
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x0307: // dot above (Polish Ż)
|
||||
switch (prev) {
|
||||
case 0x005A:
|
||||
composed = 0x017B;
|
||||
break; // Z -> Ż
|
||||
case 0x007A:
|
||||
composed = 0x017C;
|
||||
break; // z -> ż
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x0327: // cedilla
|
||||
switch (prev) {
|
||||
case 0x0043:
|
||||
composed = 0x00C7;
|
||||
break; // C -> Ç
|
||||
case 0x0063:
|
||||
composed = 0x00E7;
|
||||
break; // c -> ç
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 0x0328: // ogonek (Polish Ą, Ę)
|
||||
switch (prev) {
|
||||
case 0x0041:
|
||||
composed = 0x0104;
|
||||
break; // A -> Ą
|
||||
case 0x0061:
|
||||
composed = 0x0105;
|
||||
break; // a -> ą
|
||||
case 0x0045:
|
||||
composed = 0x0118;
|
||||
break; // E -> Ę
|
||||
case 0x0065:
|
||||
composed = 0x0119;
|
||||
break; // e -> ę
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (composed != 0) {
|
||||
cps.back().value = composed;
|
||||
continue; // skip pushing the combining mark itself
|
||||
}
|
||||
}
|
||||
|
||||
cps.push_back({cp, static_cast<size_t>(current - base)});
|
||||
}
|
||||
|
||||
return cps;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
struct CodepointInfo {
|
||||
uint32_t value;
|
||||
size_t byteOffset;
|
||||
};
|
||||
|
||||
uint32_t toLowerLatin(uint32_t cp);
|
||||
uint32_t toLowerCyrillic(uint32_t cp);
|
||||
|
||||
bool isLatinLetter(uint32_t cp);
|
||||
bool isCyrillicLetter(uint32_t cp);
|
||||
|
||||
bool isAlphabetic(uint32_t cp);
|
||||
bool isPunctuation(uint32_t cp);
|
||||
bool isAsciiDigit(uint32_t cp);
|
||||
bool isApostrophe(uint32_t cp);
|
||||
bool isExplicitHyphen(uint32_t cp);
|
||||
bool isSoftHyphen(uint32_t cp);
|
||||
void trimSurroundingPunctuationAndFootnote(std::vector<CodepointInfo>& cps);
|
||||
std::vector<CodepointInfo> collectCodepoints(const std::string& word);
|
||||
@@ -1,275 +0,0 @@
|
||||
#include "Hyphenator.h"
|
||||
|
||||
#include <Utf8.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <vector>
|
||||
|
||||
#include "HyphenationCommon.h"
|
||||
#include "LanguageHyphenator.h"
|
||||
#include "LanguageRegistry.h"
|
||||
|
||||
const LanguageHyphenator* Hyphenator::cachedHyphenator_ = nullptr;
|
||||
|
||||
namespace {
|
||||
|
||||
// Normalize ISO 639-2 (three-letter) codes to ISO 639-1 (two-letter) codes used by the
|
||||
// hyphenation registry. EPUBs may use either form in their dc:language metadata (e.g.
|
||||
// "eng" instead of "en"). Both the bibliographic ("fre"/"ger") and terminological
|
||||
// ("fra"/"deu") ISO 639-2 variants are mapped.
|
||||
struct Iso639Mapping {
|
||||
const char* iso639_2;
|
||||
const char* iso639_1;
|
||||
};
|
||||
static constexpr Iso639Mapping kIso639Mappings[] = {{"eng", "en"}, {"fra", "fr"}, {"fre", "fr"}, {"deu", "de"},
|
||||
{"ger", "de"}, {"rus", "ru"}, {"spa", "es"}, {"ita", "it"},
|
||||
{"ukr", "uk"}, {"swe", "sv"}};
|
||||
|
||||
// Maps a BCP-47 or ISO 639-2 language tag to a language-specific hyphenator.
|
||||
const LanguageHyphenator* hyphenatorForLanguage(const std::string& langTag) {
|
||||
if (langTag.empty()) return nullptr;
|
||||
|
||||
// Extract primary subtag and normalize to lowercase (e.g., "en-US" -> "en", "ENG" -> "en").
|
||||
std::string primary;
|
||||
primary.reserve(langTag.size());
|
||||
for (char c : langTag) {
|
||||
if (c == '-' || c == '_') break;
|
||||
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c - 'A' + 'a');
|
||||
primary.push_back(c);
|
||||
}
|
||||
if (primary.empty()) return nullptr;
|
||||
|
||||
// Normalize ISO 639-2 three-letter codes to two-letter equivalents.
|
||||
for (const auto& mapping : kIso639Mappings) {
|
||||
if (primary == mapping.iso639_2) {
|
||||
primary = mapping.iso639_1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return getLanguageHyphenatorForPrimaryTag(primary);
|
||||
}
|
||||
|
||||
// Maps a codepoint index back to its byte offset inside the source word.
|
||||
size_t byteOffsetForIndex(const std::vector<CodepointInfo>& cps, const size_t index) {
|
||||
return (index < cps.size()) ? cps[index].byteOffset : (cps.empty() ? 0 : cps.back().byteOffset);
|
||||
}
|
||||
|
||||
// Builds a vector of break information from explicit hyphen markers in the given codepoints.
|
||||
// Only hyphens that appear between two alphabetic characters are considered valid breaks.
|
||||
//
|
||||
// Example: "US-Satellitensystems" (cps: U, S, -, S, a, t, ...)
|
||||
// -> finds '-' at index 2 with alphabetic neighbors 'S' and 'S'
|
||||
// -> returns one BreakInfo at the byte offset of 'S' (the char after '-'),
|
||||
// with requiresInsertedHyphen=false because '-' is already visible.
|
||||
//
|
||||
// Example: "Satel\u00ADliten" (soft-hyphen between 'l' and 'l')
|
||||
// -> returns one BreakInfo with requiresInsertedHyphen=true (soft-hyphen
|
||||
// is invisible and needs a visible '-' when the break is used).
|
||||
std::vector<Hyphenator::BreakInfo> buildExplicitBreakInfos(const std::vector<CodepointInfo>& cps) {
|
||||
std::vector<Hyphenator::BreakInfo> breaks;
|
||||
|
||||
for (size_t i = 1; i + 1 < cps.size(); ++i) {
|
||||
const uint32_t cp = cps[i].value;
|
||||
if (!isExplicitHyphen(cp) || !isAlphabetic(cps[i - 1].value) || !isAlphabetic(cps[i + 1].value)) {
|
||||
continue;
|
||||
}
|
||||
// Offset points to the next codepoint so rendering starts after the hyphen marker.
|
||||
breaks.push_back({cps[i + 1].byteOffset, isSoftHyphen(cp)});
|
||||
}
|
||||
|
||||
return breaks;
|
||||
}
|
||||
|
||||
bool isSegmentSeparator(const uint32_t cp) { return isExplicitHyphen(cp) || isApostrophe(cp); }
|
||||
|
||||
void appendSegmentPatternBreaks(const std::vector<CodepointInfo>& cps, const LanguageHyphenator& hyphenator,
|
||||
const bool includeFallback, std::vector<Hyphenator::BreakInfo>& outBreaks) {
|
||||
size_t segStart = 0;
|
||||
|
||||
for (size_t i = 0; i <= cps.size(); ++i) {
|
||||
const bool atEnd = i == cps.size();
|
||||
const bool atSeparator = !atEnd && isSegmentSeparator(cps[i].value);
|
||||
if (!atEnd && !atSeparator) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i > segStart) {
|
||||
std::vector<CodepointInfo> segment(cps.begin() + segStart, cps.begin() + i);
|
||||
auto segIndexes = hyphenator.breakIndexes(segment);
|
||||
|
||||
if (includeFallback && segIndexes.empty()) {
|
||||
const size_t minPrefix = hyphenator.minPrefix();
|
||||
const size_t minSuffix = hyphenator.minSuffix();
|
||||
for (size_t idx = minPrefix; idx + minSuffix <= segment.size(); ++idx) {
|
||||
segIndexes.push_back(idx);
|
||||
}
|
||||
}
|
||||
|
||||
for (const size_t idx : segIndexes) {
|
||||
assert(idx > 0 && idx < segment.size());
|
||||
if (idx == 0 || idx >= segment.size()) continue;
|
||||
const size_t cpIdx = segStart + idx;
|
||||
if (cpIdx < cps.size()) {
|
||||
outBreaks.push_back({cps[cpIdx].byteOffset, true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
segStart = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void appendApostropheContractionBreaks(const std::vector<CodepointInfo>& cps,
|
||||
std::vector<Hyphenator::BreakInfo>& outBreaks) {
|
||||
constexpr size_t kMinLeftSegmentLen = 3;
|
||||
constexpr size_t kMinRightSegmentLen = 3;
|
||||
size_t segmentStart = 0;
|
||||
|
||||
for (size_t i = 0; i < cps.size(); ++i) {
|
||||
if (isSegmentSeparator(cps[i].value)) {
|
||||
if (isApostrophe(cps[i].value) && i > 0 && i + 1 < cps.size() && isAlphabetic(cps[i - 1].value) &&
|
||||
isAlphabetic(cps[i + 1].value)) {
|
||||
size_t leftPrefixLen = 0;
|
||||
for (size_t j = segmentStart; j < i; ++j) {
|
||||
if (isAlphabetic(cps[j].value)) {
|
||||
++leftPrefixLen;
|
||||
}
|
||||
}
|
||||
|
||||
size_t rightSuffixLen = 0;
|
||||
for (size_t j = i + 1; j < cps.size() && !isSegmentSeparator(cps[j].value); ++j) {
|
||||
if (isAlphabetic(cps[j].value)) {
|
||||
++rightSuffixLen;
|
||||
}
|
||||
}
|
||||
|
||||
// Avoid stranding short clitics like "l'"/"d'" or contraction tails like "'ve"/"'re"/"'ll".
|
||||
if (leftPrefixLen >= kMinLeftSegmentLen && rightSuffixLen >= kMinRightSegmentLen) {
|
||||
outBreaks.push_back({cps[i + 1].byteOffset, false});
|
||||
}
|
||||
}
|
||||
segmentStart = i + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void sortAndDedupeBreakInfos(std::vector<Hyphenator::BreakInfo>& infos) {
|
||||
std::sort(infos.begin(), infos.end(), [](const Hyphenator::BreakInfo& a, const Hyphenator::BreakInfo& b) {
|
||||
if (a.byteOffset != b.byteOffset) {
|
||||
return a.byteOffset < b.byteOffset;
|
||||
}
|
||||
return a.requiresInsertedHyphen < b.requiresInsertedHyphen;
|
||||
});
|
||||
|
||||
infos.erase(std::unique(infos.begin(), infos.end(),
|
||||
[](const Hyphenator::BreakInfo& a, const Hyphenator::BreakInfo& b) {
|
||||
return a.byteOffset == b.byteOffset;
|
||||
}),
|
||||
infos.end());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::vector<Hyphenator::BreakInfo> Hyphenator::breakOffsets(const std::string& word, const bool includeFallback) {
|
||||
if (word.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Convert to codepoints and normalize word boundaries.
|
||||
auto cps = collectCodepoints(word);
|
||||
trimSurroundingPunctuationAndFootnote(cps);
|
||||
const auto* hyphenator = cachedHyphenator_;
|
||||
|
||||
// Detect apostrophe-like separators early; used by both branches below.
|
||||
bool hasApostropheLikeSeparator = false;
|
||||
for (const auto& cp : cps) {
|
||||
if (isApostrophe(cp.value)) {
|
||||
hasApostropheLikeSeparator = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Explicit hyphen markers (soft or hard) take precedence over language breaks.
|
||||
auto explicitBreakInfos = buildExplicitBreakInfos(cps);
|
||||
if (!explicitBreakInfos.empty()) {
|
||||
// When a word contains explicit hyphens we also run Liang patterns on each alphabetic
|
||||
// segment between them. Without this, "US-Satellitensystems" would only offer one split
|
||||
// point (after "US-"), making it impossible to break mid-"Satellitensystems" even when
|
||||
// "US-Satelliten-" would fit on the line.
|
||||
//
|
||||
// Example: "US-Satellitensystems"
|
||||
// Segments: ["US", "Satellitensystems"]
|
||||
// Explicit break: after "US-" -> @3 (no inserted hyphen)
|
||||
// Pattern breaks on "Satellitensystems" -> @5 Sa|tel (+hyphen)
|
||||
// @8 Satel|li (+hyphen)
|
||||
// @10 Satelli|ten (+hyphen)
|
||||
// @13 Satelliten|sys (+hyphen)
|
||||
// @16 Satellitensys|tems (+hyphen)
|
||||
// Result: 6 sorted break points; the line-breaker picks the widest prefix that fits.
|
||||
if (hyphenator) {
|
||||
appendSegmentPatternBreaks(cps, *hyphenator, /*includeFallback=*/false, explicitBreakInfos);
|
||||
}
|
||||
// Also add apostrophe contraction breaks when present (e.g. "l'état-major"
|
||||
// has both an explicit hyphen and an apostrophe that can independently break).
|
||||
if (hasApostropheLikeSeparator) {
|
||||
appendApostropheContractionBreaks(cps, explicitBreakInfos);
|
||||
}
|
||||
// Merge all break points into ascending byte-offset order.
|
||||
sortAndDedupeBreakInfos(explicitBreakInfos);
|
||||
return explicitBreakInfos;
|
||||
}
|
||||
|
||||
// Apostrophe-like separators split compounds into alphabetic segments; run Liang on each segment.
|
||||
// This allows words like "all'improvviso" to hyphenate within "improvviso" instead of becoming
|
||||
// completely unsplittable due to the apostrophe punctuation. Apostrophe contraction breaks are
|
||||
// applied regardless of whether a language hyphenator is available.
|
||||
if (hasApostropheLikeSeparator) {
|
||||
std::vector<BreakInfo> segmentedBreaks;
|
||||
if (hyphenator) {
|
||||
appendSegmentPatternBreaks(cps, *hyphenator, includeFallback, segmentedBreaks);
|
||||
}
|
||||
appendApostropheContractionBreaks(cps, segmentedBreaks);
|
||||
sortAndDedupeBreakInfos(segmentedBreaks);
|
||||
return segmentedBreaks;
|
||||
}
|
||||
|
||||
// Ask language hyphenator for legal break points.
|
||||
std::vector<size_t> indexes;
|
||||
if (hyphenator) {
|
||||
indexes = hyphenator->breakIndexes(cps);
|
||||
}
|
||||
|
||||
// Only add fallback breaks if needed
|
||||
if (includeFallback && indexes.empty()) {
|
||||
const size_t minPrefix = hyphenator ? hyphenator->minPrefix() : LiangWordConfig::kDefaultMinPrefix;
|
||||
const size_t minSuffix = hyphenator ? hyphenator->minSuffix() : LiangWordConfig::kDefaultMinSuffix;
|
||||
for (size_t idx = minPrefix; idx + minSuffix <= cps.size(); ++idx) {
|
||||
indexes.push_back(idx);
|
||||
}
|
||||
}
|
||||
|
||||
if (indexes.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<Hyphenator::BreakInfo> breaks;
|
||||
breaks.reserve(indexes.size());
|
||||
for (const size_t idx : indexes) {
|
||||
// CJK characters can break without inserting a visible hyphen.
|
||||
// Check the codepoint at the break position: if it's a CJK character,
|
||||
// no hyphen is needed since CJK scripts don't use hyphenation.
|
||||
bool needsHyphen = true;
|
||||
if (idx < cps.size() && utf8IsCjkBreakable(cps[idx].value)) {
|
||||
needsHyphen = false;
|
||||
} else if (idx > 0 && utf8IsCjkBreakable(cps[idx - 1].value)) {
|
||||
needsHyphen = false;
|
||||
}
|
||||
breaks.push_back({byteOffsetForIndex(cps, idx), needsHyphen});
|
||||
}
|
||||
|
||||
return breaks;
|
||||
}
|
||||
|
||||
void Hyphenator::setPreferredLanguage(const std::string& lang) { cachedHyphenator_ = hyphenatorForLanguage(lang); }
|
||||
@@ -1,43 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class LanguageHyphenator;
|
||||
|
||||
class Hyphenator {
|
||||
public:
|
||||
struct BreakInfo {
|
||||
size_t byteOffset; // Byte position inside the UTF-8 word where a break may occur.
|
||||
bool requiresInsertedHyphen; // true = a visible '-' must be rendered at the break (pattern/fallback breaks).
|
||||
// false = break occurs at an existing visible separator boundary
|
||||
// (explicit '-' or eligible apostrophe contraction boundary).
|
||||
};
|
||||
|
||||
// Returns byte offsets where the word may be hyphenated.
|
||||
//
|
||||
// Break sources (in priority order):
|
||||
// 1. Explicit hyphens already present in the word (e.g. '-' or soft-hyphen U+00AD).
|
||||
// When found, language patterns are additionally run on each alphabetic segment
|
||||
// between separators so compound words can break within their parts.
|
||||
// Example: "US-Satellitensystems" yields breaks after "US-" (no inserted hyphen)
|
||||
// plus pattern breaks inside "Satellitensystems" (Sa|tel|li|ten|sys|tems).
|
||||
// 2. Apostrophe contractions between letters (e.g. all'improvviso).
|
||||
// Liang patterns are run per alphabetic segment around apostrophes.
|
||||
// A direct break at the apostrophe boundary is allowed only when the left
|
||||
// segment has at least 3 letters and the right segment has at least 3 letters,
|
||||
// avoiding short clitics (e.g. l', d') and contraction tails (e.g. 've, 're, 'll).
|
||||
// 3. Language-specific Liang patterns (e.g. German de_patterns).
|
||||
// Example: "Quadratkilometer" -> Qua|drat|ki|lo|me|ter.
|
||||
// 4. Fallback every-N-chars splitting (only when includeFallback is true AND no
|
||||
// pattern breaks were found). Used as a last resort to prevent a single oversized
|
||||
// word from overflowing the page width.
|
||||
static std::vector<BreakInfo> breakOffsets(const std::string& word, bool includeFallback);
|
||||
|
||||
// Provide a publication-level language hint (e.g. "en", "en-US", "ru") used to select hyphenation rules.
|
||||
static void setPreferredLanguage(const std::string& lang);
|
||||
|
||||
private:
|
||||
static const LanguageHyphenator* cachedHyphenator_;
|
||||
};
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "LiangHyphenation.h"
|
||||
|
||||
// Generic Liang-backed hyphenator that stores pattern metadata plus language-specific helpers.
|
||||
class LanguageHyphenator {
|
||||
public:
|
||||
LanguageHyphenator(const SerializedHyphenationPatterns& patterns, bool (*isLetterFn)(uint32_t),
|
||||
uint32_t (*toLowerFn)(uint32_t), size_t minPrefix = LiangWordConfig::kDefaultMinPrefix,
|
||||
size_t minSuffix = LiangWordConfig::kDefaultMinSuffix)
|
||||
: patterns_(patterns), config_(isLetterFn, toLowerFn, minPrefix, minSuffix) {}
|
||||
|
||||
std::vector<size_t> breakIndexes(const std::vector<CodepointInfo>& cps) const {
|
||||
return liangBreakIndexes(cps, patterns_, config_);
|
||||
}
|
||||
|
||||
size_t minPrefix() const { return config_.minPrefix; }
|
||||
size_t minSuffix() const { return config_.minSuffix; }
|
||||
|
||||
protected:
|
||||
const SerializedHyphenationPatterns& patterns_;
|
||||
LiangWordConfig config_;
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
#include "LanguageRegistry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
#include "HyphenationCommon.h"
|
||||
#include "generated/hyph-de.trie.h"
|
||||
#include "generated/hyph-en.trie.h"
|
||||
#include "generated/hyph-es.trie.h"
|
||||
#include "generated/hyph-fr.trie.h"
|
||||
#include "generated/hyph-it.trie.h"
|
||||
#include "generated/hyph-pl.trie.h"
|
||||
#include "generated/hyph-ru.trie.h"
|
||||
#include "generated/hyph-sv.trie.h"
|
||||
#include "generated/hyph-uk.trie.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// English hyphenation patterns (3/3 minimum prefix/suffix length)
|
||||
LanguageHyphenator englishHyphenator(en_patterns, isLatinLetter, toLowerLatin, 3, 3);
|
||||
LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator russianHyphenator(ru_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator italianHyphenator(it_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator swedishHyphenator(sv_patterns, isLatinLetter, toLowerLatin);
|
||||
LanguageHyphenator ukrainianHyphenator(uk_patterns, isCyrillicLetter, toLowerCyrillic);
|
||||
LanguageHyphenator polishHyphenator(pl_patterns, isLatinLetter, toLowerLatin);
|
||||
|
||||
using EntryArray = std::array<LanguageEntry, 9>;
|
||||
|
||||
const EntryArray& entries() {
|
||||
static const EntryArray kEntries = {{{"english", "en", &englishHyphenator},
|
||||
{"french", "fr", &frenchHyphenator},
|
||||
{"german", "de", &germanHyphenator},
|
||||
{"russian", "ru", &russianHyphenator},
|
||||
{"spanish", "es", &spanishHyphenator},
|
||||
{"italian", "it", &italianHyphenator},
|
||||
{"polish", "pl", &polishHyphenator},
|
||||
{"swedish", "sv", &swedishHyphenator},
|
||||
{"ukrainian", "uk", &ukrainianHyphenator}}};
|
||||
return kEntries;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag) {
|
||||
const auto& allEntries = entries();
|
||||
const auto it = std::find_if(allEntries.begin(), allEntries.end(),
|
||||
[&primaryTag](const LanguageEntry& entry) { return primaryTag == entry.primaryTag; });
|
||||
return (it != allEntries.end()) ? it->hyphenator : nullptr;
|
||||
}
|
||||
|
||||
LanguageEntryView getLanguageEntries() {
|
||||
const auto& allEntries = entries();
|
||||
return LanguageEntryView{allEntries.data(), allEntries.size()};
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
|
||||
#include "LanguageHyphenator.h"
|
||||
|
||||
struct LanguageEntry {
|
||||
const char* cliName;
|
||||
const char* primaryTag;
|
||||
const LanguageHyphenator* hyphenator;
|
||||
};
|
||||
|
||||
struct LanguageEntryView {
|
||||
const LanguageEntry* data;
|
||||
size_t size;
|
||||
|
||||
const LanguageEntry* begin() const { return data; }
|
||||
const LanguageEntry* end() const { return data + size; }
|
||||
};
|
||||
|
||||
// Returns the Liang-backed hyphenator for a given primary language tag (e.g., "en", "fr").
|
||||
const LanguageHyphenator* getLanguageHyphenatorForPrimaryTag(const std::string& primaryTag);
|
||||
|
||||
// Exposes the list of supported languages primarily for tooling/tests.
|
||||
LanguageEntryView getLanguageEntries();
|
||||
@@ -1,417 +0,0 @@
|
||||
#include "LiangHyphenation.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
/*
|
||||
* Liang hyphenation pipeline overview (Typst-style binary trie variant)
|
||||
* --------------------------------------------------------------------
|
||||
* 1. Input normalization (buildAugmentedWord)
|
||||
* - Accepts a vector of CodepointInfo structs emitted by the EPUB text
|
||||
* parser. Each codepoint is validated with LiangWordConfig::isLetter so
|
||||
* we abort early on digits, punctuation, etc. If the word is valid we
|
||||
* build an "augmented" byte sequence: leading '.', lowercase UTF-8 bytes
|
||||
* for every letter, then a trailing '.'. While doing this we capture the
|
||||
* UTF-8 byte offset for each character and a reverse lookup table that
|
||||
* maps UTF-8 byte indexes back to codepoint indexes. This lets the rest
|
||||
* of the algorithm stay byte-oriented (matching the serialized automaton)
|
||||
* while still emitting hyphen positions in codepoint space.
|
||||
*
|
||||
* 2. Automaton decoding
|
||||
* - SerializedHyphenationPatterns stores a contiguous blob generated from
|
||||
* Typst's binary tries. The first 4 bytes contain the root offset. Each
|
||||
* node packs transitions, variable-stride relative offsets to child
|
||||
* nodes, and an optional pointer into a shared "levels" list. We parse
|
||||
* that layout lazily via decodeState/transition, keeping everything in
|
||||
* flash memory; no heap allocations besides the stack-local AutomatonState
|
||||
* structs. getAutomaton caches parseAutomaton results per blob pointer so
|
||||
* multiple words hitting the same language only pay the cost once.
|
||||
*
|
||||
* 3. Pattern application
|
||||
* - We walk the augmented bytes left-to-right. For each starting byte we
|
||||
* stream transitions through the trie, terminating when a transition
|
||||
* fails. Whenever a node exposes level data we expand the packed
|
||||
* "dist+level" bytes: `dist` is the delta (in UTF-8 bytes) from the
|
||||
* starting cursor and `level` is the Liang priority digit. Using the
|
||||
* byte→codepoint lookup we mark the corresponding index in `scores`.
|
||||
* Scores are only updated if the new level is higher, mirroring Liang's
|
||||
* "max digit wins" rule.
|
||||
*
|
||||
* 4. Output filtering
|
||||
* - collectBreakIndexes converts odd-valued score entries back to codepoint
|
||||
* break positions while enforcing `minPrefix`/`minSuffix` constraints from
|
||||
* LiangWordConfig. The caller (language-specific hyphenators) can then
|
||||
* translate these indexes into renderer glyph offsets, page layout data,
|
||||
* etc.
|
||||
*
|
||||
* Keeping the entire algorithm small and deterministic is critical on the
|
||||
* ESP32-C3: we avoid recursion, dynamic allocations per node, or copying the
|
||||
* trie. All lookups stay within the generated blob, which lives in flash, and
|
||||
* the working buffers (augmented bytes/scores) scale with the word length rather
|
||||
* than the pattern corpus.
|
||||
*
|
||||
* Memory design note (heap fragmentation avoidance)
|
||||
* --------------------------------------------------
|
||||
* AugmentedWord previously held three std::vector<> members that were heap-
|
||||
* allocated and freed for every word during layout. For a German-language section
|
||||
* with hundreds of words, these thousands of small alloc/free cycles fragment
|
||||
* the heap enough to prevent large contiguous allocations (e.g. a 32 KB inflate
|
||||
* ring buffer) even when total free memory is sufficient.
|
||||
*
|
||||
* The fix replaces those vectors with fixed-size C arrays sized for the longest
|
||||
* plausible word. The longest known German word is ~63 codepoints; with up to
|
||||
* 2 UTF-8 bytes per German letter + 2 sentinel dots = 128 bytes. MAX_WORD_BYTES=160
|
||||
* and MAX_WORD_CHARS=70 give comfortable headroom. Words exceeding these limits
|
||||
* are silently skipped (no hyphenation), which is acceptable for correctness.
|
||||
* The struct lives on the render-task stack (8 KB) so no permanent DRAM is wasted.
|
||||
*/
|
||||
|
||||
namespace {
|
||||
|
||||
using EmbeddedAutomaton = SerializedHyphenationPatterns;
|
||||
|
||||
// Upper bounds for the fixed word buffers. Sized for German (longest known word
|
||||
// ≈63 codepoints × 2 UTF-8 bytes + 2 sentinel dots = 128 bytes). Words that
|
||||
// exceed these limits are skipped rather than heap-allocated.
|
||||
static constexpr size_t MAX_WORD_BYTES = 160; // max UTF-8 bytes in augmented word
|
||||
static constexpr size_t MAX_WORD_CHARS = 70; // max codepoints + 2 sentinel dots
|
||||
|
||||
struct AugmentedWord {
|
||||
uint8_t bytes[MAX_WORD_BYTES];
|
||||
size_t charByteOffsets[MAX_WORD_CHARS];
|
||||
int32_t byteToCharIndex[MAX_WORD_BYTES];
|
||||
size_t byteLen = 0;
|
||||
size_t charCount_ = 0;
|
||||
|
||||
bool empty() const { return byteLen == 0; }
|
||||
size_t charCount() const { return charCount_; }
|
||||
};
|
||||
|
||||
// Encode a single Unicode codepoint into UTF-8 and append to word.bytes[].
|
||||
// Returns the number of bytes written, or 0 if the codepoint is invalid or the
|
||||
// buffer would overflow. Surrogates (0xD800–0xDFFF) and values above 0x10FFFF
|
||||
// are not valid Unicode scalar values and are rejected.
|
||||
size_t encodeUtf8(uint32_t cp, AugmentedWord& word) {
|
||||
if ((cp >= 0xD800u && cp <= 0xDFFFu) || cp > 0x10FFFFu) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t encoded[4];
|
||||
size_t len = 0;
|
||||
|
||||
if (cp <= 0x7Fu) {
|
||||
encoded[len++] = static_cast<uint8_t>(cp);
|
||||
} else if (cp <= 0x7FFu) {
|
||||
encoded[len++] = static_cast<uint8_t>(0xC0u | ((cp >> 6) & 0x1Fu));
|
||||
encoded[len++] = static_cast<uint8_t>(0x80u | (cp & 0x3Fu));
|
||||
} else if (cp <= 0xFFFFu) {
|
||||
encoded[len++] = static_cast<uint8_t>(0xE0u | ((cp >> 12) & 0x0Fu));
|
||||
encoded[len++] = static_cast<uint8_t>(0x80u | ((cp >> 6) & 0x3Fu));
|
||||
encoded[len++] = static_cast<uint8_t>(0x80u | (cp & 0x3Fu));
|
||||
} else {
|
||||
encoded[len++] = static_cast<uint8_t>(0xF0u | ((cp >> 18) & 0x07u));
|
||||
encoded[len++] = static_cast<uint8_t>(0x80u | ((cp >> 12) & 0x3Fu));
|
||||
encoded[len++] = static_cast<uint8_t>(0x80u | ((cp >> 6) & 0x3Fu));
|
||||
encoded[len++] = static_cast<uint8_t>(0x80u | (cp & 0x3Fu));
|
||||
}
|
||||
|
||||
if (word.byteLen + len > MAX_WORD_BYTES) {
|
||||
return 0; // overflow: word too long for fixed buffer, skip hyphenation
|
||||
}
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
word.bytes[word.byteLen++] = encoded[i];
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
// Build the dotted, lowercase UTF-8 representation plus lookup tables into `word`.
|
||||
// Returns false if the word should be skipped (empty, non-letter, or too long).
|
||||
bool buildAugmentedWord(AugmentedWord& word, const std::vector<CodepointInfo>& cps, const LiangWordConfig& config) {
|
||||
word.byteLen = 0;
|
||||
word.charCount_ = 0;
|
||||
|
||||
if (cps.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Leading sentinel '.'
|
||||
word.charByteOffsets[word.charCount_++] = 0;
|
||||
word.bytes[word.byteLen++] = '.';
|
||||
|
||||
for (const auto& info : cps) {
|
||||
if (!config.isLetter(info.value)) {
|
||||
word.byteLen = 0;
|
||||
word.charCount_ = 0;
|
||||
return false;
|
||||
}
|
||||
// Reserve one slot for the trailing sentinel and check byte headroom.
|
||||
if (word.charCount_ >= MAX_WORD_CHARS - 1) {
|
||||
word.byteLen = 0;
|
||||
word.charCount_ = 0;
|
||||
return false; // word too long
|
||||
}
|
||||
word.charByteOffsets[word.charCount_++] = word.byteLen;
|
||||
if (encodeUtf8(config.toLower(info.value), word) == 0) {
|
||||
word.byteLen = 0;
|
||||
word.charCount_ = 0;
|
||||
return false; // byte buffer overflow
|
||||
}
|
||||
}
|
||||
|
||||
// Trailing sentinel '.'
|
||||
if (word.charCount_ >= MAX_WORD_CHARS || word.byteLen >= MAX_WORD_BYTES) {
|
||||
word.byteLen = 0;
|
||||
word.charCount_ = 0;
|
||||
return false;
|
||||
}
|
||||
word.charByteOffsets[word.charCount_++] = word.byteLen;
|
||||
word.bytes[word.byteLen++] = '.';
|
||||
|
||||
// Build byte→char reverse index: -1 for mid-codepoint bytes, char index for start bytes.
|
||||
for (size_t i = 0; i < word.byteLen; ++i) {
|
||||
word.byteToCharIndex[i] = -1;
|
||||
}
|
||||
for (size_t i = 0; i < word.charCount_; ++i) {
|
||||
const size_t offset = word.charByteOffsets[i];
|
||||
if (offset < word.byteLen) {
|
||||
word.byteToCharIndex[offset] = static_cast<int32_t>(i);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Decoded view of a single trie node pulled straight out of the serialized blob.
|
||||
// - transitions: contiguous list of next-byte values
|
||||
// - targets: packed relative offsets (1/2/3 bytes) for each transition
|
||||
// - levels: optional pointer into the global levels list with packed dist/level pairs
|
||||
struct AutomatonState {
|
||||
const uint8_t* data = nullptr;
|
||||
size_t size = 0;
|
||||
size_t addr = 0;
|
||||
uint8_t stride = 1;
|
||||
size_t childCount = 0;
|
||||
const uint8_t* transitions = nullptr;
|
||||
const uint8_t* targets = nullptr;
|
||||
const uint8_t* levels = nullptr;
|
||||
size_t levelsLen = 0;
|
||||
|
||||
bool valid() const { return data != nullptr; }
|
||||
};
|
||||
|
||||
// Interpret the node located at `addr`, returning transition metadata.
|
||||
AutomatonState decodeState(const EmbeddedAutomaton& automaton, size_t addr) {
|
||||
AutomatonState state;
|
||||
if (addr >= automaton.size) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const uint8_t* base = automaton.data + addr;
|
||||
size_t remaining = automaton.size - addr;
|
||||
size_t pos = 0;
|
||||
|
||||
const uint8_t header = base[pos++];
|
||||
// Header layout (bits):
|
||||
// 7 - hasLevels flag
|
||||
// 6..5 - stride selector (0 -> 1 byte, otherwise 1|2|3)
|
||||
// 4..0 - child count (5 bits), 31 == overflow -> extra byte
|
||||
const bool hasLevels = (header >> 7) != 0;
|
||||
uint8_t stride = static_cast<uint8_t>((header >> 5) & 0x03u);
|
||||
if (stride == 0) {
|
||||
stride = 1;
|
||||
}
|
||||
size_t childCount = static_cast<size_t>(header & 0x1Fu);
|
||||
if (childCount == 31u) {
|
||||
if (pos >= remaining) {
|
||||
return AutomatonState{};
|
||||
}
|
||||
childCount = base[pos++];
|
||||
}
|
||||
|
||||
const uint8_t* levelsPtr = nullptr;
|
||||
size_t levelsLen = 0;
|
||||
if (hasLevels) {
|
||||
if (pos + 1 >= remaining) {
|
||||
return AutomatonState{};
|
||||
}
|
||||
const uint8_t offsetHi = base[pos++];
|
||||
const uint8_t offsetLoLen = base[pos++];
|
||||
// The 12-bit offset (hi<<4 | top nibble) points into the blob-level levels list.
|
||||
// The bottom nibble stores how many packed entries belong to this node.
|
||||
const size_t offset = (static_cast<size_t>(offsetHi) << 4) | (offsetLoLen >> 4);
|
||||
levelsLen = offsetLoLen & 0x0Fu;
|
||||
if (offset + levelsLen > automaton.size) {
|
||||
return AutomatonState{};
|
||||
}
|
||||
levelsPtr = automaton.data + offset - 4u;
|
||||
}
|
||||
|
||||
if (pos + childCount > remaining) {
|
||||
return AutomatonState{};
|
||||
}
|
||||
const uint8_t* transitions = base + pos;
|
||||
pos += childCount;
|
||||
|
||||
const size_t targetsBytes = childCount * stride;
|
||||
if (pos + targetsBytes > remaining) {
|
||||
return AutomatonState{};
|
||||
}
|
||||
const uint8_t* targets = base + pos;
|
||||
|
||||
state.data = automaton.data;
|
||||
state.size = automaton.size;
|
||||
state.addr = addr;
|
||||
state.stride = stride;
|
||||
state.childCount = childCount;
|
||||
state.transitions = transitions;
|
||||
state.targets = targets;
|
||||
state.levels = levelsPtr;
|
||||
state.levelsLen = levelsLen;
|
||||
return state;
|
||||
}
|
||||
|
||||
// Convert the packed stride-sized delta back into a signed offset.
|
||||
int32_t decodeDelta(const uint8_t* buf, uint8_t stride) {
|
||||
if (stride == 1) {
|
||||
return static_cast<int8_t>(buf[0]);
|
||||
}
|
||||
if (stride == 2) {
|
||||
return static_cast<int16_t>((static_cast<uint16_t>(buf[0]) << 8) | static_cast<uint16_t>(buf[1]));
|
||||
}
|
||||
const int32_t unsignedVal =
|
||||
(static_cast<int32_t>(buf[0]) << 16) | (static_cast<int32_t>(buf[1]) << 8) | static_cast<int32_t>(buf[2]);
|
||||
return unsignedVal - (1 << 23);
|
||||
}
|
||||
|
||||
// Follow a single byte transition from `state`, decoding the child node on success.
|
||||
bool transition(const EmbeddedAutomaton& automaton, const AutomatonState& state, uint8_t letter, AutomatonState& out) {
|
||||
if (!state.valid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Children remain sorted by letter in the serialized blob, but the lists are
|
||||
// short enough that a linear scan keeps code size down compared to binary search.
|
||||
for (size_t idx = 0; idx < state.childCount; ++idx) {
|
||||
if (state.transitions[idx] != letter) {
|
||||
continue;
|
||||
}
|
||||
const uint8_t* deltaPtr = state.targets + idx * state.stride;
|
||||
const int32_t delta = decodeDelta(deltaPtr, state.stride);
|
||||
// Deltas are relative to the current node's address, allowing us to keep all
|
||||
// targets within 24 bits while still referencing further nodes in the blob.
|
||||
const int64_t nextAddr = static_cast<int64_t>(state.addr) + delta;
|
||||
if (nextAddr < 0 || static_cast<size_t>(nextAddr) >= automaton.size) {
|
||||
return false;
|
||||
}
|
||||
out = decodeState(automaton, static_cast<size_t>(nextAddr));
|
||||
return out.valid();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Converts odd score positions back into codepoint indexes, honoring min prefix/suffix constraints.
|
||||
// Each break corresponds to scores[breakIndex + 1] because of the leading '.' sentinel.
|
||||
// Convert odd score entries into hyphen positions while honoring prefix/suffix limits.
|
||||
std::vector<size_t> collectBreakIndexes(const std::vector<CodepointInfo>& cps, const uint8_t* scores,
|
||||
const size_t scoresSize, const size_t minPrefix, const size_t minSuffix) {
|
||||
std::vector<size_t> indexes;
|
||||
const size_t cpCount = cps.size();
|
||||
if (cpCount < 2) {
|
||||
return indexes;
|
||||
}
|
||||
|
||||
for (size_t breakIndex = 1; breakIndex < cpCount; ++breakIndex) {
|
||||
if (breakIndex < minPrefix) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t suffixCount = cpCount - breakIndex;
|
||||
if (suffixCount < minSuffix) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t scoreIdx = breakIndex + 1;
|
||||
if (scoreIdx >= scoresSize) {
|
||||
break;
|
||||
}
|
||||
if ((scores[scoreIdx] & 1u) == 0) {
|
||||
continue;
|
||||
}
|
||||
indexes.push_back(breakIndex);
|
||||
}
|
||||
|
||||
return indexes;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// Entry point that runs the full Liang pipeline for a single word.
|
||||
std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
|
||||
const SerializedHyphenationPatterns& patterns, const LiangWordConfig& config) {
|
||||
// AugmentedWord uses fixed-size C arrays (no heap allocation) to avoid
|
||||
// fragmenting the heap across hundreds of words during page layout.
|
||||
AugmentedWord augmented;
|
||||
if (!buildAugmentedWord(augmented, cps, config)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const EmbeddedAutomaton& automaton = patterns;
|
||||
|
||||
const AutomatonState root = decodeState(automaton, automaton.rootOffset);
|
||||
if (!root.valid()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Liang scores: one entry per augmented char (leading/trailing dots included).
|
||||
// Stack-allocated to avoid heap fragmentation (see memory design note above).
|
||||
uint8_t scores[MAX_WORD_CHARS];
|
||||
for (size_t i = 0; i < augmented.charCount_; ++i) {
|
||||
scores[i] = 0;
|
||||
}
|
||||
|
||||
// Walk every starting character position and stream bytes through the trie.
|
||||
for (size_t charStart = 0; charStart < augmented.charCount_; ++charStart) {
|
||||
const size_t byteStart = augmented.charByteOffsets[charStart];
|
||||
AutomatonState state = root;
|
||||
|
||||
for (size_t cursor = byteStart; cursor < augmented.byteLen; ++cursor) {
|
||||
AutomatonState next;
|
||||
if (!transition(automaton, state, augmented.bytes[cursor], next)) {
|
||||
break; // No more matches for this prefix.
|
||||
}
|
||||
state = next;
|
||||
|
||||
if (state.levels && state.levelsLen > 0) {
|
||||
size_t offset = 0;
|
||||
// Each packed byte stores the byte-distance delta and the Liang level digit.
|
||||
for (size_t i = 0; i < state.levelsLen; ++i) {
|
||||
const uint8_t packed = state.levels[i];
|
||||
const size_t dist = static_cast<size_t>(packed / 10);
|
||||
const uint8_t level = static_cast<uint8_t>(packed % 10);
|
||||
|
||||
offset += dist;
|
||||
const size_t splitByte = byteStart + offset;
|
||||
if (splitByte >= augmented.byteLen) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int32_t boundary = augmented.byteToCharIndex[splitByte];
|
||||
if (boundary < 0) {
|
||||
continue; // Mid-codepoint byte, wait for the next one.
|
||||
}
|
||||
if (boundary < 2 || boundary + 2 > static_cast<int32_t>(augmented.charCount_)) {
|
||||
continue; // Skip splits that land in the leading/trailing sentinels.
|
||||
}
|
||||
|
||||
const size_t idx = static_cast<size_t>(boundary);
|
||||
if (idx >= augmented.charCount_) {
|
||||
continue;
|
||||
}
|
||||
scores[idx] = std::max(scores[idx], level);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collectBreakIndexes(cps, scores, augmented.charCount_, config.minPrefix, config.minSuffix);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
#include "HyphenationCommon.h"
|
||||
#include "SerializedHyphenationTrie.h"
|
||||
|
||||
// Encapsulates every language-specific dial the Liang algorithm needs at runtime. The helpers are
|
||||
// intentionally represented as bare function pointers because we invoke them inside tight loops and
|
||||
// want to avoid the overhead of std::function or functors. The minima default to the TeX-recommended
|
||||
// "2/2" split but individual languages (English, for example) can override them.
|
||||
struct LiangWordConfig {
|
||||
static constexpr size_t kDefaultMinPrefix = 2;
|
||||
static constexpr size_t kDefaultMinSuffix = 2;
|
||||
// Predicate used to reject non-alphabetic characters before pattern lookup. Returning false causes
|
||||
// the entire word to be skipped, matching the behavior of classic TeX hyphenation tables.
|
||||
bool (*isLetter)(uint32_t);
|
||||
// Language-specific case folding that matches how the TeX patterns were authored (usually lower-case
|
||||
// ASCII for Latin and lowercase Cyrillic for Russian). Patterns are stored in UTF-8, so this must
|
||||
// operate on Unicode scalars rather than bytes.
|
||||
uint32_t (*toLower)(uint32_t);
|
||||
// Minimum codepoints required on the left/right of any break. These correspond to TeX's
|
||||
// lefthyphenmin and righthyphenmin knobs.
|
||||
size_t minPrefix;
|
||||
size_t minSuffix;
|
||||
|
||||
// Lightweight aggregate constructor so call sites can declare `const LiangWordConfig config(...)`
|
||||
// without verbose member assignment boilerplate.
|
||||
LiangWordConfig(bool (*letterFn)(uint32_t), uint32_t (*lowerFn)(uint32_t), size_t prefix = kDefaultMinPrefix,
|
||||
size_t suffix = kDefaultMinSuffix)
|
||||
: isLetter(letterFn), toLower(lowerFn), minPrefix(prefix), minSuffix(suffix) {}
|
||||
};
|
||||
|
||||
// Shared Liang pattern evaluator used by every language-specific hyphenator.
|
||||
std::vector<size_t> liangBreakIndexes(const std::vector<CodepointInfo>& cps,
|
||||
const SerializedHyphenationPatterns& patterns, const LiangWordConfig& config);
|
||||
@@ -1,11 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Lightweight descriptor that points at a serialized Liang hyphenation trie stored in flash.
|
||||
struct SerializedHyphenationPatterns {
|
||||
size_t rootOffset;
|
||||
const std::uint8_t* data;
|
||||
size_t size;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,869 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t es_trie_data[] = {
|
||||
0x01, 0x04, 0x16, 0x02, 0x0E, 0x0C, 0x02, 0x16, 0x02, 0x0D, 0x0C, 0x22, 0x0F, 0x2C, 0x0F, 0x22,
|
||||
0x0D, 0x2C, 0x0D, 0x0B, 0x16, 0x0B, 0x20, 0x15, 0x16, 0x15, 0x0C, 0x02, 0x0C, 0x17, 0x0E, 0x04,
|
||||
0x2C, 0x05, 0x04, 0x0D, 0x04, 0x21, 0x04, 0x18, 0x0D, 0x04, 0x17, 0x04, 0x0D, 0x17, 0x04, 0x0E,
|
||||
0x0D, 0x04, 0x0D, 0x21, 0x04, 0x0D, 0x21, 0x21, 0x0F, 0x0E, 0x0F, 0x0E, 0x0D, 0x0F, 0x0E, 0x17,
|
||||
0x33, 0x33, 0x0C, 0x33, 0x16, 0x29, 0x29, 0x0C, 0x29, 0x16, 0x21, 0x0C, 0x21, 0x0E, 0x34, 0x0D,
|
||||
0x3E, 0x36, 0x0D, 0x3F, 0x2B, 0x16, 0x0D, 0x3D, 0x3D, 0x0C, 0x3D, 0x16, 0x1F, 0x1F, 0x16, 0x2A,
|
||||
0x2C, 0x0D, 0x0E, 0x0E, 0x21, 0x1F, 0x0C, 0x2A, 0x0D, 0x2A, 0x0B, 0x2A, 0x0B, 0x0C, 0x2A, 0x0B,
|
||||
0x16, 0x37, 0x20, 0x0C, 0x20, 0x16, 0x35, 0x24, 0x47, 0x47, 0x0C, 0x47, 0x16, 0x20, 0x0B, 0x20,
|
||||
0x0D, 0x0C, 0x20, 0x0D, 0x16, 0x20, 0x20, 0x03, 0x17, 0x0E, 0x0D, 0x23, 0x0E, 0x17, 0x17, 0x17,
|
||||
0x21, 0x16, 0x0D, 0x18, 0x48, 0x49, 0x16, 0x0C, 0x0C, 0x16, 0x0C, 0x16, 0x2D, 0x2B, 0x0E, 0x0D,
|
||||
0x2B, 0x0E, 0x17, 0x17, 0x2B, 0x34, 0x0B, 0x34, 0x0B, 0x0C, 0x34, 0x0B, 0x16, 0x21, 0x20, 0x0D,
|
||||
0x21, 0x0E, 0x17, 0x20, 0x0D, 0x04, 0x0F, 0x19, 0x0C, 0x0D, 0x2E, 0x0F, 0x0E, 0x21, 0x17, 0x0E,
|
||||
0x2D, 0x0E, 0x2B, 0x0E, 0x22, 0x17, 0x17, 0x0E, 0x22, 0x0D, 0x0E, 0x38, 0x19, 0x18, 0x03, 0x0C,
|
||||
0x22, 0x0B, 0x0E, 0x22, 0x0B, 0x18, 0x40, 0x2A, 0x0C, 0x0C, 0x2A, 0x0C, 0x16, 0x18, 0x0D, 0x0C,
|
||||
0x18, 0x0D, 0x0E, 0x2B, 0x21, 0x2B, 0x17, 0x2A, 0x16, 0x02, 0x33, 0x02, 0x33, 0x0C, 0x02, 0x33,
|
||||
0x16, 0x35, 0x0E, 0x04, 0x0C, 0x20, 0x0C, 0x0C, 0x20, 0x0C, 0x16, 0x2B, 0x0E, 0x0E, 0x2B, 0x0E,
|
||||
0x18, 0x04, 0x0D, 0x0E, 0x0D, 0x19, 0x0E, 0x41, 0x10, 0x2A, 0x20, 0x04, 0x0C, 0x0D, 0x03, 0x0E,
|
||||
0x16, 0x0D, 0x0E, 0x18, 0x0F, 0x05, 0x0E, 0x07, 0x0E, 0xA0, 0x00, 0x51, 0xA0, 0x00, 0x71, 0xA0,
|
||||
0x00, 0xC3, 0xA3, 0x00, 0x71, 0x74, 0x6E, 0x7A, 0xFD, 0xFD, 0xFD, 0xA1, 0x00, 0x71, 0x74, 0xF4,
|
||||
0xA1, 0x00, 0x71, 0x6E, 0xEF, 0xA3, 0x00, 0x71, 0x74, 0x73, 0x6E, 0xEA, 0xEA, 0xEA, 0xA2, 0x00,
|
||||
0x71, 0x7A, 0x73, 0xE1, 0xE1, 0xA0, 0x00, 0xA2, 0xB6, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79,
|
||||
0x7A, 0xD1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x01, 0xD2, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD,
|
||||
0x21, 0x6E, 0xFD, 0xA0, 0x05, 0xB1, 0xA0, 0x05, 0xC2, 0xA0, 0x05, 0xE2, 0x25, 0xA1, 0xA9, 0xAD,
|
||||
0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC,
|
||||
0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21, 0x6F, 0xF1, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA0,
|
||||
0x05, 0x81, 0xA0, 0x06, 0x72, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0xA2, 0x05, 0x81, 0x6F, 0x61,
|
||||
0xFA, 0xFD, 0xAE, 0x06, 0x31, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x70, 0x71, 0x73,
|
||||
0x74, 0x76, 0x7A, 0xED, 0xED, 0xF9, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED, 0xED,
|
||||
0xED, 0x21, 0x6E, 0xE1, 0xA0, 0x06, 0x01, 0xA0, 0x06, 0x92, 0xA0, 0x06, 0x12, 0x25, 0xA1, 0xA9,
|
||||
0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x51, 0x21, 0x61, 0xFD, 0x21, 0xAD,
|
||||
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6F, 0xFD, 0x28, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0x6C, 0xDA, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xE3, 0xFD, 0x44, 0x75, 0x62, 0x65, 0x6F,
|
||||
0xFF, 0x65, 0xFF, 0x91, 0xFF, 0xC6, 0xFF, 0xEF, 0xA0, 0x04, 0x41, 0xA0, 0x04, 0x52, 0xA0, 0x04,
|
||||
0x72, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21, 0x61, 0xF1, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x69, 0x75, 0xFE,
|
||||
0xC5, 0xFE, 0xC8, 0xFE, 0xCE, 0xFE, 0xC8, 0xFE, 0xD7, 0xFE, 0xDC, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE,
|
||||
0xC8, 0xFE, 0xDC, 0xFE, 0xC8, 0xFE, 0xE1, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xEA, 0xFE, 0xC8, 0xFE,
|
||||
0xC8, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xC8, 0xFE, 0xF4, 0xFE, 0xF4, 0xFF, 0xC7, 0xFF, 0xFD, 0x41,
|
||||
0x6C, 0xFF, 0x45, 0x21, 0x61, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x72, 0xFF, 0x3B, 0x22, 0x6E, 0x75,
|
||||
0xF9, 0xFC, 0x41, 0x78, 0xFF, 0x32, 0x41, 0x78, 0xFF, 0x34, 0x21, 0xB3, 0xFC, 0x41, 0x6E, 0xFF,
|
||||
0x27, 0xA0, 0x01, 0x52, 0x21, 0x64, 0xFD, 0xA0, 0x06, 0x43, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFA,
|
||||
0x23, 0x6E, 0x70, 0x76, 0xF4, 0xFA, 0xFD, 0x21, 0x74, 0xEA, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFA,
|
||||
0x21, 0x69, 0xED, 0x21, 0x6C, 0xFD, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xEA, 0xF4, 0xF7, 0xFD, 0x21,
|
||||
0x6E, 0xF7, 0x25, 0x61, 0x6F, 0xC3, 0x75, 0x65, 0xBB, 0xC0, 0xC8, 0xCB, 0xFD, 0xA1, 0x00, 0x61,
|
||||
0x69, 0xF5, 0xA0, 0x07, 0xB1, 0x21, 0x62, 0xFD, 0xA0, 0x00, 0xF1, 0x21, 0x68, 0xFD, 0x22, 0x69,
|
||||
0x6F, 0xFA, 0xFA, 0x21, 0x74, 0xF5, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x24, 0x63, 0x73, 0x72,
|
||||
0x74, 0xEF, 0xF2, 0xFD, 0xEC, 0xA2, 0x06, 0x01, 0x69, 0x65, 0xE0, 0xF7, 0xA0, 0x02, 0x91, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x65, 0xFA, 0x21, 0x65, 0xFD, 0x22, 0x65, 0x72, 0xF7, 0xFD, 0x21, 0x6E, 0xEF,
|
||||
0x41, 0x6C, 0xFE, 0x6F, 0x22, 0x65, 0x75, 0xF9, 0xFC, 0x21, 0x74, 0xE3, 0x21, 0x73, 0xFD, 0x21,
|
||||
0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0x41, 0x63, 0xFE, 0x5A, 0x21, 0x73, 0xFC, 0x22, 0x65, 0x69, 0xFD,
|
||||
0xF9, 0x21, 0x64, 0xCB, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xD3,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x6F, 0xB9, 0x21, 0x74, 0xFD, 0xA7, 0x07, 0x62, 0x63, 0x67, 0x70, 0x6C,
|
||||
0x72, 0x78, 0x75, 0xBF, 0xCB, 0xD9, 0xE3, 0xF1, 0xF7, 0xFD, 0x42, 0x63, 0x74, 0xFF, 0xA2, 0xFF,
|
||||
0xA2, 0x41, 0x63, 0xFF, 0x9B, 0x22, 0x69, 0x75, 0xF5, 0xFC, 0x41, 0x69, 0xFF, 0x92, 0x21, 0x63,
|
||||
0xFC, 0x21, 0x69, 0xFD, 0x41, 0xA1, 0xFE, 0x0B, 0x21, 0xC3, 0xFC, 0x41, 0x73, 0xFF, 0x81, 0x21,
|
||||
0x69, 0xFC, 0xA4, 0x07, 0x62, 0x64, 0x66, 0x74, 0x78, 0xE3, 0xEF, 0xF6, 0xFD, 0x41, 0x75, 0xFF,
|
||||
0x8C, 0x21, 0x70, 0xFC, 0x41, 0x6F, 0xFD, 0xEB, 0xA2, 0x07, 0x62, 0x6D, 0x74, 0xF9, 0xFC, 0xA0,
|
||||
0x00, 0xF2, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x43, 0x65,
|
||||
0xC3, 0x74, 0xFF, 0xFA, 0xFF, 0xFD, 0xFF, 0x2A, 0x41, 0x6E, 0xFF, 0x20, 0x21, 0xAD, 0xFC, 0x23,
|
||||
0x65, 0x69, 0xC3, 0xF9, 0xF9, 0xFD, 0x21, 0x64, 0xF9, 0xA3, 0x05, 0x02, 0x6B, 0x70, 0x72, 0xD9,
|
||||
0xE5, 0xFD, 0xA0, 0x07, 0x62, 0xA0, 0x07, 0xA1, 0x21, 0x6C, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x07,
|
||||
0x82, 0x67, 0xFD, 0xA0, 0x07, 0x82, 0xC1, 0x07, 0x82, 0x70, 0xFE, 0xFD, 0x25, 0xA1, 0xA9, 0xAD,
|
||||
0xB3, 0xBA, 0xF2, 0xF7, 0xF7, 0xFA, 0xF7, 0xA0, 0x01, 0xA1, 0x21, 0x62, 0xFD, 0x21, 0x72, 0xFD,
|
||||
0x21, 0x75, 0xFD, 0x48, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0x6E, 0xFE, 0xF2, 0xFF, 0x46,
|
||||
0xFF, 0x7F, 0xFF, 0x95, 0xFF, 0xC6, 0xFF, 0xCF, 0xFF, 0xE9, 0xFF, 0xFD, 0xA0, 0x0A, 0x01, 0x21,
|
||||
0x74, 0xFD, 0x21, 0x75, 0xFD, 0xA2, 0x00, 0x61, 0x6F, 0x61, 0xDE, 0xFD, 0xA0, 0x08, 0x12, 0xA0,
|
||||
0x08, 0x33, 0xC2, 0x07, 0x82, 0x6D, 0x6E, 0xFD, 0x4D, 0xFD, 0x4D, 0xA0, 0x0B, 0x45, 0x23, 0xA1,
|
||||
0xA9, 0xB3, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F, 0xC3, 0xF6, 0xF6, 0xF6, 0xF9, 0x21, 0x73,
|
||||
0xF7, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0xA1, 0x07, 0x82,
|
||||
0x6E, 0xFD, 0xA0, 0x08, 0x63, 0xA0, 0x08, 0x92, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFA, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFF, 0xB9, 0xFF, 0xBC, 0xFF,
|
||||
0xBF, 0xFF, 0xEA, 0xFF, 0x70, 0xFF, 0x70, 0xFF, 0xF5, 0x42, 0x73, 0x75, 0xFF, 0xEA, 0xFF, 0x96,
|
||||
0xA0, 0x09, 0x91, 0x21, 0x68, 0xFD, 0xA1, 0x09, 0x81, 0x63, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x69,
|
||||
0xFD, 0x21, 0x63, 0xFD, 0x21, 0x65, 0xFD, 0xA2, 0x00, 0x61, 0x65, 0x69, 0xE2, 0xFD, 0xA0, 0x00,
|
||||
0x61, 0xA0, 0x0C, 0xC3, 0x21, 0x75, 0xFD, 0xA0, 0x04, 0x91, 0x21, 0x74, 0xFD, 0x22, 0x64, 0x73,
|
||||
0xF7, 0xFD, 0x22, 0x6E, 0x73, 0xF5, 0xF5, 0x21, 0x6F, 0xFB, 0x22, 0x74, 0x7A, 0xED, 0xED, 0x21,
|
||||
0x6E, 0xFB, 0x21, 0x61, 0xFD, 0x21, 0x64, 0xFD, 0x43, 0x63, 0x65, 0x6E, 0xFF, 0xEF, 0xFD, 0x20,
|
||||
0xFF, 0xFD, 0x21, 0x6E, 0xD8, 0x23, 0x65, 0x61, 0x69, 0xD8, 0xF3, 0xFD, 0x21, 0x6C, 0xF9, 0x41,
|
||||
0x69, 0xFD, 0x1D, 0xA0, 0x04, 0xA2, 0xA0, 0x04, 0xC2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xB3, 0xEF, 0xEF, 0xEF,
|
||||
0xEF, 0xEF, 0xF5, 0x22, 0x6C, 0x6F, 0xDC, 0xF1, 0xA2, 0x00, 0x61, 0x61, 0x69, 0xD4, 0xFB, 0xA0,
|
||||
0x0D, 0x43, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x62, 0xF4, 0x21, 0xA1,
|
||||
0xFD, 0x22, 0xC3, 0x61, 0xFD, 0xFA, 0x23, 0x66, 0x6D, 0x72, 0xEF, 0xF2, 0xFB, 0xA0, 0x0D, 0x73,
|
||||
0x21, 0x62, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x0D, 0x42, 0x21, 0x69, 0xFD, 0x21,
|
||||
0x74, 0xFD, 0x21, 0x70, 0xFD, 0x22, 0xA1, 0xB3, 0xF1, 0xFD, 0x21, 0x70, 0xEF, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x6D, 0xE3, 0x21, 0xA1, 0xFD, 0x22, 0x61, 0xC3, 0xFA,
|
||||
0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFE, 0x28, 0x21, 0x73, 0xFC, 0x21, 0x6C,
|
||||
0xCB, 0x22, 0x69, 0x65, 0xFA, 0xFD, 0x45, 0x61, 0xC3, 0x65, 0x69, 0x68, 0xFF, 0xB0, 0xFF, 0xCF,
|
||||
0xFF, 0xDD, 0xFF, 0xEE, 0xFF, 0xFB, 0x21, 0x6E, 0xF0, 0xA0, 0x06, 0xD2, 0x41, 0x69, 0xFB, 0xE3,
|
||||
0xA0, 0x0E, 0x92, 0x21, 0x65, 0xFD, 0xC3, 0x0D, 0xB3, 0x63, 0x72, 0x6A, 0xFF, 0xF6, 0xFB, 0xD9,
|
||||
0xFF, 0xFD, 0x21, 0x6F, 0xEE, 0x21, 0xAD, 0xFD, 0x43, 0x67, 0x69, 0xC3, 0xFB, 0xC7, 0xFF, 0xE8,
|
||||
0xFF, 0xFD, 0xA0, 0x06, 0xB2, 0xA1, 0x0E, 0x92, 0x61, 0xDB, 0x22, 0x63, 0x72, 0xF8, 0xFB, 0x21,
|
||||
0x65, 0xFB, 0x41, 0x72, 0xFB, 0xAD, 0xA1, 0x0E, 0x92, 0x73, 0xCA, 0x23, 0x65, 0x61, 0x69, 0xC5,
|
||||
0xFB, 0xC5, 0x21, 0x61, 0xBE, 0xC6, 0x0D, 0xB3, 0x72, 0x6C, 0x61, 0x6D, 0x74, 0x6F, 0xFF, 0xD3,
|
||||
0xFF, 0xEA, 0xFF, 0xED, 0xFF, 0xF6, 0xFF, 0xFD, 0xFB, 0x9A, 0xA0, 0x05, 0x71, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x64, 0xFD, 0xC3, 0x05, 0x81, 0x64, 0x65, 0x75, 0xFC, 0x8E, 0xFF, 0x9D, 0xFF, 0xFD, 0xC1,
|
||||
0x0E, 0x92, 0x6F, 0xFF, 0x91, 0x43, 0xA1, 0xA9, 0xB3, 0xFF, 0x8B, 0xFF, 0x8B, 0xFF, 0x8B, 0x45,
|
||||
0x61, 0x65, 0x6C, 0x6F, 0xC3, 0xFF, 0x81, 0xFF, 0x81, 0xFF, 0xF0, 0xFF, 0x81, 0xFF, 0xF6, 0x42,
|
||||
0x61, 0x6F, 0xFF, 0x71, 0xFF, 0x71, 0x41, 0x72, 0xFF, 0x8C, 0x21, 0x70, 0xFC, 0x41, 0x72, 0xFF,
|
||||
0x63, 0x21, 0x65, 0xFC, 0x41, 0x61, 0xFB, 0x3B, 0x42, 0x6D, 0x74, 0xFB, 0x37, 0xFF, 0xFC, 0xC7,
|
||||
0x0D, 0xB3, 0x6E, 0x67, 0x6C, 0x7A, 0x6D, 0x63, 0x73, 0xFF, 0xB4, 0xFF, 0x63, 0xFF, 0xD0, 0xFF,
|
||||
0xE0, 0xFF, 0xEB, 0xFF, 0xF2, 0xFF, 0xF9, 0x41, 0x65, 0xFF, 0x5B, 0x41, 0x73, 0xFF, 0x8F, 0x21,
|
||||
0x65, 0xFC, 0xA2, 0x0D, 0xB3, 0x70, 0x72, 0xF5, 0xFD, 0x42, 0x61, 0x65, 0xFF, 0x27, 0xFF, 0x39,
|
||||
0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFF, 0x20, 0xFF, 0x95, 0xFF, 0x20, 0xFF, 0x20, 0xA2, 0x0D, 0xB3,
|
||||
0x72, 0x6C, 0xEC, 0xF3, 0xA0, 0x0D, 0xE3, 0xC1, 0x0D, 0xE3, 0x6E, 0xFA, 0xE8, 0xA0, 0x0E, 0x72,
|
||||
0xA1, 0x05, 0x81, 0x69, 0xFD, 0xA1, 0x0D, 0xE3, 0x6E, 0xFB, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA,
|
||||
0xEA, 0xEA, 0xED, 0xFB, 0xEA, 0x41, 0x76, 0xFF, 0x0D, 0x41, 0x6D, 0xFF, 0x09, 0x22, 0x65, 0x6F,
|
||||
0xF8, 0xFC, 0x48, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0x72, 0xFE, 0xD7, 0xFE, 0xE4, 0xFF,
|
||||
0x23, 0xFF, 0x8D, 0xFF, 0xB0, 0xFF, 0xCB, 0xFF, 0xE8, 0xFF, 0xFB, 0x21, 0x74, 0xE7, 0x21, 0x73,
|
||||
0xFD, 0x41, 0x70, 0xFB, 0xB0, 0x21, 0xBA, 0xFC, 0x22, 0x75, 0xC3, 0xF9, 0xFD, 0xA0, 0x01, 0x11,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD, 0x21, 0x64, 0xFB, 0xA2, 0x04,
|
||||
0xA2, 0x63, 0x72, 0xEA, 0xFD, 0x21, 0x6C, 0xE8, 0x21, 0x75, 0xFD, 0x21, 0x62, 0xFD, 0xA1, 0x04,
|
||||
0xC2, 0x6D, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xFB, 0xFD, 0xE3, 0xFD, 0xE3, 0xFD,
|
||||
0xE3, 0xFD, 0xE3, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFD, 0x94, 0xFD, 0xD0, 0xFD,
|
||||
0xD0, 0xFD, 0xD0, 0xFF, 0xDB, 0xFD, 0xD0, 0xFF, 0xF0, 0x22, 0xA1, 0xAD, 0xB4, 0xB4, 0x24, 0x61,
|
||||
0xC3, 0x65, 0x69, 0xAF, 0xFB, 0xAF, 0xAF, 0x21, 0x62, 0xF7, 0xC2, 0x01, 0x11, 0x61, 0x6F, 0xFF,
|
||||
0xA3, 0xFF, 0xA3, 0x21, 0x62, 0xF7, 0x21, 0xAD, 0xFD, 0xA2, 0x04, 0x91, 0x69, 0xC3, 0xEE, 0xFD,
|
||||
0x41, 0x74, 0xFA, 0x1F, 0x21, 0x72, 0xFC, 0x21, 0x6F, 0xFD, 0xA1, 0x0D, 0xB2, 0x62, 0xFD, 0x41,
|
||||
0x72, 0xFE, 0x63, 0x21, 0x61, 0xFC, 0xA1, 0x0D, 0xB2, 0x74, 0xFD, 0xA0, 0x0D, 0xB2, 0xA0, 0x0E,
|
||||
0xB2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0xCD, 0xDE, 0xEA, 0xEF, 0xEF, 0xEF, 0xF5, 0x42, 0x65, 0x6F, 0xFF, 0x88,
|
||||
0xFF, 0xF1, 0xC3, 0x00, 0x61, 0x61, 0x6F, 0x72, 0xFD, 0xF4, 0xFF, 0x3C, 0xFF, 0xF9, 0x41, 0x72,
|
||||
0xFB, 0x6B, 0x21, 0x65, 0xFC, 0x41, 0xB3, 0xFB, 0x4A, 0x42, 0x6F, 0xC3, 0xFB, 0x46, 0xFF, 0xFC,
|
||||
0x43, 0x72, 0x69, 0x73, 0xFB, 0x3C, 0xFF, 0xF2, 0xFF, 0xF9, 0x42, 0x73, 0x74, 0xFB, 0x32, 0xFB,
|
||||
0x32, 0x41, 0xAD, 0xFB, 0x48, 0x22, 0x69, 0xC3, 0xF5, 0xFC, 0x21, 0x6D, 0xFB, 0x41, 0x6D, 0xFB,
|
||||
0x1F, 0x21, 0x72, 0xFC, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA, 0xFD, 0x41, 0x76, 0xFB, 0x10,
|
||||
0x21, 0xA1, 0xFC, 0xA0, 0x04, 0xE2, 0x21, 0x70, 0xFD, 0x23, 0x61, 0xC3, 0x75, 0xF3, 0xF7, 0xFD,
|
||||
0x21, 0x72, 0xF9, 0x41, 0x69, 0xFB, 0x5E, 0x21, 0x64, 0xFC, 0x21, 0x6E, 0xFD, 0x41, 0xB1, 0xFA,
|
||||
0xEF, 0x21, 0xC3, 0xFC, 0x21, 0xBA, 0xFD, 0x23, 0x6F, 0x75, 0xC3, 0xF3, 0xFA, 0xFD, 0x41, 0x73,
|
||||
0xFF, 0x42, 0x21, 0xBA, 0xFC, 0x42, 0x75, 0xC3, 0xFA, 0xF7, 0xFF, 0xFD, 0x41, 0x67, 0xFA, 0xD3,
|
||||
0x41, 0x7A, 0xFE, 0x14, 0x41, 0x6A, 0xFA, 0xC8, 0x23, 0xA9, 0xAD, 0xB3, 0xF4, 0xF8, 0xFC, 0x42,
|
||||
0x61, 0xC3, 0xF9, 0x40, 0xFB, 0x35, 0x42, 0x6D, 0x74, 0xF9, 0x39, 0xF9, 0x39, 0x43, 0x7A, 0x6D,
|
||||
0x73, 0xFF, 0xF2, 0xFA, 0xAF, 0xFF, 0xF9, 0x45, 0x65, 0xC3, 0x69, 0x6F, 0x71, 0xFF, 0xD5, 0xFF,
|
||||
0xE1, 0xFF, 0xF6, 0xFF, 0xDD, 0xFA, 0xA5, 0x41, 0xAD, 0xFF, 0x76, 0x42, 0x69, 0xC3, 0xFF, 0x72,
|
||||
0xFF, 0xFC, 0x43, 0xA1, 0xA9, 0xB3, 0xFA, 0x8A, 0xFA, 0x8A, 0xFA, 0x8A, 0x44, 0x61, 0xC3, 0x65,
|
||||
0x6F, 0xFA, 0x80, 0xFF, 0xF6, 0xFA, 0x80, 0xFA, 0x80, 0x41, 0x65, 0xFA, 0xD8, 0x21, 0x72, 0xFC,
|
||||
0x42, 0x6E, 0x74, 0xFA, 0xA1, 0xFA, 0x6C, 0xA0, 0x00, 0x40, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x42, 0xA9, 0xAD, 0xFA, 0x94, 0xFF, 0xFD, 0x22, 0x65, 0xC3, 0xE9, 0xF9, 0x22, 0x61, 0x72, 0xE1,
|
||||
0xFB, 0x41, 0x67, 0xFF, 0x42, 0x41, 0xBA, 0xFF, 0x28, 0x42, 0x75, 0xC3, 0xFF, 0x24, 0xFF, 0xFC,
|
||||
0xCE, 0x07, 0x62, 0x62, 0x64, 0x66, 0x67, 0x63, 0x6A, 0x6C, 0x6E, 0x6D, 0x70, 0x65, 0x71, 0x7A,
|
||||
0x73, 0xFF, 0x00, 0xFF, 0x1A, 0xFF, 0x27, 0xFF, 0x40, 0xFF, 0x57, 0xFF, 0x65, 0xFF, 0x97, 0xFF,
|
||||
0xAB, 0xFF, 0xBC, 0xFF, 0xEC, 0xFF, 0xF1, 0xFF, 0x33, 0xFF, 0x33, 0xFF, 0xF9, 0xA0, 0x05, 0x02,
|
||||
0x49, 0x6F, 0x63, 0x69, 0x66, 0x67, 0x76, 0x61, 0x73, 0x74, 0xF8, 0x8F, 0xFA, 0x0C, 0xFA, 0x71,
|
||||
0xFA, 0x0C, 0xFA, 0x0C, 0xFA, 0x0C, 0xF8, 0x8F, 0xFA, 0x0C, 0xFA, 0x0C, 0x41, 0x64, 0xF8, 0x73,
|
||||
0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x07, 0x62, 0x6E, 0x6D, 0x76, 0xFF, 0xDA, 0xFE, 0xDD,
|
||||
0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0xF7, 0x21, 0x65, 0xFC, 0x41, 0x61, 0xF9, 0xD3, 0x22, 0x69, 0x67,
|
||||
0xF9, 0xFC, 0xC4, 0x07, 0x62, 0x62, 0x72, 0x63, 0x6A, 0xFE, 0xC1, 0xFF, 0xFB, 0xF9, 0xCA, 0xFA,
|
||||
0x73, 0x42, 0xA1, 0xB3, 0xF9, 0xBB, 0xF9, 0xBB, 0x43, 0x61, 0xC3, 0x6F, 0xF9, 0xB4, 0xFF, 0xF9,
|
||||
0xF9, 0xB4, 0x42, 0x63, 0x71, 0xFF, 0xF6, 0xF9, 0xAA, 0x42, 0x63, 0x71, 0xFF, 0xD0, 0xF9, 0xA3,
|
||||
0x21, 0xAD, 0xF9, 0x22, 0x69, 0xC3, 0xEF, 0xFD, 0xC1, 0x05, 0x81, 0x74, 0xFC, 0x34, 0x41, 0x74,
|
||||
0xFC, 0x2E, 0x21, 0xA1, 0xFC, 0x22, 0x61, 0xC3, 0xF3, 0xFD, 0x42, 0xA9, 0xB3, 0xF8, 0x05, 0xF8,
|
||||
0x05, 0x48, 0x72, 0x61, 0x73, 0x6D, 0x65, 0xC3, 0x64, 0x66, 0xF7, 0xFE, 0xF7, 0xFE, 0xF7, 0xFE,
|
||||
0xFF, 0x16, 0xF7, 0xFE, 0xFF, 0xF9, 0xF7, 0xFE, 0xF9, 0x7B, 0xC1, 0x05, 0x81, 0x72, 0xF7, 0xE5,
|
||||
0x42, 0xAD, 0xA1, 0xFF, 0xFA, 0xF7, 0xDF, 0x43, 0x69, 0xC3, 0x74, 0xFF, 0xDA, 0xFF, 0xF9, 0xF9,
|
||||
0x55, 0x41, 0xA1, 0xF9, 0x4E, 0x42, 0x61, 0xC3, 0xF9, 0x4A, 0xFF, 0xFC, 0x41, 0x7A, 0xF9, 0x40,
|
||||
0x21, 0xAD, 0xFC, 0x22, 0x69, 0xC3, 0xF9, 0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x69, 0xFD, 0xC5, 0x07,
|
||||
0x62, 0x62, 0x6D, 0x6E, 0x73, 0x74, 0xFF, 0x95, 0xFF, 0xA7, 0xFF, 0xD9, 0xFF, 0xE7, 0xFF, 0xFD,
|
||||
0x43, 0x61, 0x65, 0x6F, 0xF9, 0x1C, 0xF9, 0x1C, 0xF9, 0x1C, 0xC2, 0x07, 0x82, 0x62, 0x6D, 0xF9,
|
||||
0x15, 0xFF, 0xF6, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xF7, 0xF9, 0xF0, 0xF9, 0xF0, 0xF9,
|
||||
0xF0, 0xF9, 0xF0, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFE, 0xBD, 0xFE, 0xEA, 0xFF, 0x13,
|
||||
0xFF, 0x2F, 0xFF, 0xCB, 0xFF, 0xF0, 0xA1, 0x00, 0x61, 0x65, 0xED, 0x43, 0x6E, 0x72, 0x73, 0xFE,
|
||||
0xD2, 0xF8, 0xE1, 0xF8, 0xE1, 0xA0, 0x0C, 0x12, 0xA1, 0x0C, 0x12, 0x72, 0xFD, 0x21, 0x6E, 0xF8,
|
||||
0x21, 0xB3, 0xFD, 0x23, 0x61, 0x6F, 0xC3, 0xF2, 0xF5, 0xFD, 0x41, 0x70, 0xF7, 0x45, 0x41, 0x6E,
|
||||
0xF7, 0x41, 0x21, 0x65, 0xFC, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x73,
|
||||
0x74, 0xEC, 0xFD, 0x41, 0xA9, 0xF8, 0xBA, 0x21, 0x65, 0xD6, 0x21, 0x69, 0xFD, 0xC7, 0x0F, 0x93,
|
||||
0x65, 0x6C, 0x64, 0x6E, 0x72, 0xC3, 0x6D, 0xFF, 0xBE, 0xF9, 0x7B, 0xFF, 0xD6, 0xFF, 0xF1, 0xF8,
|
||||
0x9F, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0xA1, 0xFC, 0xEB, 0x21, 0xC3, 0xFC, 0x42, 0x70, 0x74, 0xF8,
|
||||
0xA9, 0xF7, 0x03, 0x22, 0x75, 0x65, 0xF6, 0xF9, 0x41, 0xA1, 0xF8, 0x74, 0x44, 0x61, 0xC3, 0x65,
|
||||
0x6F, 0xF8, 0x70, 0xFF, 0xFC, 0xF8, 0x70, 0xF8, 0x70, 0x21, 0x74, 0xF3, 0x41, 0x65, 0xF6, 0xE3,
|
||||
0x21, 0x75, 0xFC, 0x21, 0x6C, 0xFD, 0x41, 0x61, 0xFA, 0xF6, 0x41, 0x6E, 0xFC, 0xB6, 0x21, 0x65,
|
||||
0xFC, 0x21, 0x6D, 0xFD, 0x41, 0x65, 0xF8, 0x4B, 0x23, 0x63, 0x69, 0x74, 0xEE, 0xF9, 0xFC, 0x41,
|
||||
0x69, 0xF8, 0x66, 0x21, 0x6D, 0xFC, 0x21, 0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0xC6, 0x0F, 0x93, 0x63,
|
||||
0x73, 0x66, 0x6C, 0x72, 0x74, 0xFF, 0xB7, 0xFF, 0xCD, 0xFF, 0xD7, 0xFF, 0xEC, 0xFB, 0x06, 0xFF,
|
||||
0xFD, 0x41, 0x75, 0xFC, 0x7F, 0x21, 0x63, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x6D, 0xFF, 0x57, 0x21,
|
||||
0x65, 0xFC, 0x21, 0x6C, 0xAA, 0x21, 0x70, 0xFD, 0x41, 0x74, 0xFF, 0x4A, 0x41, 0x65, 0xF8, 0x29,
|
||||
0x41, 0x6D, 0xF6, 0x7F, 0x21, 0xAD, 0xFC, 0x41, 0x75, 0xF8, 0x1E, 0x44, 0x61, 0x69, 0xC3, 0x72,
|
||||
0xF8, 0x1A, 0xFF, 0xF5, 0xFF, 0xF9, 0xFF, 0xFC, 0x22, 0x70, 0x74, 0xE4, 0xF3, 0xA5, 0x0F, 0x93,
|
||||
0x6A, 0x6C, 0x6D, 0x6E, 0x73, 0xCB, 0xD2, 0xD8, 0xDB, 0xFB, 0x41, 0x69, 0xFC, 0x36, 0x21, 0x70,
|
||||
0xFC, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x41, 0x63, 0xFA, 0x65, 0x21, 0x69, 0xFC, 0x41, 0xAD,
|
||||
0xF7, 0xCF, 0x42, 0x69, 0xC3, 0xF7, 0xCB, 0xFF, 0xFC, 0x21, 0x64, 0xF9, 0xA3, 0x0F, 0x93, 0x63,
|
||||
0x66, 0x72, 0xE8, 0xEF, 0xFD, 0x41, 0x62, 0xFA, 0xEF, 0xA1, 0x0F, 0x93, 0x72, 0xFC, 0x42, 0xA9,
|
||||
0xB3, 0xF7, 0x9E, 0xF7, 0x9E, 0x42, 0x61, 0xC3, 0xF7, 0x97, 0xFF, 0xF9, 0x21, 0x74, 0xF9, 0x41,
|
||||
0x74, 0xFF, 0x50, 0xA2, 0x0F, 0xC3, 0x73, 0x72, 0xF9, 0xFC, 0xA0, 0x0F, 0xC3, 0xC3, 0x0F, 0xC3,
|
||||
0x6E, 0x6D, 0x72, 0xFD, 0x8F, 0xFA, 0x1F, 0xF7, 0x7F, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xEA,
|
||||
0xF1, 0xF4, 0xF1, 0xF1, 0x41, 0x79, 0xF8, 0x11, 0x21, 0x61, 0xFC, 0x48, 0x69, 0x68, 0x61, 0x65,
|
||||
0x6F, 0x75, 0xC3, 0x72, 0xFE, 0xC2, 0xF8, 0x91, 0xFF, 0x31, 0xFF, 0x82, 0xFF, 0xB1, 0xFF, 0xBE,
|
||||
0xFF, 0xEE, 0xFF, 0xFD, 0x41, 0x74, 0xF8, 0x78, 0x21, 0x73, 0xFC, 0x41, 0x73, 0xF8, 0x71, 0x21,
|
||||
0x65, 0xFC, 0x22, 0x65, 0x6F, 0xF6, 0xFD, 0x22, 0x62, 0x72, 0xD4, 0xFB, 0x41, 0x73, 0xFD, 0x21,
|
||||
0x21, 0x61, 0xFC, 0x41, 0x61, 0xFD, 0x39, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x62, 0xFD, 0xA3, 0x00, 0x61, 0x75, 0x6F, 0x65, 0xE1, 0xEA, 0xFD, 0x41,
|
||||
0x70, 0xF6, 0x09, 0x21, 0x6D, 0xFC, 0x41, 0x6A, 0xF6, 0x02, 0xA0, 0x05, 0x52, 0x21, 0x74, 0xFD,
|
||||
0x21, 0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0x22, 0x62, 0x6C, 0xF0, 0xFD, 0x22, 0x69, 0x6F, 0xE8, 0xFB,
|
||||
0x21, 0x65, 0xFB, 0x21, 0x6C, 0xFD, 0xC1, 0x0E, 0xB2, 0x67, 0xF9, 0x8A, 0x41, 0x67, 0xF5, 0x63,
|
||||
0xA1, 0x0E, 0xB2, 0x65, 0xFC, 0x41, 0xB1, 0xF9, 0x7B, 0xA1, 0x0E, 0xB2, 0xC3, 0xFC, 0x43, 0xA1,
|
||||
0xA9, 0xB3, 0xF5, 0x51, 0xF5, 0x51, 0xF5, 0x51, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xF5, 0x47, 0xFF,
|
||||
0xF6, 0xF5, 0x47, 0xF5, 0x47, 0x21, 0x74, 0xF3, 0xC2, 0x0E, 0xB2, 0x64, 0x6E, 0xFA, 0x38, 0xFF,
|
||||
0xFD, 0xA0, 0x10, 0xD2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x47,
|
||||
0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF9, 0x3A, 0xFB, 0x1F, 0xFF, 0xB7, 0xFF, 0xC1, 0xFF,
|
||||
0xCA, 0xFF, 0xE9, 0xFF, 0xF5, 0x21, 0x73, 0xEA, 0x41, 0x78, 0xF8, 0x7E, 0x21, 0xB3, 0xFC, 0x21,
|
||||
0xC3, 0xFD, 0x22, 0x61, 0x69, 0xF3, 0xFD, 0xC2, 0x00, 0x61, 0x65, 0x72, 0xFF, 0x8C, 0xFF, 0xFB,
|
||||
0x42, 0x61, 0x6F, 0xF6, 0x48, 0xF6, 0x48, 0x21, 0x65, 0xF9, 0x21, 0x6D, 0xFD, 0x41, 0x65, 0xF6,
|
||||
0x3B, 0x21, 0x65, 0xFC, 0x21, 0x6D, 0xFD, 0x22, 0x75, 0x65, 0xF3, 0xFD, 0x41, 0x65, 0xF5, 0x60,
|
||||
0x21, 0x72, 0xFC, 0x41, 0x6F, 0xF5, 0x59, 0x21, 0x72, 0xFC, 0x41, 0x72, 0xFB, 0x39, 0x21, 0x67,
|
||||
0xFC, 0x21, 0x69, 0xFD, 0x21, 0x70, 0xFD, 0x41, 0x68, 0xFB, 0x2C, 0x21, 0x6F, 0xFC, 0x21, 0x63,
|
||||
0xFD, 0x41, 0x69, 0xF6, 0x72, 0x21, 0x6E, 0xFC, 0x41, 0x72, 0xF6, 0x6B, 0x23, 0x6C, 0x6D, 0x65,
|
||||
0xF2, 0xF9, 0xFC, 0x41, 0xB3, 0xFC, 0x0A, 0x42, 0x6F, 0xC3, 0xFC, 0x06, 0xFF, 0xFC, 0x21, 0x73,
|
||||
0xF9, 0xA0, 0x05, 0x22, 0x21, 0x65, 0xFD, 0x42, 0x6A, 0x6E, 0xFF, 0xFD, 0xF9, 0x78, 0x21, 0x6F,
|
||||
0xF9, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF5, 0x0B, 0x24, 0x65, 0x61, 0x69, 0x74, 0xBC, 0xD4, 0xE6,
|
||||
0xF9, 0x41, 0x74, 0xFF, 0xA2, 0xC4, 0x02, 0xB1, 0x62, 0x63, 0x6E, 0x74, 0xFF, 0x9B, 0xFF, 0xA2,
|
||||
0xFF, 0xF3, 0xFF, 0xFC, 0x41, 0x74, 0xF6, 0xD3, 0x21, 0x73, 0xFC, 0xA1, 0x01, 0x82, 0x65, 0xFD,
|
||||
0x42, 0x6F, 0xC3, 0xF8, 0xA2, 0xFA, 0x85, 0x41, 0x69, 0xF5, 0xE2, 0x21, 0x65, 0xFC, 0x41, 0xA9,
|
||||
0xFD, 0x00, 0x42, 0x65, 0xC3, 0xFC, 0xFC, 0xFF, 0xFC, 0x41, 0x62, 0xF5, 0xB3, 0xA4, 0x09, 0xA3,
|
||||
0x6D, 0x63, 0x6A, 0x72, 0xE3, 0xEE, 0xF5, 0xFC, 0x41, 0xAD, 0xFA, 0xC6, 0x42, 0x69, 0xC3, 0xFA,
|
||||
0xC2, 0xFF, 0xFC, 0xA1, 0x09, 0xA3, 0x6D, 0xF9, 0xA0, 0x09, 0xA3, 0x44, 0x61, 0xC3, 0x65, 0x6F,
|
||||
0xFC, 0x2F, 0xFE, 0xC3, 0xF4, 0x14, 0xF4, 0x14, 0xA1, 0x09, 0xA3, 0x6A, 0xF3, 0x43, 0x61, 0xC3,
|
||||
0x65, 0xF4, 0x02, 0xF5, 0xF7, 0xF4, 0x02, 0x21, 0x72, 0xF6, 0x21, 0x65, 0xFD, 0xA1, 0x09, 0xA3,
|
||||
0x6D, 0xFD, 0xA0, 0x09, 0xD3, 0xC1, 0x09, 0xD3, 0x6A, 0xF6, 0x40, 0x25, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xF7, 0xF7, 0xF7, 0xFA, 0xF7, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFF, 0x85,
|
||||
0xFF, 0xA7, 0xFF, 0xBD, 0xFF, 0xC2, 0xFF, 0xD2, 0xFF, 0xE7, 0xFF, 0xF5, 0x41, 0x73, 0xF6, 0x3B,
|
||||
0x42, 0x6C, 0x75, 0xF6, 0x37, 0xFF, 0xFC, 0x41, 0x6C, 0xF6, 0x30, 0x41, 0x72, 0xFF, 0x59, 0x41,
|
||||
0x6D, 0xF6, 0x28, 0x44, 0xA1, 0xAD, 0xB3, 0xBA, 0xFF, 0xF4, 0xF6, 0x27, 0xFF, 0xF8, 0xFF, 0xFC,
|
||||
0xC5, 0x01, 0x82, 0x61, 0xC3, 0x69, 0x6F, 0x75, 0xFF, 0xE0, 0xFF, 0xF3, 0xF6, 0x1A, 0xFF, 0xEB,
|
||||
0xFF, 0xEF, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xA0, 0xFF, 0xA0, 0xFF, 0xA0, 0xFF, 0xA0,
|
||||
0xFF, 0xA0, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xFF, 0xDE, 0xFF, 0x66, 0xFF, 0x66,
|
||||
0xFF, 0x66, 0xFF, 0x66, 0xFF, 0x66, 0xFF, 0xF0, 0x42, 0x6E, 0x78, 0xFF, 0x8E, 0xFF, 0xEA, 0xA0,
|
||||
0x01, 0x82, 0x41, 0x72, 0xF5, 0x3F, 0x41, 0x72, 0xF5, 0x0B, 0x22, 0x61, 0x6F, 0xF8, 0xFC, 0x42,
|
||||
0x6E, 0x70, 0xF4, 0xEA, 0xF4, 0xEA, 0x21, 0x65, 0xF9, 0x41, 0x70, 0xF4, 0xE0, 0x22, 0x61, 0x6F,
|
||||
0xFC, 0xFC, 0x41, 0x61, 0xFA, 0xE0, 0x21, 0x75, 0xFC, 0x41, 0x6D, 0xFF, 0x00, 0x21, 0xA1, 0xFC,
|
||||
0x41, 0x65, 0xF4, 0xBD, 0x22, 0xC3, 0x69, 0xF9, 0xFC, 0x41, 0x69, 0xFB, 0x63, 0x21, 0x6C, 0xFC,
|
||||
0x42, 0x6D, 0x63, 0xF4, 0x9C, 0xF3, 0x1F, 0x22, 0x61, 0x69, 0xF6, 0xF9, 0x41, 0x61, 0xFE, 0xDD,
|
||||
0x21, 0x67, 0xFC, 0x41, 0x6C, 0xF4, 0x89, 0x42, 0x61, 0x69, 0xFB, 0x45, 0xF4, 0xEA, 0x43, 0x63,
|
||||
0x68, 0x6E, 0xF4, 0xEC, 0xFF, 0xD2, 0xF4, 0xFD, 0x21, 0x65, 0xF6, 0x24, 0x61, 0x65, 0x6C, 0x72,
|
||||
0xE5, 0xE8, 0xEC, 0xFD, 0x41, 0x63, 0xF4, 0x85, 0x21, 0x65, 0xFC, 0x41, 0xB3, 0xF4, 0x72, 0x21,
|
||||
0xC3, 0xFC, 0x41, 0x67, 0xF4, 0x5A, 0x21, 0x75, 0xFC, 0x22, 0x6D, 0x72, 0xF6, 0xFD, 0x41, 0x69,
|
||||
0xF4, 0x6E, 0x41, 0x62, 0xF2, 0xCD, 0x21, 0x69, 0xFC, 0x21, 0x76, 0xFD, 0x21, 0x6F, 0xFD, 0xCC,
|
||||
0x09, 0xA3, 0x62, 0x63, 0x64, 0x67, 0x6C, 0x6E, 0x70, 0x66, 0x72, 0x73, 0x74, 0x6D, 0xFF, 0x6B,
|
||||
0xFF, 0x77, 0xFF, 0x7E, 0xFF, 0x87, 0xFF, 0x95, 0xFF, 0xA8, 0xFF, 0xCC, 0xFF, 0xD9, 0xFF, 0xEA,
|
||||
0xFF, 0xEF, 0xFA, 0x67, 0xFF, 0xFD, 0xC1, 0x02, 0x91, 0x69, 0xF4, 0x16, 0x21, 0x63, 0xFA, 0x21,
|
||||
0x69, 0xFD, 0x41, 0x64, 0xF4, 0x78, 0x22, 0x75, 0x65, 0xFC, 0xAC, 0x41, 0x6F, 0xFA, 0x27, 0x42,
|
||||
0x63, 0x61, 0xFF, 0xFC, 0xF8, 0x70, 0x41, 0x76, 0xF2, 0x79, 0x21, 0xAD, 0xFC, 0x42, 0x69, 0xC3,
|
||||
0xF4, 0x24, 0xFF, 0xFD, 0x21, 0x75, 0xF9, 0xC2, 0x02, 0x91, 0x61, 0x68, 0xFF, 0x7D, 0xFA, 0x12,
|
||||
0xC6, 0x09, 0xA3, 0x66, 0x6C, 0x6E, 0x71, 0x78, 0x76, 0xFF, 0xCF, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF,
|
||||
0xF4, 0xFF, 0xF7, 0xFE, 0x17, 0x42, 0x6F, 0x61, 0xF2, 0x4A, 0xF2, 0x4A, 0x21, 0x75, 0xF9, 0x41,
|
||||
0x6C, 0xFF, 0x2D, 0x21, 0x61, 0xFC, 0x21, 0x75, 0xFD, 0xC3, 0x09, 0xA3, 0x63, 0x67, 0x6E, 0xFF,
|
||||
0xF3, 0xFF, 0xFD, 0xF3, 0xB3, 0x41, 0x73, 0xFB, 0x5F, 0x44, 0x74, 0x61, 0xC3, 0x65, 0xF3, 0xA3,
|
||||
0xF2, 0x26, 0xF4, 0x1B, 0xF2, 0x26, 0x43, 0x6F, 0x61, 0x6C, 0xF2, 0x19, 0xF2, 0x19, 0xFF, 0xF3,
|
||||
0x42, 0x63, 0x74, 0xF2, 0x0F, 0xF2, 0x0F, 0x21, 0x6E, 0xF9, 0x22, 0x75, 0x65, 0xEC, 0xFD, 0x41,
|
||||
0x73, 0xF2, 0x00, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41, 0x6F, 0xF8, 0x25, 0xA4, 0x09, 0xA3,
|
||||
0x62, 0x63, 0x66, 0x70, 0xC8, 0xED, 0xF9, 0xFC, 0x41, 0x7A, 0xF1, 0xE7, 0x21, 0x69, 0xFC, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x09, 0xA3, 0x74, 0xFD, 0x41, 0x6D, 0xF4, 0x2B, 0x21, 0x69,
|
||||
0xFC, 0xA1, 0x09, 0xD3, 0x6E, 0xFD, 0x41, 0x74, 0xF4, 0x1F, 0x21, 0x69, 0xFC, 0xA1, 0x09, 0xD3,
|
||||
0x64, 0xFD, 0x41, 0x69, 0xF4, 0x16, 0xA1, 0x09, 0xD3, 0x74, 0xFC, 0x45, 0xA1, 0xA9, 0xAD, 0xB3,
|
||||
0xBA, 0xFF, 0xE6, 0xFF, 0xF2, 0xFD, 0xC7, 0xFD, 0xC7, 0xFF, 0xFB, 0xA0, 0x0C, 0x13, 0x21, 0x67,
|
||||
0xFD, 0x22, 0x63, 0x74, 0xFA, 0xFA, 0x21, 0x70, 0xF5, 0x22, 0x70, 0x6D, 0xF8, 0xFD, 0xA2, 0x05,
|
||||
0x22, 0x6F, 0x75, 0xF0, 0xFB, 0xA0, 0x0A, 0x92, 0xA0, 0x0A, 0xB3, 0xA0, 0x0B, 0x13, 0x23, 0xA1,
|
||||
0xA9, 0xB3, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0x65, 0x6F, 0xC3, 0xF6, 0xF6, 0xF6, 0xF9, 0xA1, 0x0A,
|
||||
0xB3, 0x73, 0xF7, 0xA0, 0x0A, 0xE3, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0x28, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xCD, 0xD4, 0xD7, 0xED, 0xD7, 0xD7,
|
||||
0xD7, 0xF5, 0x21, 0x72, 0xEF, 0x21, 0x65, 0xFD, 0x48, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3,
|
||||
0x74, 0xFD, 0xE7, 0xFE, 0x87, 0xFE, 0xE8, 0xFF, 0x11, 0xFF, 0x55, 0xFF, 0x6D, 0xFF, 0x93, 0xFF,
|
||||
0xFD, 0x21, 0x6E, 0xE7, 0x58, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70,
|
||||
0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x68, 0x61, 0x65, 0x69, 0xF2, 0x79, 0xF3,
|
||||
0xD1, 0xF4, 0x53, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4, 0xC4, 0xF4,
|
||||
0x5A, 0xF7, 0x4E, 0xF4, 0x5A, 0xF9, 0xC2, 0xFB, 0x92, 0xFC, 0x33, 0xF4, 0x5A, 0xF4, 0x5A, 0xF4,
|
||||
0x5A, 0xF4, 0x5A, 0xF4, 0x5A, 0xFC, 0x53, 0xFC, 0xC1, 0xFD, 0xC4, 0xFF, 0xFD, 0x41, 0x63, 0xFC,
|
||||
0x16, 0xA0, 0x0D, 0x22, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xC3, 0x00, 0x71, 0x2E, 0x69, 0x65,
|
||||
0xF0, 0x3F, 0xFF, 0xF3, 0xFF, 0xFD, 0xC3, 0x00, 0x71, 0x2E, 0x7A, 0x73, 0xF0, 0x33, 0xF0, 0x39,
|
||||
0xF0, 0x39, 0xC1, 0x00, 0x71, 0x2E, 0xF0, 0x27, 0xD6, 0x00, 0x81, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79,
|
||||
0x7A, 0xF0, 0x21, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0,
|
||||
0x24, 0xF0, 0x24, 0xF3, 0xE6, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF3, 0xE6, 0xF0,
|
||||
0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0xF0, 0x24, 0x41, 0x74, 0xF0,
|
||||
0x69, 0x21, 0x70, 0xFC, 0xD7, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x65, 0xEF, 0xD5,
|
||||
0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01,
|
||||
0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01,
|
||||
0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xF0, 0x01, 0xFF, 0xFD, 0x42, 0x6F, 0x70, 0xF3,
|
||||
0xA8, 0xFF, 0xB1, 0x41, 0x6E, 0xFB, 0x50, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67,
|
||||
0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A,
|
||||
0x69, 0x6F, 0xEF, 0x82, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE,
|
||||
0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE,
|
||||
0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xEF, 0xAE, 0xFF, 0xF5,
|
||||
0xFF, 0xFC, 0x41, 0x74, 0xF1, 0xF3, 0x21, 0x70, 0xFC, 0x41, 0x6F, 0xFC, 0x90, 0x21, 0x6C, 0xFC,
|
||||
0x41, 0x74, 0xF0, 0x5B, 0x41, 0x6D, 0xFA, 0xEF, 0x41, 0x61, 0xEF, 0x9F, 0x21, 0x72, 0xFC, 0x21,
|
||||
0x74, 0xFD, 0x25, 0x6D, 0x75, 0x72, 0x73, 0x6E, 0xE4, 0xEB, 0xEE, 0xF2, 0xFD, 0xA0, 0x02, 0x32,
|
||||
0x21, 0x61, 0xFD, 0x41, 0x2E, 0xEF, 0x06, 0xA1, 0x02, 0x32, 0x73, 0xFC, 0x22, 0x6F, 0x61, 0xF1,
|
||||
0xFB, 0x41, 0x64, 0xEF, 0x88, 0x23, 0x63, 0x67, 0x72, 0xEB, 0xF7, 0xFC, 0x21, 0x6F, 0xE1, 0x41,
|
||||
0x75, 0xEF, 0x68, 0x21, 0x72, 0xFC, 0x42, 0x64, 0x73, 0xFF, 0xFD, 0xF2, 0xE9, 0x22, 0x6C, 0x61,
|
||||
0xEF, 0xF9, 0x41, 0x6C, 0xEF, 0x64, 0x21, 0x61, 0xFC, 0xA0, 0x07, 0x51, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0xA1, 0x04, 0x72, 0x72, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xFB, 0xEF,
|
||||
0xD7, 0xEF, 0xD7, 0xEF, 0xD7, 0xEF, 0xD7, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEF,
|
||||
0xC1, 0xEF, 0xC4, 0xEF, 0xC4, 0xEF, 0xC4, 0xEF, 0xC4, 0xEF, 0xC4, 0xFF, 0xF0, 0x21, 0x69, 0xEA,
|
||||
0x21, 0x74, 0xFD, 0x22, 0x66, 0x6E, 0xC3, 0xFD, 0x42, 0x68, 0x6F, 0xF2, 0x5F, 0xEF, 0xB4, 0x21,
|
||||
0x6E, 0xF9, 0xA0, 0x06, 0xF3, 0xA0, 0x07, 0x23, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD,
|
||||
0xFD, 0xFD, 0xFD, 0x48, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF3, 0x4F, 0xF3, 0x26,
|
||||
0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xEF, 0xFF, 0xF5, 0x21, 0x72, 0xE7, 0x21,
|
||||
0x65, 0xFD, 0x41, 0x6C, 0xFA, 0x21, 0x41, 0x6F, 0xF2, 0x6E, 0x24, 0x61, 0x62, 0x63, 0x74, 0xC5,
|
||||
0xF5, 0xF8, 0xFC, 0x41, 0x6F, 0xEF, 0x25, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD,
|
||||
0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xA9, 0xFD, 0xDC, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64,
|
||||
0x66, 0x67, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x68,
|
||||
0x6C, 0x72, 0x6F, 0x61, 0x75, 0x65, 0x69, 0xC3, 0xEE, 0x30, 0xEE, 0x33, 0xEE, 0x39, 0xEE, 0x33,
|
||||
0xEE, 0x42, 0xEE, 0x47, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x47, 0xFD, 0xF1, 0xEE, 0x4C, 0xEE, 0x33,
|
||||
0xEE, 0x33, 0xFD, 0xFD, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x33, 0xEE, 0x33, 0xFE, 0x09, 0xFE, 0x0F,
|
||||
0xFE, 0x5B, 0xFE, 0xAE, 0xFF, 0x19, 0xFF, 0x3C, 0xFF, 0x54, 0xFF, 0x9A, 0xFF, 0xE1, 0xFF, 0xFD,
|
||||
0x41, 0x74, 0xF2, 0xB2, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x04, 0xA2,
|
||||
0x6D, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xF1, 0x95, 0xF1, 0xD1, 0xF1, 0xD1,
|
||||
0xFF, 0xFB, 0xF1, 0xD1, 0xF1, 0xD1, 0xF1, 0xD7, 0x21, 0x61, 0xEA, 0xA0, 0x07, 0xC1, 0xA0, 0x07,
|
||||
0xD2, 0xA0, 0x07, 0xF2, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x27,
|
||||
0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5, 0x21, 0x6F,
|
||||
0xF1, 0x21, 0x74, 0xFD, 0x42, 0x61, 0x6F, 0xFF, 0xFD, 0xEE, 0xA8, 0x21, 0x6D, 0xF9, 0xA0, 0x05,
|
||||
0x92, 0x21, 0x65, 0xFD, 0x21, 0x64, 0xFD, 0xA0, 0x09, 0x32, 0x21, 0x61, 0xFD, 0x22, 0x72, 0x6C,
|
||||
0xF7, 0xFD, 0xA0, 0x02, 0x12, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x61,
|
||||
0xEF, 0x4A, 0x22, 0x68, 0x6C, 0xF9, 0xFC, 0x22, 0x61, 0x65, 0xE0, 0xE0, 0x21, 0x68, 0xFB, 0x21,
|
||||
0x74, 0xE3, 0x22, 0x63, 0x72, 0xFA, 0xFD, 0x23, 0xB3, 0xA1, 0xA9, 0xD6, 0xEB, 0xFB, 0x21, 0x6A,
|
||||
0xC0, 0x21, 0x6C, 0xBD, 0x21, 0x74, 0xBA, 0x21, 0x6E, 0xFD, 0x22, 0x6C, 0x65, 0xF7, 0xFD, 0xA0,
|
||||
0x02, 0x11, 0x21, 0x6A, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x69, 0xFF, 0xA6, 0x21, 0x6E, 0xFC, 0x21,
|
||||
0x61, 0xFD, 0x41, 0x6D, 0xFF, 0x9C, 0x21, 0x61, 0xFC, 0x46, 0x64, 0x65, 0x69, 0x74, 0x67, 0x6E,
|
||||
0xFF, 0x98, 0xFF, 0xD5, 0xFF, 0xE1, 0xFF, 0xEC, 0xFF, 0xF6, 0xFF, 0xFD, 0x42, 0x63, 0x7A, 0xFF,
|
||||
0x82, 0xFF, 0x82, 0x41, 0x6E, 0xFF, 0x7B, 0x21, 0x65, 0xFC, 0x22, 0x65, 0x69, 0xF2, 0xFD, 0x21,
|
||||
0x64, 0xFB, 0x41, 0x67, 0xFF, 0x6C, 0x21, 0x69, 0xFC, 0x41, 0x72, 0xFF, 0x65, 0x21, 0x74, 0xFC,
|
||||
0x23, 0x65, 0x6C, 0x73, 0xEF, 0xF6, 0xFD, 0xA0, 0x09, 0x12, 0x21, 0x73, 0xFD, 0x41, 0x70, 0xFF,
|
||||
0x51, 0x21, 0xBA, 0xFC, 0x23, 0x61, 0x75, 0xC3, 0xF6, 0xF9, 0xFD, 0x21, 0x6F, 0xDE, 0xC2, 0x09,
|
||||
0x12, 0x63, 0x64, 0xFF, 0x91, 0xFF, 0x91, 0x23, 0xA1, 0xA9, 0xB3, 0xE0, 0xE0, 0xE0, 0x45, 0x61,
|
||||
0xC3, 0x65, 0x6F, 0x6C, 0xFF, 0xF0, 0xFF, 0xF9, 0xFF, 0xD9, 0xFF, 0xD9, 0xFF, 0x81, 0x41, 0x69,
|
||||
0xFF, 0x84, 0x21, 0x72, 0xFC, 0x41, 0x65, 0xFF, 0x6A, 0x21, 0x63, 0xFC, 0x42, 0xA1, 0xA9, 0xFF,
|
||||
0x12, 0xFF, 0x12, 0x43, 0x61, 0xC3, 0x69, 0xFF, 0x0B, 0xFF, 0xF9, 0xFF, 0x0B, 0x41, 0xA9, 0xFF,
|
||||
0x01, 0x42, 0x65, 0xC3, 0xFE, 0xFD, 0xFF, 0xFC, 0x41, 0x67, 0xFF, 0x0A, 0x21, 0x65, 0xFC, 0x4B,
|
||||
0x72, 0x62, 0x63, 0x64, 0x6C, 0x70, 0x6E, 0x76, 0x78, 0x79, 0x73, 0xFF, 0x5A, 0xFF, 0x91, 0xFF,
|
||||
0xA5, 0xFF, 0xAC, 0xFF, 0xBF, 0xFF, 0xD3, 0xFF, 0xDA, 0xFF, 0xE4, 0xFF, 0x49, 0xFF, 0xF2, 0xFF,
|
||||
0xFD, 0xA0, 0x08, 0xC3, 0x21, 0x64, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xF7, 0x22, 0x72, 0x6F,
|
||||
0xFA, 0xFD, 0x22, 0x7A, 0x63, 0xEF, 0xEF, 0x21, 0x69, 0xFB, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x23, 0xA1, 0xA9, 0xB3, 0xDE, 0xDE, 0xDE, 0x22, 0x61, 0xC3, 0xD7, 0xF9, 0x23,
|
||||
0x61, 0x65, 0x6F, 0xD2, 0xD2, 0xD2, 0x21, 0xAD, 0xF9, 0x22, 0x69, 0xC3, 0xF1, 0xFD, 0xA0, 0x08,
|
||||
0xF2, 0x21, 0x73, 0xC0, 0x22, 0x61, 0x69, 0xFA, 0xFD, 0x21, 0x75, 0xFB, 0x21, 0xAD, 0xC6, 0x22,
|
||||
0x69, 0xC3, 0xC3, 0xFD, 0x42, 0x76, 0x6E, 0xFF, 0xAD, 0xFF, 0xFB, 0x42, 0x61, 0x69, 0xED, 0xDD,
|
||||
0xFF, 0xF9, 0x41, 0x6C, 0xFE, 0x80, 0x42, 0x72, 0x65, 0xFE, 0x7C, 0xFF, 0xFC, 0x21, 0x67, 0xF9,
|
||||
0x41, 0x76, 0xFF, 0x91, 0x21, 0x69, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x41, 0x6C, 0xFF, 0x7E, 0x21, 0x6C, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD,
|
||||
0x41, 0x72, 0xFE, 0x52, 0x43, 0x61, 0x65, 0x74, 0xF1, 0xB9, 0xF1, 0xB9, 0xFF, 0xFC, 0x41, 0x73,
|
||||
0xFF, 0xA0, 0x21, 0x65, 0xFC, 0x41, 0x6E, 0xFF, 0x5C, 0x21, 0x75, 0xFC, 0x21, 0xB3, 0xF9, 0x22,
|
||||
0xC3, 0x6F, 0xFD, 0xF6, 0x4D, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74,
|
||||
0x79, 0x7A, 0xFF, 0x59, 0xFF, 0x6C, 0xFF, 0x85, 0xFF, 0x95, 0xFE, 0x37, 0xFF, 0xA7, 0xFF, 0xB9,
|
||||
0xFF, 0xCC, 0xFF, 0xD9, 0xFF, 0xE0, 0xFF, 0xEE, 0xFF, 0xF5, 0xFF, 0xFB, 0x44, 0x61, 0xC3, 0x65,
|
||||
0x6F, 0xFF, 0x25, 0xFF, 0x47, 0xFF, 0x25, 0xFF, 0x25, 0x21, 0x6A, 0xF3, 0x41, 0x6A, 0xFF, 0x43,
|
||||
0x21, 0xA9, 0xFC, 0x41, 0xB1, 0xFD, 0xEF, 0x21, 0xC3, 0xFC, 0x21, 0xA9, 0xFD, 0x22, 0x65, 0xC3,
|
||||
0xFA, 0xFD, 0x23, 0x65, 0xC3, 0x70, 0xE7, 0xEE, 0xFB, 0x41, 0x6E, 0xFD, 0xD9, 0x21, 0xA9, 0xFC,
|
||||
0x22, 0x65, 0xC3, 0xF9, 0xFD, 0x21, 0x72, 0xFB, 0x21, 0x66, 0xFD, 0xC6, 0x02, 0x11, 0x6E, 0x72,
|
||||
0x62, 0x64, 0x6D, 0x73, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04, 0xFE, 0x04,
|
||||
0x46, 0x6E, 0x72, 0x62, 0x64, 0x6D, 0x73, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD, 0xEF, 0xFD,
|
||||
0xEF, 0xFD, 0xEF, 0x21, 0xA1, 0xED, 0xC6, 0x09, 0x12, 0x6E, 0x72, 0x62, 0x64, 0x6D, 0x73, 0xFE,
|
||||
0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0xFE, 0x31, 0x42, 0xA1, 0xB3, 0xFF, 0xEB,
|
||||
0xFE, 0x1C, 0x44, 0x61, 0xC3, 0x65, 0x6F, 0xFE, 0x15, 0xFE, 0x35, 0xFE, 0x15, 0xFE, 0x15, 0x44,
|
||||
0x6F, 0x61, 0xC3, 0x68, 0xFE, 0x08, 0xFF, 0xD7, 0xFF, 0xEC, 0xFF, 0xF3, 0x41, 0xA9, 0xFE, 0x85,
|
||||
0x42, 0x65, 0xC3, 0xFE, 0x81, 0xFF, 0xFC, 0xA1, 0x05, 0x92, 0x75, 0xF9, 0x41, 0x66, 0xFD, 0x42,
|
||||
0x42, 0x63, 0x71, 0xFD, 0x3E, 0xFD, 0x3E, 0x22, 0x69, 0x75, 0xF5, 0xF9, 0x41, 0x62, 0xFD, 0xCD,
|
||||
0x21, 0x6D, 0xFC, 0x21, 0x6F, 0xFD, 0x41, 0x7A, 0xFD, 0x28, 0x42, 0x63, 0x6E, 0xFD, 0x75, 0xFF,
|
||||
0xFC, 0x21, 0x61, 0xF9, 0x21, 0x72, 0xFD, 0x44, 0x61, 0x65, 0x69, 0x75, 0xFD, 0x17, 0xFF, 0xFD,
|
||||
0xFD, 0x9C, 0xFD, 0x7B, 0x41, 0x69, 0xFD, 0x4D, 0x41, 0x69, 0xFD, 0x8B, 0x43, 0x62, 0x63, 0x6C,
|
||||
0xFF, 0xF8, 0xFD, 0x5C, 0xFF, 0xFC, 0x41, 0x73, 0xFC, 0xF8, 0x41, 0x63, 0xFC, 0xF4, 0x22, 0x65,
|
||||
0x75, 0xF8, 0xFC, 0x43, 0x61, 0x69, 0x72, 0xFF, 0xE9, 0xFD, 0x4F, 0xFF, 0xFB, 0x23, 0x63, 0x70,
|
||||
0x74, 0xB6, 0xCA, 0xF6, 0x42, 0x63, 0x74, 0xFC, 0xF1, 0xFC, 0xEE, 0x4A, 0x6D, 0x6E, 0x6F, 0x61,
|
||||
0xC3, 0x63, 0x71, 0x64, 0x73, 0x72, 0xFF, 0x07, 0xFF, 0x1D, 0xFD, 0x24, 0xFF, 0x20, 0xFF, 0x48,
|
||||
0xFF, 0x74, 0xFF, 0x8C, 0xFF, 0x9C, 0xFF, 0xF2, 0xFF, 0xF9, 0x42, 0x72, 0x6F, 0xFD, 0x05, 0xFC,
|
||||
0xF7, 0x42, 0x61, 0x6F, 0xFC, 0xFE, 0xFC, 0xFE, 0x22, 0x65, 0x69, 0xF2, 0xF9, 0x41, 0x74, 0xFC,
|
||||
0xF2, 0x21, 0x72, 0xFC, 0x41, 0xA1, 0xFC, 0xDD, 0x42, 0x61, 0xC3, 0xFC, 0xD9, 0xFF, 0xFC, 0x42,
|
||||
0x6E, 0x75, 0xFC, 0xE0, 0xFF, 0xF9, 0x41, 0xB3, 0xFD, 0x0D, 0x42, 0x6F, 0xC3, 0xFD, 0x09, 0xFF,
|
||||
0xFC, 0x21, 0x69, 0xF9, 0x21, 0x73, 0xFD, 0x21, 0x75, 0xFD, 0x42, 0x67, 0x6E, 0xFF, 0x6E, 0xFC,
|
||||
0x74, 0x41, 0x65, 0xFF, 0x75, 0x42, 0x6F, 0x72, 0xFC, 0xEE, 0xFF, 0xFC, 0x22, 0x61, 0x70, 0xEE,
|
||||
0xF9, 0x41, 0x72, 0xFD, 0x0C, 0x41, 0x73, 0xFC, 0x9F, 0x21, 0x75, 0xFC, 0x44, 0x65, 0x6C, 0x6F,
|
||||
0x72, 0xFC, 0x9B, 0xFF, 0x4C, 0xFF, 0xF5, 0xFF, 0xFD, 0x42, 0x63, 0x74, 0xFC, 0xEE, 0xFC, 0xEE,
|
||||
0x21, 0x6E, 0xF9, 0x41, 0x63, 0xFC, 0x8C, 0x41, 0x72, 0xFC, 0x7D, 0xC1, 0x05, 0x92, 0x61, 0xFC,
|
||||
0x97, 0x41, 0x72, 0xFC, 0x91, 0x24, 0x65, 0x61, 0x6C, 0x6F, 0xEE, 0xF2, 0xF6, 0xFC, 0x41, 0x62,
|
||||
0xFC, 0x20, 0x21, 0x69, 0xFC, 0x41, 0x63, 0xFC, 0x5F, 0x41, 0x61, 0xFC, 0x58, 0x22, 0x65, 0x74,
|
||||
0xF8, 0xFC, 0x42, 0x67, 0x72, 0xFD, 0xCE, 0xFC, 0x20, 0x41, 0x78, 0xFC, 0x05, 0x23, 0x65, 0x6F,
|
||||
0x75, 0xF5, 0xFC, 0xE1, 0x41, 0x65, 0xFC, 0x95, 0x47, 0x63, 0x65, 0x66, 0x68, 0x73, 0x74, 0x76,
|
||||
0xFF, 0xA4, 0xFF, 0xB8, 0xFF, 0xCD, 0xFF, 0xDA, 0xFF, 0xE5, 0xFF, 0xF5, 0xFF, 0xFC, 0x41, 0x6E,
|
||||
0xFC, 0x31, 0x21, 0x65, 0xFC, 0x21, 0x74, 0xFD, 0x47, 0x64, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x73,
|
||||
0xFF, 0x30, 0xFF, 0x39, 0xFF, 0x47, 0xFF, 0x5F, 0xFF, 0x74, 0xFF, 0xE0, 0xFF, 0xFD, 0x43, 0x72,
|
||||
0x73, 0x6E, 0xFC, 0x69, 0xFC, 0x69, 0xFC, 0x69, 0x21, 0x61, 0xF6, 0x41, 0x6C, 0xFC, 0x04, 0x21,
|
||||
0x6C, 0xFC, 0x41, 0x61, 0xFD, 0xE7, 0x21, 0x74, 0xFC, 0xA0, 0x09, 0x53, 0x22, 0x63, 0x71, 0xFD,
|
||||
0xFD, 0x22, 0x73, 0x69, 0xF5, 0xFB, 0xC1, 0x05, 0x92, 0x61, 0xFB, 0x98, 0x43, 0x6E, 0x72, 0x73,
|
||||
0xFB, 0x92, 0xFF, 0xFA, 0xFB, 0x92, 0x43, 0x6E, 0x72, 0x73, 0xFB, 0x88, 0xFB, 0x88, 0xFB, 0x88,
|
||||
0x42, 0xA9, 0xB3, 0xFF, 0xF6, 0xFB, 0x7E, 0x45, 0x72, 0x64, 0x65, 0xC3, 0x6D, 0xFB, 0x77, 0xFB,
|
||||
0x77, 0xFF, 0xE5, 0xFF, 0xF9, 0xFB, 0x77, 0x42, 0x6E, 0x73, 0xFB, 0x67, 0xFB, 0x67, 0x42, 0xA1,
|
||||
0xAD, 0xFB, 0x60, 0xFF, 0xC8, 0x45, 0x69, 0x61, 0x65, 0x6F, 0xC3, 0xFF, 0xE2, 0xFF, 0xF2, 0xFB,
|
||||
0x59, 0xFB, 0x59, 0xFF, 0xF9, 0x41, 0xA1, 0xFB, 0x49, 0x42, 0x61, 0xC3, 0xFB, 0x45, 0xFF, 0xFC,
|
||||
0x21, 0xB1, 0xF9, 0x41, 0x62, 0xFB, 0x9C, 0x47, 0x64, 0x65, 0x62, 0x73, 0x6E, 0xC3, 0x72, 0xFF,
|
||||
0x81, 0xFF, 0x88, 0xFF, 0x9A, 0xFF, 0x8F, 0xFF, 0xDE, 0xFF, 0xF9, 0xFF, 0xFC, 0x46, 0xC3, 0x6F,
|
||||
0x61, 0x65, 0x69, 0x75, 0xFB, 0x5A, 0xFC, 0x32, 0xFD, 0x07, 0xFE, 0x4E, 0xFF, 0x4B, 0xFF, 0xEA,
|
||||
0x41, 0x69, 0xFB, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x45, 0x63, 0x6E, 0x72, 0x73, 0x69,
|
||||
0xFA, 0xCE, 0xF4, 0xA7, 0xFB, 0x01, 0xFF, 0xE3, 0xFF, 0xFD, 0xA0, 0x11, 0x72, 0x21, 0x63, 0xFD,
|
||||
0x21, 0xA9, 0xFD, 0x22, 0x65, 0xC3, 0xFA, 0xFD, 0x21, 0x6C, 0xFB, 0x21, 0x65, 0xFD, 0xD8, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x73,
|
||||
0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x72, 0x65, 0x69, 0xE8, 0x5B, 0xE8, 0x5E, 0xE8, 0x64, 0xE8,
|
||||
0x5E, 0xE8, 0x6D, 0xE8, 0x72, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x72, 0xE8,
|
||||
0x5E, 0xE8, 0x77, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x80, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8, 0x5E, 0xE8,
|
||||
0x5E, 0xE8, 0x5E, 0xE8, 0x8A, 0xFF, 0xDC, 0xFF, 0xFD, 0x42, 0x6D, 0x72, 0xF4, 0x38, 0xF3, 0xDE,
|
||||
0x41, 0x69, 0xF3, 0xD3, 0x43, 0x6C, 0x73, 0x74, 0xF9, 0xB2, 0xFF, 0xFC, 0xF9, 0xB2, 0x42, 0x6E,
|
||||
0x74, 0xF9, 0xA8, 0xF9, 0xA8, 0x41, 0x69, 0xEB, 0x9B, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0xDA, 0x00, 0x41, 0x2E, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x79, 0x7A, 0x6C, 0x72, 0x65, 0x69, 0x6F, 0x61, 0xE7, 0xDE, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1,
|
||||
0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1,
|
||||
0xE7, 0xE1, 0xE7, 0xE1, 0xF7, 0xB7, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1, 0xE7, 0xE1,
|
||||
0xE8, 0x0D, 0xE8, 0x0D, 0xFF, 0xCE, 0xFF, 0xD9, 0xFF, 0xE3, 0xFF, 0xFD, 0xD7, 0x00, 0x91, 0x2E,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74,
|
||||
0x76, 0x77, 0x78, 0x79, 0x7A, 0x75, 0xE7, 0x8D, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9,
|
||||
0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9,
|
||||
0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9, 0xE7, 0xB9,
|
||||
0xE7, 0xB9, 0xF7, 0x41, 0xA0, 0x08, 0xB1, 0x21, 0x2E, 0xFD, 0x49, 0x68, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xC3, 0x2E, 0x73, 0xE8, 0x4E, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51, 0xE8, 0x51,
|
||||
0xE8, 0x57, 0xFF, 0xFA, 0xFF, 0xFD, 0x22, 0x2E, 0x73, 0xDE, 0xE1, 0x21, 0x61, 0xFB, 0x21, 0xAD,
|
||||
0xFD, 0x23, 0x6F, 0x61, 0xC3, 0xD9, 0xF5, 0xFD, 0x21, 0x66, 0xF9, 0xD7, 0x00, 0x91, 0x2E, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x61, 0xE7, 0x0E, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7,
|
||||
0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7,
|
||||
0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7, 0x3A, 0xE7,
|
||||
0x3A, 0xFF, 0xFD, 0x41, 0x73, 0xFF, 0x84, 0x42, 0x2E, 0x65, 0xFF, 0x7D, 0xFF, 0xFC, 0x21, 0x6C,
|
||||
0xF9, 0x42, 0x6F, 0x61, 0xFF, 0x95, 0xFF, 0xFD, 0x21, 0x6E, 0xF9, 0x41, 0x72, 0xF9, 0x23, 0x42,
|
||||
0x65, 0x72, 0xFF, 0xFC, 0xE7, 0x37, 0x21, 0x74, 0xF9, 0x42, 0x6C, 0x73, 0xF8, 0x4D, 0xFF, 0xFD,
|
||||
0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE7, 0x64, 0xE7, 0x67, 0xE7, 0x67, 0xE7, 0x67,
|
||||
0xE7, 0x67, 0xE7, 0x67, 0xE7, 0x6D, 0x41, 0x6E, 0xF8, 0xFB, 0x21, 0x6F, 0xFC, 0x22, 0x6F, 0x72,
|
||||
0xE3, 0xFD, 0x41, 0x63, 0xE7, 0x04, 0x21, 0x65, 0xFC, 0x41, 0x61, 0xEA, 0x8B, 0x22, 0x6E, 0x67,
|
||||
0xF9, 0xFC, 0x41, 0x64, 0xF7, 0x46, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0xDB, 0x00, 0x41, 0x2E,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x6C, 0x72, 0x6F, 0x61, 0x65, 0x69, 0x75, 0xE6, 0x5D, 0xE6, 0x60, 0xE6, 0x60,
|
||||
0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xF6, 0x36,
|
||||
0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60, 0xE6, 0x60,
|
||||
0xE6, 0x60, 0xFE, 0xD0, 0xFF, 0x4F, 0xFF, 0xAC, 0xFF, 0xBD, 0xFF, 0xE1, 0xFF, 0xF1, 0xFF, 0xFD,
|
||||
0x41, 0x2E, 0xE6, 0x0C, 0x42, 0x2E, 0x73, 0xE6, 0x08, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9,
|
||||
0x21, 0x6C, 0xFB, 0x42, 0x6F, 0x61, 0xE6, 0xD5, 0xE6, 0xD5, 0x21, 0x6E, 0xF9, 0x21, 0x61, 0xFD,
|
||||
0x22, 0x65, 0x6D, 0xF0, 0xFD, 0x41, 0x65, 0xFE, 0x9F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x65, 0xFA, 0x22, 0x6C, 0x69, 0xFA, 0xFD, 0x42, 0x6C, 0x62, 0xF7, 0x7C, 0xFF,
|
||||
0xFB, 0x42, 0x63, 0x6F, 0xE6, 0x55, 0xE6, 0xEB, 0x21, 0x69, 0xF9, 0x41, 0x2E, 0xE8, 0xAA, 0x42,
|
||||
0x2E, 0x73, 0xE8, 0xA6, 0xFF, 0xFC, 0x21, 0x61, 0xF9, 0xA1, 0x04, 0xA2, 0x6C, 0xFD, 0x47, 0x68,
|
||||
0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE9, 0x79, 0xE9, 0xB5, 0xE9, 0xB5, 0xE9, 0xB5, 0xFF, 0xFB,
|
||||
0xE9, 0xB5, 0xE9, 0xBB, 0x43, 0x61, 0x69, 0x6F, 0xF5, 0xB9, 0xFF, 0xEA, 0xE9, 0xB0, 0x42, 0x61,
|
||||
0x74, 0xF5, 0xAF, 0xE6, 0xBD, 0x41, 0x72, 0xE6, 0x11, 0x21, 0x65, 0xFC, 0x46, 0x63, 0x6C, 0x6D,
|
||||
0x70, 0x74, 0x78, 0xF1, 0xA5, 0xFF, 0xBC, 0xFF, 0xE8, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0x0D, 0xA0,
|
||||
0x0A, 0x13, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0xA1, 0x06,
|
||||
0xF3, 0x63, 0xFD, 0x21, 0x69, 0xEC, 0x21, 0x6D, 0xFD, 0x21, 0xAD, 0xFD, 0x22, 0x69, 0xC3, 0xFA,
|
||||
0xFD, 0x21, 0x61, 0xDE, 0x21, 0x69, 0xFD, 0xA2, 0x06, 0xF3, 0x6E, 0x78, 0xF5, 0xFD, 0xA0, 0x0A,
|
||||
0x43, 0x21, 0x6F, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x07, 0x23, 0x6E, 0xFD, 0x45,
|
||||
0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xF6, 0xA6, 0xF6, 0xA6, 0xF6, 0xA6, 0xFF, 0xFB, 0xF6, 0xA6, 0x48,
|
||||
0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE9, 0xF3, 0xE9, 0xCA, 0xF6, 0x93, 0xF6, 0x93,
|
||||
0xFF, 0xBF, 0xFF, 0xD8, 0xF6, 0x93, 0xFF, 0xF0, 0x21, 0x72, 0xE7, 0x42, 0x65, 0x6F, 0xFF, 0xFD,
|
||||
0xE9, 0x19, 0x43, 0x64, 0x70, 0x73, 0xF0, 0xC5, 0xFF, 0xF9, 0xF1, 0x1F, 0x42, 0x6F, 0x65, 0xE9,
|
||||
0x08, 0xF0, 0xB7, 0x42, 0x6C, 0x6D, 0xF6, 0x93, 0xFF, 0xF9, 0x5B, 0x2E, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79,
|
||||
0x7A, 0x75, 0x61, 0x65, 0x69, 0x6F, 0xE4, 0xDF, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2,
|
||||
0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2,
|
||||
0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2, 0xE4, 0xE2,
|
||||
0xE4, 0xE2, 0xFE, 0xF6, 0xFF, 0x10, 0xFF, 0x62, 0xFF, 0xE8, 0xFF, 0xF9, 0xD6, 0x00, 0x41, 0x2E,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74,
|
||||
0x76, 0x77, 0x78, 0x79, 0x7A, 0xE4, 0x8D, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4,
|
||||
0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4,
|
||||
0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4, 0x90, 0xE4,
|
||||
0x90, 0x41, 0x6C, 0xF5, 0xF5, 0xD7, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x69, 0xE4,
|
||||
0x44, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4,
|
||||
0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4,
|
||||
0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x47, 0xE4, 0x73, 0xE4, 0x73, 0xFF, 0xFC, 0xD6, 0x00, 0x81,
|
||||
0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73,
|
||||
0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xE3, 0xFC, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF,
|
||||
0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF,
|
||||
0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF,
|
||||
0xE3, 0xFF, 0x41, 0x75, 0xF3, 0x6B, 0x41, 0x66, 0xEF, 0x7D, 0xA0, 0x0D, 0x02, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0xA1, 0xFD, 0x44, 0x6E, 0x70, 0x74, 0xC3, 0xFF, 0xED,
|
||||
0xF5, 0x4D, 0xF5, 0x4D, 0xFF, 0xFD, 0x41, 0x61, 0xFC, 0x4E, 0x21, 0xAD, 0xFC, 0x21, 0xC3, 0xFD,
|
||||
0x21, 0x67, 0xFD, 0xD9, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D,
|
||||
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x65, 0x69, 0x6F, 0xE3,
|
||||
0x86, 0xE3, 0x89, 0xE3, 0x8F, 0xE3, 0x89, 0xE3, 0x98, 0xE3, 0x9D, 0xE3, 0x89, 0xE3, 0x89, 0xE3,
|
||||
0x89, 0xE3, 0x9D, 0xE3, 0x89, 0xE3, 0xA2, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0xAB, 0xE3,
|
||||
0x89, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0x89, 0xE3, 0x89, 0xFF, 0x8A, 0xFF, 0xCF, 0xFF, 0xE6, 0xFF,
|
||||
0xFD, 0x42, 0x2E, 0x73, 0xE3, 0x38, 0xF4, 0x32, 0x21, 0x65, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x62,
|
||||
0xFD, 0x41, 0x2E, 0xE4, 0x07, 0x21, 0x65, 0xFC, 0x21, 0x74, 0xFD, 0x48, 0x6C, 0x68, 0x61, 0x65,
|
||||
0x69, 0x6F, 0x75, 0xC3, 0xE3, 0xAB, 0xE6, 0xEC, 0xE7, 0x28, 0xE7, 0x28, 0xE7, 0x28, 0xE7, 0x28,
|
||||
0xE7, 0x28, 0xE7, 0x2E, 0x21, 0x61, 0xE7, 0x41, 0x6E, 0xE3, 0x8F, 0x21, 0x61, 0xFC, 0x47, 0x6F,
|
||||
0x61, 0x6E, 0x67, 0x6C, 0x73, 0x74, 0xF3, 0xF5, 0xFF, 0xD0, 0xFF, 0xDA, 0xFF, 0xF6, 0xFF, 0xFD,
|
||||
0xF4, 0xA8, 0xFC, 0x8B, 0xA0, 0x05, 0x51, 0x21, 0x61, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD,
|
||||
0xA0, 0x02, 0xB2, 0xCC, 0x01, 0xA1, 0x68, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6D, 0x70, 0x71, 0x73,
|
||||
0x74, 0x76, 0xFF, 0xFD, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9,
|
||||
0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0xE4, 0xE9, 0x41, 0x69, 0xE6, 0xCA, 0x44, 0x6E,
|
||||
0x63, 0x6C, 0x78, 0xFF, 0xCF, 0xEE, 0x79, 0xFF, 0xD5, 0xFF, 0xFC, 0x41, 0x72, 0xE8, 0xA2, 0x21,
|
||||
0x61, 0xFC, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0xA1, 0x04,
|
||||
0xA2, 0x74, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE6, 0x54, 0xFF, 0xFB, 0xE6,
|
||||
0x90, 0xE6, 0x90, 0xE6, 0x90, 0xE6, 0x90, 0xE6, 0x96, 0x21, 0x69, 0xEA, 0x41, 0x69, 0xE3, 0x9F,
|
||||
0x44, 0x63, 0x6C, 0x6E, 0x72, 0xEE, 0x37, 0xFF, 0xD2, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74, 0xE6,
|
||||
0x62, 0x21, 0x6C, 0xFC, 0x43, 0x6E, 0x72, 0x74, 0xF4, 0x02, 0xFE, 0xA2, 0xF4, 0x02, 0xDB, 0x00,
|
||||
0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||
0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x65, 0x61, 0x69, 0x75, 0x6F, 0xE2, 0x4B, 0xE2, 0x4E,
|
||||
0xE2, 0x54, 0xE2, 0x4E, 0xE2, 0x5D, 0xE2, 0x62, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E,
|
||||
0xE2, 0x62, 0xF2, 0x24, 0xE2, 0x67, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x70, 0xE2, 0x4E,
|
||||
0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xE2, 0x4E, 0xFF, 0x50, 0xFF, 0xA0, 0xFF, 0xE2, 0xFF, 0xF3,
|
||||
0xFF, 0xF6, 0xA0, 0x0B, 0x95, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0xC3, 0x00,
|
||||
0x71, 0x7A, 0x73, 0x65, 0xE1, 0xF1, 0xE1, 0xF1, 0xFF, 0xFD, 0x41, 0x74, 0xED, 0xA2, 0x42, 0x2E,
|
||||
0x72, 0xE1, 0xDE, 0xFF, 0xFC, 0x43, 0x6D, 0x6E, 0x72, 0xF3, 0x81, 0xF3, 0x81, 0xF1, 0x88, 0x45,
|
||||
0x63, 0x66, 0x6F, 0x74, 0x75, 0xED, 0x98, 0xED, 0x98, 0xFB, 0x31, 0xF3, 0x77, 0xF2, 0xA5, 0xD9,
|
||||
0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71,
|
||||
0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6F, 0x61, 0x65, 0xE1, 0xBA, 0xE1, 0xBD, 0xE1,
|
||||
0xC3, 0xE1, 0xBD, 0xE1, 0xCC, 0xE1, 0xD1, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1,
|
||||
0xD1, 0xE1, 0xBD, 0xE1, 0xD6, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xFF, 0xCF, 0xE1, 0xBD, 0xE1,
|
||||
0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xE1, 0xBD, 0xFF, 0xDF, 0xFF, 0xE6, 0xFF, 0xF0, 0xC1, 0x0D, 0x22,
|
||||
0x6F, 0xE2, 0x8F, 0x42, 0x63, 0x71, 0xFF, 0xFA, 0xF1, 0x1E, 0xC2, 0x00, 0x71, 0x2E, 0x69, 0xE1,
|
||||
0x5F, 0xFF, 0xF9, 0xC2, 0x00, 0x71, 0x2E, 0x65, 0xE1, 0x56, 0xED, 0x24, 0x41, 0x74, 0xFE, 0xB9,
|
||||
0x21, 0x63, 0xFC, 0x21, 0x6E, 0xFD, 0x41, 0x72, 0xE5, 0x49, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0x61, 0x75, 0xE1, 0x3F, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1,
|
||||
0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1,
|
||||
0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1, 0x6B, 0xE1,
|
||||
0x6B, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x70, 0xE2, 0xB2, 0x42, 0x6D, 0x74, 0xFF, 0xFC, 0xEC, 0xBA,
|
||||
0xD7, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70,
|
||||
0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6F, 0xE0, 0xE9, 0xE1, 0x15, 0xE1, 0x15,
|
||||
0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15,
|
||||
0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15,
|
||||
0xE1, 0x15, 0xE1, 0x15, 0xE1, 0x15, 0xFF, 0xF9, 0x42, 0x61, 0x6F, 0xF1, 0x95, 0xF1, 0x95, 0x21,
|
||||
0x74, 0xF9, 0x41, 0x61, 0xF4, 0x4F, 0x21, 0x69, 0xFC, 0x21, 0x6D, 0xFD, 0xA0, 0x10, 0x92, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x10, 0xB2, 0x21, 0x72, 0xFD, 0x21, 0x64,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x23, 0x65, 0x61, 0x70, 0xE2, 0xEE, 0xFD, 0x44, 0x64,
|
||||
0x72, 0x6E, 0x74, 0xF1, 0x7E, 0xFF, 0xF9, 0xF1, 0x42, 0xF9, 0xFB, 0x41, 0x6E, 0xEB, 0x6F, 0x21,
|
||||
0x6F, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x65, 0xEC, 0x1B, 0xA0, 0x06, 0x31, 0x41,
|
||||
0xB1, 0xE1, 0xF2, 0x21, 0xC3, 0xFC, 0x22, 0x2E, 0x65, 0xF6, 0xFD, 0xA1, 0x04, 0xA2, 0x73, 0xFB,
|
||||
0x41, 0x61, 0xE6, 0x3D, 0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0xA1, 0x04, 0xA2, 0x6C, 0xFD, 0x41,
|
||||
0x6F, 0xE6, 0x2E, 0xA1, 0x04, 0xC2, 0x73, 0xFC, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xE4, 0x2E,
|
||||
0xE4, 0x2E, 0xFF, 0xFB, 0xE4, 0x2E, 0xE4, 0x2E, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3,
|
||||
0xE3, 0xDF, 0xE4, 0x1B, 0xE4, 0x1B, 0xFF, 0xD3, 0xE4, 0x1B, 0xFF, 0xE2, 0xFF, 0xF0, 0x21, 0x61,
|
||||
0xEA, 0x23, 0x6E, 0x6C, 0x72, 0xA4, 0xA7, 0xFD, 0x41, 0x7A, 0xEB, 0xBB, 0x43, 0x63, 0x65, 0x72,
|
||||
0xF1, 0x9A, 0xFF, 0xFC, 0xF1, 0x9A, 0x42, 0x71, 0x63, 0xE5, 0xE7, 0xFF, 0xAA, 0x41, 0x65, 0xFF,
|
||||
0xA3, 0x42, 0x64, 0x74, 0xFD, 0x3A, 0xFF, 0xFC, 0xA2, 0x04, 0xA2, 0x72, 0x6E, 0xEE, 0xF9, 0x41,
|
||||
0x65, 0xFD, 0x36, 0x21, 0x69, 0xFC, 0xA1, 0x04, 0xA2, 0x6D, 0xFD, 0xC1, 0x04, 0xA2, 0x72, 0xE1,
|
||||
0x66, 0x41, 0x71, 0xE5, 0xBC, 0xA1, 0x04, 0xC2, 0x72, 0xFC, 0x41, 0x65, 0xE5, 0xB3, 0x21, 0x74,
|
||||
0xFC, 0xA1, 0x04, 0xC2, 0x73, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xEF, 0xFF, 0xFB,
|
||||
0xE3, 0xB0, 0xE3, 0xB0, 0xE3, 0xB0, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE3, 0x61,
|
||||
0xFF, 0xC2, 0xE3, 0x9D, 0xE3, 0x9D, 0xFF, 0xD0, 0xFF, 0xD5, 0xFF, 0xF0, 0x21, 0x69, 0xEA, 0x41,
|
||||
0x6F, 0xEA, 0x8B, 0xA1, 0x04, 0x52, 0x72, 0xFC, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3,
|
||||
0xE0, 0x80, 0xE0, 0x83, 0xFF, 0xFB, 0xE0, 0x83, 0xE0, 0x83, 0xE0, 0x83, 0xE0, 0x89, 0x21, 0x61,
|
||||
0xEA, 0x21, 0x74, 0xFD, 0x41, 0x72, 0xFC, 0x7C, 0x21, 0x70, 0xFC, 0x41, 0x64, 0xFC, 0x75, 0x22,
|
||||
0x6D, 0x6E, 0xF9, 0xFC, 0xA0, 0x0E, 0x13, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x70, 0xFD,
|
||||
0xA0, 0x0E, 0x43, 0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xD8, 0x22,
|
||||
0x6C, 0x73, 0xFA, 0xFD, 0x41, 0x2E, 0xE1, 0x38, 0x42, 0x2E, 0x73, 0xE1, 0x34, 0xFF, 0xFC, 0x42,
|
||||
0x61, 0x73, 0xFF, 0xF9, 0xE1, 0xD0, 0x24, 0x69, 0x6F, 0x65, 0x74, 0xC9, 0xD7, 0xE9, 0xF9, 0x43,
|
||||
0x6C, 0x72, 0x73, 0xFF, 0x8D, 0xFF, 0xB2, 0xFF, 0xF7, 0xDB, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64,
|
||||
0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A,
|
||||
0x6C, 0x72, 0x75, 0x65, 0x61, 0x69, 0x6F, 0xDF, 0x00, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF,
|
||||
0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xEE, 0xD9, 0xDF, 0x03, 0xDF,
|
||||
0x03, 0xFD, 0xA1, 0xFD, 0xAA, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xDF, 0x03, 0xFD,
|
||||
0xC1, 0xFE, 0x17, 0xFE, 0x66, 0xFE, 0x95, 0xFF, 0x08, 0xFF, 0x13, 0xFF, 0xF6, 0x42, 0x6D, 0x72,
|
||||
0xDF, 0x3C, 0xEA, 0x76, 0x42, 0x65, 0x69, 0xFC, 0xC6, 0xFF, 0xF9, 0xD7, 0x00, 0x41, 0x2E, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0x75, 0xDE, 0x9E, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE,
|
||||
0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE,
|
||||
0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE, 0xA1, 0xDE,
|
||||
0xA1, 0xFF, 0xF9, 0xC2, 0x00, 0x71, 0x6E, 0x61, 0xDE, 0x5C, 0xEE, 0xD0, 0x41, 0xA1, 0xF0, 0xDB,
|
||||
0x43, 0x61, 0xC3, 0x65, 0xF0, 0xD7, 0xFF, 0xFC, 0xF0, 0xD7, 0x21, 0x69, 0xF6, 0x21, 0x63, 0xFD,
|
||||
0xA0, 0x0A, 0x72, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0xAD, 0xFD, 0x22,
|
||||
0x69, 0xC3, 0xEE, 0xFD, 0x21, 0x6E, 0xFB, 0x42, 0x65, 0x72, 0xE2, 0x3D, 0xE9, 0xEC, 0x22, 0x69,
|
||||
0x74, 0xF6, 0xF9, 0xA0, 0x0B, 0xB1, 0x23, 0xA1, 0xA9, 0xAD, 0xFD, 0xFD, 0xFD, 0x24, 0x61, 0xC3,
|
||||
0x65, 0x6F, 0xF6, 0xF9, 0xF6, 0xF6, 0x43, 0x64, 0x6E, 0x72, 0xF5, 0xFA, 0xED, 0xB7, 0xFF, 0xF7,
|
||||
0x41, 0x6D, 0xEF, 0xA6, 0xD9, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x72, 0x65, 0x61, 0x6F,
|
||||
0xDD, 0xF5, 0xDD, 0xF8, 0xDD, 0xFE, 0xDD, 0xF8, 0xDE, 0x07, 0xDE, 0x0C, 0xDD, 0xF8, 0xDD, 0xF8,
|
||||
0xDD, 0xF8, 0xDD, 0xF8, 0xFF, 0x9F, 0xDD, 0xF8, 0xDE, 0x11, 0xDD, 0xF8, 0xDD, 0xF8, 0xDE, 0x1A,
|
||||
0xDD, 0xF8, 0xDD, 0xF8, 0xDD, 0xF8, 0xDD, 0xF8, 0xDD, 0xF8, 0xDE, 0x24, 0xFF, 0xDA, 0xFF, 0xF2,
|
||||
0xFF, 0xFC, 0xC4, 0x00, 0x71, 0x74, 0x73, 0x6E, 0x61, 0xDD, 0xAD, 0xDD, 0xAD, 0xDD, 0xAD, 0xEE,
|
||||
0x21, 0xA0, 0x00, 0xD1, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0xA0, 0x03, 0x02, 0x21,
|
||||
0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x22, 0x2E, 0x65, 0xEC, 0xFD, 0x21, 0x6C, 0xFB, 0x22, 0x2E, 0x73,
|
||||
0xEF, 0xF2, 0x21, 0x6E, 0xED, 0x21, 0xB3, 0xFD, 0x21, 0x65, 0xEA, 0x21, 0x6E, 0xFD, 0x23, 0x61,
|
||||
0xC3, 0x6F, 0xEF, 0xF7, 0xFD, 0x21, 0x6C, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xC9, 0x23, 0x2E,
|
||||
0x61, 0x65, 0xC3, 0xC9, 0xFD, 0x21, 0x72, 0xF9, 0xC6, 0x00, 0x71, 0x7A, 0x73, 0x65, 0x61, 0x69,
|
||||
0x6F, 0xDD, 0x57, 0xDD, 0x57, 0xFF, 0xBF, 0xFF, 0xD2, 0xFF, 0xF0, 0xFF, 0xFD, 0x41, 0x74, 0xDF,
|
||||
0xF2, 0x21, 0x63, 0xFC, 0x41, 0x76, 0xDE, 0x67, 0x44, 0x6E, 0x2E, 0x73, 0x6C, 0xFF, 0xF9, 0xF5,
|
||||
0xEC, 0xF5, 0xEF, 0xFF, 0xFC, 0x41, 0x65, 0xFA, 0x22, 0x41, 0x76, 0xE8, 0xEA, 0xA0, 0x0E, 0xD2,
|
||||
0xA0, 0x0E, 0xF3, 0xA0, 0x0F, 0x23, 0x25, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0x27, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xEC, 0xEF, 0xEF, 0xEF, 0xEF, 0xEF, 0xF5,
|
||||
0x21, 0x6F, 0xF1, 0x21, 0x64, 0xFD, 0x44, 0x6C, 0x6D, 0x72, 0x75, 0xFF, 0xCF, 0xFA, 0x44, 0xFF,
|
||||
0xD3, 0xFF, 0xFD, 0xA0, 0x0F, 0x52, 0xA1, 0x0F, 0x52, 0x73, 0xFD, 0x21, 0x61, 0xFB, 0xA1, 0x04,
|
||||
0x52, 0x73, 0xFD, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xDD, 0xE5, 0xFF, 0xFB, 0xDD,
|
||||
0xE8, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xE8, 0xDD, 0xEE, 0x21, 0x65, 0xEA, 0x21, 0x72, 0xFD, 0x42,
|
||||
0x62, 0x63, 0xFF, 0xFD, 0xF4, 0xB1, 0xA0, 0x0F, 0xF3, 0xA1, 0x06, 0xF3, 0x72, 0xFD, 0x41, 0x72,
|
||||
0xF9, 0xC6, 0xA1, 0x06, 0xF3, 0x6F, 0xFC, 0xA0, 0x10, 0x23, 0x41, 0x2E, 0xF7, 0x64, 0x42, 0x2E,
|
||||
0x73, 0xF7, 0x60, 0xFF, 0xFC, 0x21, 0x74, 0xF9, 0x21, 0x69, 0xFD, 0xA2, 0x07, 0x23, 0x72, 0x76,
|
||||
0xEC, 0xFD, 0x45, 0xA1, 0xA9, 0xAD, 0xB3, 0xBA, 0xFF, 0xF9, 0xEE, 0x03, 0xEE, 0x03, 0xEE, 0x03,
|
||||
0xEE, 0x03, 0x48, 0x72, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xE1, 0x50, 0xE1, 0x27, 0xFF,
|
||||
0xC7, 0xED, 0xF0, 0xFF, 0xD0, 0xED, 0xF0, 0xED, 0xF0, 0xFF, 0xF0, 0x21, 0x72, 0xE7, 0xC7, 0x07,
|
||||
0xB1, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xDD, 0x6A, 0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x6D,
|
||||
0xDD, 0x6D, 0xDD, 0x6D, 0xDD, 0x73, 0x21, 0x61, 0xE8, 0x22, 0x65, 0x72, 0xE2, 0xFD, 0xA0, 0x11,
|
||||
0x43, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x70, 0x64, 0x72, 0xE0, 0xFD, 0xFD,
|
||||
0xDA, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70,
|
||||
0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0x65, 0x6F, 0x75, 0xDC, 0x19, 0xDC,
|
||||
0x1C, 0xDC, 0x22, 0xDC, 0x1C, 0xDC, 0x2B, 0xDC, 0x30, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC,
|
||||
0x1C, 0xDC, 0x30, 0xDC, 0x1C, 0xFE, 0x72, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xFE, 0xC8, 0xDC,
|
||||
0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xDC, 0x1C, 0xFE, 0xE8, 0xFF, 0x26, 0xFF, 0x5F, 0xFF,
|
||||
0xF9, 0x41, 0x65, 0xE1, 0x23, 0x41, 0x6E, 0xDF, 0x92, 0x21, 0x69, 0xFC, 0x22, 0x74, 0x64, 0xF5,
|
||||
0xFD, 0x41, 0x6C, 0xDF, 0x86, 0x21, 0x65, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x62, 0xDF, 0x7C, 0x21,
|
||||
0x6F, 0xFC, 0x41, 0x72, 0xDF, 0x75, 0x21, 0x61, 0xFC, 0x43, 0x63, 0x70, 0x74, 0xFF, 0xF6, 0xDF,
|
||||
0x6E, 0xFF, 0xFD, 0x41, 0xA1, 0xDF, 0x8F, 0x21, 0xC3, 0xFC, 0x21, 0x6C, 0xFD, 0x24, 0x6E, 0x62,
|
||||
0x6C, 0x74, 0xCF, 0xDB, 0xEC, 0xFD, 0x21, 0xA1, 0xBF, 0x21, 0xC3, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x41, 0x2E, 0xE4, 0xB3, 0x42, 0x2E, 0x73, 0xE4, 0xAF, 0xFF, 0xFC, 0x22, 0x6F, 0x61,
|
||||
0xF9, 0xF9, 0x21, 0x72, 0xFB, 0x23, 0x61, 0x6F, 0x65, 0xD8, 0xEA, 0xFD, 0x41, 0x73, 0xDE, 0x49,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x64, 0xE0, 0x00, 0x41, 0x6C, 0xDF,
|
||||
0xFC, 0x41, 0x69, 0xE9, 0x65, 0x42, 0x63, 0x74, 0xDF, 0xF7, 0xFF, 0xFC, 0xA4, 0x0E, 0xB2, 0x6D,
|
||||
0x6E, 0x74, 0x63, 0xEA, 0xED, 0xF1, 0xF9, 0x41, 0xBA, 0xE4, 0x87, 0x41, 0x75, 0xDF, 0xE5, 0xA2,
|
||||
0x0E, 0xB2, 0xC3, 0x78, 0xF8, 0xFC, 0x41, 0x6E, 0xDF, 0xDA, 0x21, 0x61, 0xFC, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0xB3, 0xF0, 0x22, 0x6F, 0xC3, 0xED, 0xFD, 0x21, 0x69,
|
||||
0xFB, 0x41, 0x2E, 0xDB, 0x9E, 0x42, 0x2E, 0x73, 0xDB, 0x9A, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9,
|
||||
0xF9, 0x41, 0xAD, 0xDF, 0xAF, 0x43, 0x69, 0xC3, 0x65, 0xDF, 0xAB, 0xFF, 0xFC, 0xDF, 0xAB, 0x41,
|
||||
0xA1, 0xDF, 0xA1, 0x43, 0x61, 0xC3, 0x6F, 0xDF, 0x9D, 0xFF, 0xFC, 0xDF, 0x9D, 0x41, 0x61, 0xE4,
|
||||
0x31, 0x21, 0x76, 0xFC, 0x41, 0x74, 0xDD, 0x80, 0x41, 0x69, 0xDF, 0x88, 0xA1, 0x0E, 0x92, 0x72,
|
||||
0xFC, 0x41, 0x76, 0xDF, 0x7F, 0x45, 0x61, 0xC3, 0x65, 0x6F, 0x69, 0xDF, 0x7B, 0xDF, 0xF0, 0xDF,
|
||||
0x7B, 0xFF, 0xF7, 0xFF, 0xFC, 0xC8, 0x0E, 0xB2, 0x62, 0x63, 0x64, 0x67, 0x6A, 0x6C, 0x73, 0x74,
|
||||
0xFF, 0x9E, 0xFF, 0xA9, 0xFF, 0xB7, 0xFF, 0xC0, 0xFF, 0xCE, 0xFF, 0xDC, 0xFF, 0xDF, 0xFF, 0xF0,
|
||||
0x41, 0x65, 0xDF, 0x49, 0x41, 0x69, 0xDD, 0x81, 0x21, 0x63, 0xFC, 0x21, 0x61, 0xFD, 0xA2, 0x0E,
|
||||
0xB2, 0x63, 0x72, 0xF2, 0xFD, 0xC3, 0x0E, 0xB2, 0x72, 0x62, 0x73, 0xDF, 0x34, 0xE1, 0xB9, 0xE0,
|
||||
0xFB, 0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xDF, 0x28, 0xFF, 0x3B, 0xFF, 0x4E, 0xFF,
|
||||
0xC4, 0xFF, 0xED, 0xFF, 0xF4, 0xE5, 0xE3, 0x21, 0x73, 0xEA, 0x42, 0x73, 0x6E, 0xFE, 0xFB, 0xFF,
|
||||
0xFD, 0x41, 0x70, 0xE6, 0x22, 0xD8, 0x00, 0x91, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0x6F,
|
||||
0xDA, 0x54, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80,
|
||||
0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80,
|
||||
0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xDA, 0x80, 0xFF, 0xF5, 0xFF, 0xFC,
|
||||
0x41, 0x68, 0xEC, 0xA2, 0x21, 0x63, 0xFC, 0xC2, 0x01, 0xF2, 0x2E, 0x73, 0xDA, 0x02, 0xFF, 0xFD,
|
||||
0xC1, 0x01, 0xF2, 0x2E, 0xD9, 0xF9, 0xA0, 0x01, 0xF2, 0x42, 0x61, 0x72, 0xF6, 0xB8, 0xDB, 0x22,
|
||||
0x41, 0x65, 0xDE, 0x04, 0x42, 0x61, 0x6D, 0xDE, 0x00, 0xE5, 0xAF, 0x44, 0x74, 0x6C, 0x63, 0x72,
|
||||
0xFF, 0xEE, 0xFF, 0xF5, 0xEA, 0x58, 0xFF, 0xF9, 0x41, 0x75, 0xEC, 0x56, 0x41, 0x6F, 0xEC, 0x52,
|
||||
0x22, 0x71, 0x63, 0xF8, 0xFC, 0x21, 0x6F, 0xFB, 0x41, 0x6C, 0xEA, 0x9C, 0x41, 0x70, 0xEB, 0x6A,
|
||||
0x41, 0x62, 0xE5, 0x83, 0x21, 0x72, 0xFC, 0x41, 0x63, 0xDA, 0x91, 0x21, 0x69, 0xFC, 0x21, 0x6E,
|
||||
0xFD, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD, 0xDC, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67,
|
||||
0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x74, 0x76, 0x77, 0x79, 0x72, 0x7A, 0x73, 0x6C, 0x78,
|
||||
0x65, 0x69, 0x61, 0x6F, 0x75, 0xC3, 0xD9, 0xA2, 0xD9, 0xA5, 0xD9, 0xAB, 0xD9, 0xA5, 0xD9, 0xB4,
|
||||
0xD9, 0xB9, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xB9, 0xD9, 0xA5, 0xD9, 0xBE, 0xD9, 0xA5,
|
||||
0xD9, 0xC7, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xFF, 0x4E, 0xFF, 0xA0, 0xFF, 0xA9, 0xFF, 0xAF,
|
||||
0xFF, 0xAF, 0xFF, 0xC4, 0xFF, 0xDE, 0xFF, 0xE1, 0xFF, 0xE5, 0xFF, 0xED, 0xFF, 0xFD, 0x42, 0x63,
|
||||
0x64, 0xFF, 0x62, 0xF8, 0xFA, 0xD7, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6D, 0x6E, 0x70, 0x71, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6C, 0x72, 0x69, 0xD9,
|
||||
0x44, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9,
|
||||
0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9,
|
||||
0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x47, 0xD9, 0x73, 0xD9, 0x73, 0xFF, 0xF9, 0x41, 0x73, 0xFE,
|
||||
0xF3, 0xD7, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x61, 0xD8, 0xF8, 0xD8, 0xFB, 0xD8,
|
||||
0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8,
|
||||
0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8,
|
||||
0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xD8, 0xFB, 0xFF, 0xFC, 0x42, 0x6E, 0x72, 0xEA, 0x5D, 0xEA, 0x5D,
|
||||
0xD8, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70,
|
||||
0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x65, 0x69, 0xD8, 0xA9, 0xD8, 0xAC, 0xD8,
|
||||
0xB2, 0xD8, 0xAC, 0xD8, 0xBB, 0xD8, 0xC0, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8,
|
||||
0xC0, 0xD8, 0xAC, 0xD8, 0xC5, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xCE, 0xD8, 0xAC, 0xD8,
|
||||
0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xD8, 0xAC, 0xFF, 0xF9, 0xF4, 0x61, 0xD6, 0x00, 0x41, 0x2E, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x79, 0x7A, 0xD8, 0x5E, 0xD8, 0x61, 0xD8, 0x67, 0xD8, 0x61, 0xD8, 0x70, 0xD8, 0x75,
|
||||
0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x75, 0xD8, 0x61, 0xD8, 0x7A, 0xD8, 0x61,
|
||||
0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x83, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61, 0xD8, 0x61,
|
||||
0x41, 0x6F, 0xF1, 0x80, 0xD7, 0x00, 0x41, 0x2E, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x6F, 0xD8, 0x15,
|
||||
0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18,
|
||||
0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18,
|
||||
0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xFF, 0xFC, 0xC1, 0x00, 0x41, 0x2E,
|
||||
0xD7, 0xCD, 0x41, 0x73, 0xE8, 0xC1, 0xA0, 0x02, 0x82, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA,
|
||||
0xFD, 0x43, 0x65, 0x6F, 0x61, 0xF4, 0x80, 0xF4, 0x80, 0xFF, 0xFB, 0x21, 0x6C, 0xF6, 0x21, 0x65,
|
||||
0xFD, 0x43, 0x65, 0x6F, 0x61, 0xF4, 0x70, 0xF4, 0x70, 0xF4, 0x70, 0x21, 0x6C, 0xF6, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x73, 0xFA, 0x21, 0x6F, 0xFD, 0xA0, 0x02, 0xD2, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73,
|
||||
0xFA, 0xFD, 0x22, 0x6F, 0x61, 0xFB, 0xFB, 0x21, 0x63, 0xFB, 0x21, 0x69, 0xFD, 0x25, 0x6D, 0x74,
|
||||
0x73, 0x6E, 0x72, 0xD1, 0xD1, 0xE1, 0xE7, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xE5, 0xE5, 0xE5, 0x21,
|
||||
0x6C, 0xF9, 0x21, 0x73, 0xFD, 0x25, 0x6D, 0x74, 0x73, 0x6E, 0x6F, 0xB9, 0xB9, 0xC9, 0xCF, 0xFD,
|
||||
0x46, 0x73, 0x69, 0x2E, 0x64, 0x6F, 0x72, 0xD7, 0x59, 0xFF, 0x92, 0xD7, 0x59, 0xFF, 0xDD, 0xFF,
|
||||
0xC1, 0xFF, 0xF5, 0x41, 0x73, 0xFF, 0x86, 0x21, 0x6F, 0xFC, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E,
|
||||
0xD7, 0x3F, 0xE8, 0x39, 0xFF, 0xFD, 0xFF, 0x78, 0xE8, 0x39, 0xA0, 0x02, 0xA3, 0x21, 0x2E, 0xFD,
|
||||
0x23, 0x2E, 0x73, 0x69, 0xFA, 0xFD, 0xE3, 0x42, 0x6F, 0x61, 0xF3, 0xEA, 0xF3, 0xEA, 0x21, 0x63,
|
||||
0xF9, 0x43, 0x65, 0x61, 0x69, 0xFF, 0xEF, 0xF3, 0xE0, 0xFF, 0xFD, 0x41, 0x6F, 0xF3, 0xD6, 0x22,
|
||||
0x74, 0x6D, 0xF2, 0xFC, 0x41, 0x73, 0xFF, 0x76, 0x21, 0x65, 0xFC, 0x21, 0x6F, 0xF9, 0x41, 0x65,
|
||||
0xFF, 0x6F, 0x21, 0x6C, 0xFC, 0x47, 0x61, 0x2E, 0x73, 0x74, 0x6D, 0x64, 0x62, 0xFF, 0xB5, 0xD6,
|
||||
0xF4, 0xFF, 0xEA, 0xFF, 0xF3, 0xFF, 0xF6, 0xFF, 0x6D, 0xFF, 0xFD, 0x21, 0x6D, 0xE0, 0x42, 0x2E,
|
||||
0x65, 0xD6, 0xDB, 0xFF, 0xFD, 0x21, 0x61, 0xF6, 0xA0, 0x02, 0xA2, 0x42, 0x2E, 0x73, 0xFF, 0xFD,
|
||||
0xFF, 0xA2, 0x42, 0x2E, 0x73, 0xFF, 0x98, 0xFF, 0x9B, 0x23, 0x65, 0x6F, 0x61, 0xF2, 0xF2, 0xF9,
|
||||
0x21, 0x6C, 0xF9, 0x21, 0x65, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xEC, 0xEC, 0xEC, 0x21, 0x6C, 0xF9,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x73, 0xFA, 0x21, 0x6F, 0xFD, 0x47, 0x61, 0x65, 0x6D, 0x74, 0x73, 0x6E,
|
||||
0x6F, 0xFF, 0xC2, 0xFF, 0xC2, 0xFF, 0xEA, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xFD, 0xFF, 0x08, 0x44,
|
||||
0x6D, 0x74, 0x73, 0x6E, 0xFE, 0xDF, 0xFE, 0xDF, 0xFE, 0xEF, 0xFE, 0xF5, 0x41, 0x6F, 0xFE, 0xB6,
|
||||
0x43, 0x6F, 0x61, 0x65, 0xF3, 0x41, 0xF3, 0x41, 0xF3, 0x41, 0x42, 0x2E, 0x6C, 0xD6, 0x6F, 0xFF,
|
||||
0xF6, 0x21, 0x65, 0xF9, 0x41, 0x65, 0xE7, 0x5F, 0x44, 0x2E, 0x6D, 0x6C, 0x6E, 0xD6, 0x61, 0xFF,
|
||||
0xFC, 0xFF, 0xE8, 0xFF, 0xE4, 0x21, 0x65, 0xF3, 0x46, 0x6C, 0x6E, 0x6F, 0x6D, 0x74, 0x73, 0xFE,
|
||||
0xA9, 0xFF, 0xD4, 0xFE, 0x8A, 0xFF, 0xE9, 0xFF, 0xFD, 0xFF, 0xFD, 0x21, 0x6F, 0xED, 0x21, 0x64,
|
||||
0xFD, 0x47, 0x73, 0x69, 0x62, 0x72, 0x64, 0x6F, 0x6E, 0xFF, 0x5D, 0xFE, 0x71, 0xFF, 0x64, 0xFF,
|
||||
0x98, 0xFF, 0xAE, 0xFE, 0xA0, 0xFF, 0xFD, 0x41, 0x67, 0xFE, 0x9B, 0x21, 0x6F, 0xFC, 0x41, 0x63,
|
||||
0xD6, 0x1E, 0x21, 0x69, 0xFC, 0x41, 0x65, 0xFF, 0x06, 0x21, 0x74, 0xFC, 0x45, 0x2E, 0x6C, 0x74,
|
||||
0x6E, 0x73, 0xD6, 0x0D, 0xFF, 0xEF, 0xFF, 0xF6, 0xE7, 0x07, 0xFF, 0xFD, 0x45, 0xB1, 0xA9, 0xAD,
|
||||
0xA1, 0xB3, 0xFE, 0x30, 0xFE, 0xA4, 0xFF, 0x09, 0xFF, 0xC5, 0xFF, 0xF0, 0xA0, 0x01, 0x72, 0xA0,
|
||||
0x01, 0x92, 0x21, 0xB3, 0xFD, 0x22, 0x75, 0xC3, 0xF7, 0xFD, 0x21, 0x65, 0xF2, 0xA0, 0x02, 0x62,
|
||||
0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x65, 0xF5, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73, 0x6D, 0xE6, 0xE9, 0xFD, 0x22, 0x6F,
|
||||
0x61, 0xE5, 0xF9, 0x21, 0x63, 0xFB, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0xB3, 0xFD, 0x21,
|
||||
0x61, 0xD4, 0x21, 0xAD, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x67, 0xFD, 0x41, 0x67, 0xE1, 0x68, 0x23,
|
||||
0xC3, 0x6F, 0x69, 0xED, 0xF9, 0xFC, 0xA0, 0x00, 0xC2, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA,
|
||||
0xFD, 0x21, 0x65, 0xF8, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73,
|
||||
0x6D, 0xE9, 0xEC, 0xFD, 0x44, 0x2E, 0x6F, 0x61, 0x74, 0xD5, 0x78, 0xFF, 0xE8, 0xFF, 0xF9, 0xF5,
|
||||
0x24, 0x42, 0x6F, 0x61, 0xD9, 0x83, 0xD9, 0x83, 0x21, 0x74, 0xF9, 0x41, 0x6E, 0xF2, 0xAF, 0x43,
|
||||
0x63, 0x74, 0x65, 0xE7, 0x07, 0xE7, 0x07, 0xFD, 0x93, 0x41, 0x74, 0xE6, 0xFD, 0x41, 0x69, 0xE5,
|
||||
0x70, 0x41, 0x61, 0xD6, 0xF0, 0x21, 0xAD, 0xFC, 0x21, 0xC3, 0xFD, 0xA1, 0x04, 0xA2, 0x70, 0xFD,
|
||||
0x47, 0x68, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xC3, 0xD9, 0x07, 0xD9, 0x43, 0xFF, 0xFB, 0xD9, 0x43,
|
||||
0xD9, 0x43, 0xD9, 0x43, 0xD9, 0x49, 0x21, 0x6F, 0xEA, 0x22, 0x6E, 0x74, 0xD4, 0xFD, 0xA0, 0x00,
|
||||
0x91, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x0F, 0x72, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73,
|
||||
0xFA, 0xFD, 0x22, 0x6F, 0x61, 0xFB, 0xFB, 0xA0, 0x03, 0x32, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73,
|
||||
0xFA, 0xFD, 0xA0, 0x10, 0xF3, 0x21, 0x2E, 0xFD, 0x23, 0x61, 0x2E, 0x73, 0xF5, 0xFA, 0xFD, 0x21,
|
||||
0x73, 0xEB, 0x22, 0x2E, 0x65, 0xE5, 0xFD, 0x21, 0x6C, 0xFB, 0x22, 0x65, 0x61, 0xEE, 0xFD, 0x22,
|
||||
0x63, 0x64, 0xD3, 0xFB, 0x4D, 0x65, 0x61, 0x2E, 0x78, 0x6C, 0x73, 0x63, 0x6D, 0x6E, 0x70, 0x72,
|
||||
0x6F, 0x69, 0xFE, 0xF1, 0xFE, 0xF6, 0xD4, 0xD5, 0xFF, 0x04, 0xFF, 0x3B, 0xFF, 0x60, 0xFF, 0x74,
|
||||
0xFF, 0x77, 0xFF, 0x7B, 0xFF, 0x85, 0xFF, 0xB5, 0xFF, 0xC0, 0xFF, 0xFB, 0x42, 0x65, 0xC3, 0xFE,
|
||||
0xC0, 0xFE, 0xC6, 0xA0, 0x03, 0x23, 0x21, 0x2E, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x67, 0xFD, 0x43, 0x2E, 0x73, 0x69, 0xD4, 0x97, 0xE5, 0x91, 0xFC, 0xD0, 0x21, 0x65, 0xF6, 0x41,
|
||||
0x73, 0xFE, 0xB1, 0x44, 0x2E, 0x73, 0x69, 0x6E, 0xFE, 0xAA, 0xFE, 0xAD, 0xFF, 0xFC, 0xFE, 0xAD,
|
||||
0x43, 0x2E, 0x74, 0x65, 0xD4, 0x79, 0xFF, 0xEC, 0xFF, 0xF3, 0xA0, 0x0C, 0xF1, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0xA0, 0x11, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x23, 0x6F, 0x69, 0x65, 0xC7, 0xEB, 0xFD, 0x42,
|
||||
0x6F, 0x72, 0xD4, 0x4A, 0xE0, 0x14, 0x45, 0x2E, 0x64, 0x66, 0x67, 0x74, 0xD4, 0x43, 0xFF, 0xF9,
|
||||
0xF1, 0x94, 0xE5, 0xEC, 0xFA, 0x5A, 0x41, 0x65, 0xFC, 0x6C, 0x42, 0x6E, 0x73, 0xFE, 0x56, 0xFE,
|
||||
0x56, 0x41, 0x6F, 0xFF, 0x9E, 0x41, 0x73, 0xFE, 0x48, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xFE,
|
||||
0x44, 0xFE, 0x47, 0xFF, 0xF8, 0xFF, 0xFC, 0xFE, 0x47, 0x42, 0x61, 0x73, 0xFF, 0xF0, 0xFE, 0x37,
|
||||
0x43, 0x2E, 0x73, 0x69, 0xFE, 0x2D, 0xFE, 0x30, 0xFF, 0x7F, 0x43, 0x73, 0x2E, 0x6E, 0xFE, 0x26,
|
||||
0xFE, 0x23, 0xFE, 0x26, 0x23, 0xAD, 0xA9, 0xA1, 0xE5, 0xEC, 0xF6, 0x45, 0x6D, 0x2E, 0x73, 0x69,
|
||||
0x6E, 0xFF, 0xC6, 0xFE, 0x12, 0xFE, 0x15, 0xFF, 0x64, 0xFE, 0x15, 0xA0, 0x03, 0x22, 0x21, 0x2E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x41, 0x65, 0xFF, 0x32, 0x42, 0x2E, 0x73, 0xFF, 0x2B, 0xFF, 0x2E, 0x23,
|
||||
0x65, 0x61, 0x6F, 0xF9, 0xF9, 0xF9, 0x41, 0x73, 0xFF, 0x20, 0x21, 0x6F, 0xFC, 0x41, 0x68, 0xD7,
|
||||
0xC2, 0xC2, 0x00, 0xD1, 0x2E, 0x73, 0xFD, 0xDC, 0xFD, 0xDF, 0x22, 0x6F, 0x61, 0xF7, 0xF7, 0x4C,
|
||||
0x6F, 0xC3, 0x65, 0x61, 0x2E, 0x6D, 0x74, 0x6C, 0x73, 0x6E, 0x63, 0x69, 0xFF, 0x7B, 0xFF, 0xB5,
|
||||
0xFF, 0xBC, 0xFF, 0x24, 0xD3, 0xAA, 0xFF, 0xD2, 0xFF, 0xD5, 0xFF, 0xE0, 0xFF, 0xD5, 0xFF, 0xEB,
|
||||
0xFF, 0xEE, 0xFF, 0xFB, 0x41, 0x61, 0xFE, 0xFF, 0x43, 0x65, 0x6F, 0x61, 0xF0, 0x49, 0xF0, 0x49,
|
||||
0xFB, 0xBA, 0x43, 0x2E, 0x61, 0x65, 0xFD, 0x9B, 0xFD, 0xA1, 0xFE, 0xED, 0x43, 0x2E, 0x73, 0x72,
|
||||
0xFD, 0x91, 0xFD, 0x94, 0xFF, 0xF6, 0x48, 0x2E, 0x6D, 0x74, 0x6C, 0x6E, 0x6F, 0x61, 0x65, 0xD3,
|
||||
0x63, 0xFC, 0xFE, 0xFC, 0xFE, 0xFF, 0xE2, 0xFC, 0xE6, 0xFF, 0xF6, 0xFD, 0x8D, 0xE3, 0xDD, 0xA0,
|
||||
0x05, 0x41, 0x21, 0x65, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xFD, 0x65, 0x21,
|
||||
0xB3, 0xFC, 0x41, 0x65, 0xFE, 0xAD, 0x21, 0x6E, 0xFC, 0x22, 0xC3, 0x6F, 0xF6, 0xFD, 0x43, 0x74,
|
||||
0x61, 0x69, 0xE4, 0xD8, 0xFF, 0xEA, 0xFF, 0xFB, 0x41, 0x72, 0xE4, 0xCE, 0x41, 0x64, 0xFE, 0xBA,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63, 0xFD, 0x42,
|
||||
0x72, 0x69, 0xE4, 0xB7, 0xFF, 0xFD, 0x41, 0x69, 0xE2, 0xB7, 0x41, 0x74, 0xED, 0x7B, 0x43, 0x64,
|
||||
0x73, 0x74, 0xEA, 0xF2, 0xFF, 0xFC, 0xE4, 0xA8, 0x41, 0x73, 0xEC, 0xE8, 0x42, 0x2E, 0x65, 0xD2,
|
||||
0xF0, 0xFF, 0xFC, 0xA0, 0x08, 0xF1, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x6F,
|
||||
0xFB, 0x21, 0x73, 0xFD, 0x21, 0xAD, 0xFD, 0x53, 0x61, 0x69, 0x73, 0x2E, 0x6D, 0x6E, 0x74, 0x72,
|
||||
0x62, 0x64, 0x6F, 0x63, 0x65, 0x66, 0x67, 0x70, 0x75, 0x6C, 0xC3, 0xFE, 0x25, 0xFE, 0x38, 0xFE,
|
||||
0x59, 0xD2, 0xD2, 0xFE, 0x81, 0xFE, 0x8F, 0xFE, 0x9F, 0xFF, 0x28, 0xFF, 0x4D, 0xFF, 0x6F, 0xFB,
|
||||
0x0B, 0xFF, 0xA7, 0xFF, 0xB1, 0xFF, 0xC8, 0xFF, 0xB1, 0xFF, 0xCF, 0xFF, 0xD7, 0xFF, 0xE5, 0xFF,
|
||||
0xFD, 0xA0, 0x01, 0xB2, 0x21, 0xA1, 0xFD, 0x43, 0xC3, 0x65, 0x73, 0xFF, 0xFD, 0xD2, 0xF0, 0xE3,
|
||||
0x8C, 0x41, 0x65, 0xEB, 0xDA, 0x21, 0x6C, 0xFC, 0x41, 0x65, 0xE4, 0xF6, 0x21, 0x72, 0xFC, 0x21,
|
||||
0x65, 0xFD, 0x43, 0x2E, 0x63, 0x74, 0xD2, 0x77, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x61, 0xD2, 0x6D,
|
||||
0x21, 0x63, 0xFC, 0x21, 0x6F, 0xFD, 0x41, 0x6F, 0xD5, 0x4C, 0x43, 0x6F, 0x62, 0x69, 0xFD, 0xD5,
|
||||
0xFF, 0xF9, 0xFF, 0xFC, 0xA0, 0x01, 0xB1, 0x21, 0x72, 0xFD, 0x21, 0x62, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x43, 0x65, 0x6F, 0x72, 0xEC, 0xC5, 0xD6, 0x64, 0xDE, 0x0C, 0x45, 0x2E, 0x68, 0x64, 0x65, 0x74,
|
||||
0xD2, 0x3F, 0xFF, 0xF3, 0xE3, 0xEC, 0xEB, 0xCF, 0xFF, 0xF6, 0xA0, 0x04, 0x13, 0x21, 0x2E, 0xFD,
|
||||
0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6D, 0xFD, 0x42, 0x2E, 0x65, 0xFC, 0x44, 0xFF, 0xFD,
|
||||
0xA0, 0x03, 0xC2, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xED, 0x22, 0x61, 0x65, 0xEA,
|
||||
0xEA, 0x46, 0x73, 0x2E, 0x6E, 0x69, 0x62, 0x72, 0xFF, 0xE8, 0xFC, 0x2C, 0xFC, 0x2F, 0xFF, 0xF5,
|
||||
0xFF, 0xF8, 0xFF, 0xFB, 0x45, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xFC, 0x19, 0xFC, 0x1C, 0xFD, 0xCD,
|
||||
0xFD, 0x6B, 0xFC, 0x1C, 0x42, 0x73, 0x61, 0xFC, 0x0C, 0xFF, 0xF0, 0x43, 0xA9, 0xA1, 0xAD, 0xFD,
|
||||
0xD5, 0xFF, 0xD6, 0xFF, 0xF9, 0xA0, 0x02, 0xF3, 0x21, 0x2E, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x6D, 0xFD, 0x43, 0x65, 0x61, 0x6F, 0xEE, 0x8D, 0xEE, 0x8D, 0xEE, 0x8D, 0x43, 0x2E,
|
||||
0x73, 0x69, 0xFF, 0xA2, 0xFF, 0xA5, 0xFF, 0xA8, 0x21, 0x65, 0xF6, 0xA0, 0x03, 0xE3, 0x21, 0x2E,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0x24, 0x2E, 0x73, 0x69, 0x6E, 0xF7, 0xFA, 0xFD, 0xFA, 0x43, 0x2E, 0x74,
|
||||
0x65, 0xFF, 0x83, 0xFF, 0xEB, 0xFF, 0xF7, 0x21, 0x6F, 0xEA, 0x41, 0x65, 0xFF, 0x7C, 0x21, 0x6E,
|
||||
0xE0, 0x21, 0x73, 0xDA, 0x25, 0x2E, 0x73, 0x6D, 0x69, 0x6E, 0xD7, 0xDA, 0xF3, 0xFD, 0xDA, 0x22,
|
||||
0x61, 0x73, 0xF5, 0xCF, 0x23, 0x2E, 0x73, 0x69, 0xC7, 0xCA, 0xCD, 0x23, 0x73, 0x2E, 0x6E, 0xC3,
|
||||
0xC0, 0xC3, 0x23, 0xAD, 0xA9, 0xA1, 0xED, 0xF2, 0xF9, 0x45, 0x6D, 0x2E, 0x73, 0x69, 0x6E, 0xFF,
|
||||
0xCE, 0xFF, 0xB2, 0xFF, 0xB5, 0xFF, 0xB8, 0xFF, 0xB5, 0x44, 0x6F, 0xC3, 0x65, 0x61, 0xFF, 0xC5,
|
||||
0xFF, 0xE9, 0xFF, 0xF0, 0xFF, 0xAB, 0xA0, 0x10, 0x54, 0x21, 0x2E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x23, 0x2E, 0x73, 0x6D, 0xEE, 0xF1, 0xFD, 0x21,
|
||||
0x65, 0xF9, 0x42, 0x61, 0x6C, 0xFF, 0x82, 0xFF, 0xFD, 0x42, 0x2E, 0x73, 0xFF, 0x72, 0xFF, 0x75,
|
||||
0x43, 0x2E, 0x61, 0x65, 0xFF, 0x6B, 0xFF, 0xF9, 0xFF, 0x71, 0x43, 0x2E, 0x73, 0x72, 0xFF, 0x61,
|
||||
0xFF, 0x64, 0xFF, 0xF6, 0x43, 0x2E, 0x6F, 0x61, 0xFE, 0xEC, 0xFF, 0xF6, 0xFF, 0xE5, 0x47, 0x73,
|
||||
0x6D, 0x6E, 0x74, 0x72, 0x62, 0x64, 0xFF, 0x5F, 0xFF, 0x69, 0xFE, 0xE5, 0xFF, 0x6C, 0xFF, 0xAB,
|
||||
0xFF, 0xD4, 0xFF, 0xF6, 0x42, 0x2E, 0x65, 0xFB, 0x09, 0xFC, 0x5B, 0x21, 0x64, 0xF9, 0x21, 0x61,
|
||||
0xFD, 0x21, 0x64, 0xFD, 0x45, 0x2E, 0x65, 0x61, 0x6D, 0x69, 0xFA, 0xF9, 0xFC, 0x4B, 0xFA, 0xFF,
|
||||
0xFB, 0x10, 0xFF, 0xFD, 0x21, 0x72, 0xF0, 0x21, 0x6F, 0xFD, 0x4B, 0xC3, 0x65, 0x2E, 0x6D, 0x74,
|
||||
0x6C, 0x73, 0x6E, 0x6F, 0x61, 0x69, 0xFE, 0xE1, 0xFE, 0xF7, 0xD0, 0xBF, 0xFA, 0x5A, 0xFA, 0x5A,
|
||||
0xFE, 0xFA, 0xFA, 0x5A, 0xFA, 0x42, 0xFC, 0x35, 0xFF, 0xC4, 0xFF, 0xFD, 0x46, 0x2E, 0x6D, 0x74,
|
||||
0x6C, 0x6E, 0x72, 0xD0, 0x9D, 0xFA, 0x38, 0xFA, 0x38, 0xFD, 0x1C, 0xFA, 0x20, 0xFA, 0xCC, 0x41,
|
||||
0x61, 0xE1, 0x84, 0x21, 0x6C, 0xFC, 0x21, 0x64, 0xFD, 0x41, 0x6F, 0xFB, 0x7E, 0x21, 0x74, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x2E, 0xE3, 0x46, 0x42, 0x2E, 0x73,
|
||||
0xE3, 0x42, 0xFF, 0xFC, 0x22, 0x6F, 0x61, 0xF9, 0xF9, 0x21, 0x69, 0xFB, 0x23, 0x64, 0x6D, 0x63,
|
||||
0xD7, 0xEA, 0xFD, 0xA0, 0x00, 0x81, 0x21, 0x6F, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0xA1, 0xFD, 0x42, 0x6F, 0x72, 0xD4, 0x62, 0xDC, 0x11, 0xA0, 0x11, 0x92, 0x21, 0x6C, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61,
|
||||
0xFD, 0x44, 0x61, 0x6F, 0x74, 0x75, 0xE0, 0xA2, 0xE9, 0x8F, 0xFF, 0xE1, 0xFF, 0xFD, 0x41, 0x6E,
|
||||
0xE1, 0xC8, 0x42, 0x63, 0x72, 0xE1, 0xC4, 0xE1, 0xC4, 0x42, 0x6D, 0x72, 0xD9, 0x69, 0xD4, 0xC3,
|
||||
0x41, 0x67, 0xD9, 0xBC, 0x42, 0x61, 0x6F, 0xD0, 0x9B, 0xD0, 0x9B, 0x43, 0x61, 0x65, 0x6F, 0xD0,
|
||||
0x94, 0xD0, 0x94, 0xD0, 0x94, 0x21, 0x69, 0xF6, 0x44, 0x61, 0x69, 0x65, 0x6F, 0xD0, 0x87, 0xD0,
|
||||
0x87, 0xD0, 0x87, 0xD0, 0x87, 0x44, 0x6A, 0x67, 0x6C, 0x6D, 0xFF, 0xDF, 0xD9, 0x97, 0xFF, 0xF0,
|
||||
0xFF, 0xF3, 0x44, 0xA1, 0xA9, 0xB3, 0xAD, 0xFF, 0xC7, 0xFF, 0xCE, 0xD8, 0x5C, 0xFF, 0xF3, 0x41,
|
||||
0x72, 0xDC, 0x2A, 0x21, 0x65, 0xFC, 0x41, 0x6D, 0xE2, 0x99, 0x21, 0x75, 0xFC, 0x41, 0x69, 0xD1,
|
||||
0xE0, 0x44, 0x63, 0x67, 0x6C, 0x6D, 0xFF, 0xF2, 0xD9, 0x83, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74,
|
||||
0xD2, 0x2C, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x41, 0x69, 0xD7, 0xE1, 0x21, 0x75, 0xFC, 0x43,
|
||||
0x63, 0x67, 0x71, 0xD1, 0xB0, 0xFF, 0xF6, 0xFF, 0xFD, 0x43, 0x61, 0xC3, 0x6F, 0xD1, 0xA3, 0xD9,
|
||||
0x2F, 0xD1, 0xA3, 0x41, 0xAD, 0xD1, 0x99, 0x43, 0x65, 0x69, 0xC3, 0xD1, 0x95, 0xD1, 0x95, 0xFF,
|
||||
0xFC, 0x42, 0x69, 0x61, 0xD7, 0x0B, 0xD1, 0x8E, 0x44, 0xA1, 0xAD, 0xA9, 0xB3, 0xD1, 0x84, 0xD1,
|
||||
0x84, 0xD1, 0x84, 0xD1, 0x84, 0x45, 0x61, 0xC3, 0x69, 0x65, 0x6F, 0xD1, 0x77, 0xFF, 0xF3, 0xD1,
|
||||
0x77, 0xD1, 0x77, 0xD1, 0x77, 0x41, 0x6F, 0xD1, 0xE6, 0x25, 0x6A, 0x67, 0x6C, 0x6D, 0x74, 0xC0,
|
||||
0xCE, 0xD8, 0xEC, 0xFC, 0x41, 0xB3, 0xD1, 0x58, 0x21, 0xC3, 0xFC, 0x21, 0x69, 0xFD, 0x41, 0x72,
|
||||
0xFF, 0x7F, 0x41, 0xA9, 0xD1, 0x4D, 0x43, 0x63, 0x71, 0x73, 0xD1, 0x46, 0xD1, 0x46, 0xD6, 0x27,
|
||||
0x22, 0xC3, 0x69, 0xF2, 0xF6, 0x41, 0x6D, 0xD1, 0xA5, 0x21, 0xA1, 0xFC, 0x22, 0x61, 0xC3, 0xF9,
|
||||
0xFD, 0x41, 0x71, 0xD1, 0x2B, 0x21, 0x73, 0xFC, 0x41, 0x61, 0xD1, 0x35, 0x21, 0x6C, 0xFC, 0x47,
|
||||
0x62, 0x6E, 0x63, 0x74, 0x67, 0x65, 0x70, 0xFF, 0xCC, 0xD8, 0xD5, 0xFF, 0xCF, 0xFF, 0xE1, 0xFF,
|
||||
0xED, 0xFF, 0xF6, 0xFF, 0xFD, 0x43, 0x72, 0x74, 0x63, 0xD1, 0x07, 0xD1, 0x07, 0xD1, 0x07, 0x21,
|
||||
0x61, 0xF6, 0x42, 0x62, 0x64, 0xD8, 0xB2, 0xFF, 0xFD, 0x41, 0x72, 0xD0, 0x12, 0xA0, 0x0D, 0xA1,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x48, 0xC3, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0x74, 0x70, 0xFE, 0xF9, 0xFF, 0x18, 0xFF, 0x36, 0xFF, 0x80, 0xFF, 0xC6, 0xFF, 0xE9, 0xFF, 0xF0,
|
||||
0xFF, 0xFD, 0x41, 0x72, 0xFA, 0x54, 0x21, 0x74, 0xFC, 0x21, 0x63, 0xFD, 0x21, 0xA9, 0xFD, 0x22,
|
||||
0x65, 0xC3, 0xFA, 0xFD, 0x4F, 0x6F, 0x73, 0x2E, 0x6D, 0x6E, 0x72, 0x64, 0x65, 0x61, 0xC3, 0x63,
|
||||
0x74, 0x75, 0x78, 0x6C, 0xFC, 0x13, 0xFC, 0x2E, 0xCE, 0xA5, 0xFC, 0x46, 0xFC, 0x66, 0xFD, 0xE6,
|
||||
0xFE, 0x08, 0xFE, 0x22, 0xFE, 0x48, 0xFE, 0x5B, 0xFE, 0x7D, 0xFE, 0x8A, 0xFE, 0x8E, 0xFF, 0xD5,
|
||||
0xFF, 0xFB, 0x43, 0x2E, 0x73, 0x6D, 0xF8, 0x9B, 0xF8, 0x9E, 0xFA, 0x4F, 0xA0, 0x03, 0x53, 0x21,
|
||||
0x2E, 0xFD, 0x22, 0x2E, 0x73, 0xFA, 0xFD, 0xA0, 0x03, 0x84, 0x21, 0x2E, 0xFD, 0x22, 0x2E, 0x73,
|
||||
0xFA, 0xFD, 0x23, 0x65, 0x6F, 0x61, 0xF0, 0xF0, 0xFB, 0x22, 0x2E, 0x6C, 0xE3, 0xF9, 0x21, 0x65,
|
||||
0xFB, 0x23, 0x65, 0x61, 0x6F, 0xE1, 0xE1, 0xE1, 0x23, 0x65, 0x6F, 0x61, 0xDA, 0xDA, 0xDA, 0x21,
|
||||
0x6C, 0xF9, 0x24, 0x6D, 0x74, 0x6C, 0x65, 0xEC, 0xEC, 0xEF, 0xFD, 0x22, 0x2E, 0x6C, 0xC1, 0xED,
|
||||
0x21, 0x73, 0xFB, 0x21, 0x6F, 0xFD, 0x23, 0x73, 0x6E, 0x6F, 0xEC, 0xFD, 0xFA, 0x21, 0x6F, 0xF9,
|
||||
0x43, 0x73, 0x69, 0x6D, 0xF8, 0x40, 0xF9, 0x8F, 0xFF, 0xFD, 0x21, 0xA1, 0xF6, 0x43, 0x6F, 0x61,
|
||||
0xC3, 0xF8, 0x33, 0xFF, 0x95, 0xFF, 0xFD, 0x41, 0x69, 0xF9, 0x78, 0x21, 0x74, 0xFC, 0x41, 0x73,
|
||||
0xF8, 0xEC, 0x42, 0x2E, 0x65, 0xF8, 0xE5, 0xFF, 0xFC, 0x21, 0x6C, 0xF9, 0x43, 0x69, 0x61, 0x65,
|
||||
0xFF, 0xEF, 0xFF, 0xFD, 0xF8, 0x1C, 0x41, 0x61, 0xEA, 0xAB, 0x42, 0x6F, 0x69, 0xCE, 0x5D, 0xCE,
|
||||
0xBE, 0x21, 0x6D, 0xF9, 0x41, 0x69, 0xCE, 0xB4, 0x21, 0x6D, 0xFC, 0x21, 0xA1, 0xFD, 0x22, 0x61,
|
||||
0xC3, 0xF3, 0xFD, 0x44, 0x6D, 0x74, 0x6C, 0x6F, 0xF6, 0xB8, 0xFF, 0xE3, 0xFF, 0xFB, 0xE7, 0x2D,
|
||||
0xC3, 0x02, 0x91, 0x61, 0xC3, 0x65, 0xCF, 0xCC, 0xD7, 0x58, 0xCF, 0xCC, 0x21, 0x69, 0xF4, 0x21,
|
||||
0x63, 0xFD, 0xC1, 0x05, 0x81, 0x61, 0xCE, 0x3D, 0x21, 0x69, 0xFA, 0x21, 0x63, 0xFD, 0x21, 0xAD,
|
||||
0xFD, 0xA0, 0x02, 0xB1, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x23, 0x62, 0x65,
|
||||
0xC3, 0xF4, 0xFA, 0xFD, 0x21, 0x62, 0xED, 0x21, 0x6C, 0xEA, 0x21, 0x6D, 0xE7, 0x21, 0x69, 0xE7,
|
||||
0x21, 0x70, 0xFD, 0x21, 0x73, 0xFD, 0x25, 0xAD, 0xA1, 0xA9, 0xBA, 0xB3, 0xEE, 0xF1, 0xE1, 0xF4,
|
||||
0xFD, 0x22, 0x74, 0x69, 0xD0, 0xD0, 0x21, 0x6E, 0xCE, 0x21, 0x65, 0xFD, 0x22, 0x73, 0x72, 0xF5,
|
||||
0xFD, 0x25, 0x69, 0xC3, 0x61, 0x65, 0x75, 0xCC, 0xE5, 0xD6, 0xFB, 0xD9, 0x41, 0x75, 0xEA, 0x4B,
|
||||
0xA0, 0x0B, 0xE3, 0x22, 0x75, 0x74, 0xFD, 0xFD, 0x22, 0x73, 0x64, 0xFB, 0xF8, 0xA0, 0x0C, 0x63,
|
||||
0x21, 0x72, 0xFD, 0xA0, 0x0C, 0x93, 0x21, 0x2E, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xEF, 0xF7, 0xFD,
|
||||
0x41, 0x73, 0xEA, 0x44, 0x21, 0xA9, 0xFC, 0xA0, 0x0A, 0x12, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x73, 0xFD, 0xA0, 0x0C, 0x42, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x67, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x24, 0x69, 0xC3, 0x65, 0x72, 0xD7, 0xE2, 0xEE, 0xFD, 0x21, 0x72, 0xF7, 0x42, 0x65,
|
||||
0x72, 0xFF, 0xFD, 0xCE, 0x2D, 0x41, 0x72, 0xFC, 0xB4, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21,
|
||||
0x75, 0xFD, 0x41, 0x72, 0xCD, 0xC6, 0x21, 0x65, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x4A, 0x69, 0xC3, 0x68, 0x66, 0x6D, 0x74, 0x6F, 0x61, 0x64, 0x67, 0xFF, 0x2D, 0xFF,
|
||||
0x3C, 0xFF, 0x7F, 0xFD, 0xF7, 0xFF, 0x8A, 0xFF, 0xDC, 0xE9, 0x9F, 0xE9, 0x9F, 0xFF, 0xED, 0xFF,
|
||||
0xFD, 0x41, 0x65, 0xD8, 0x86, 0x43, 0x6E, 0x2E, 0x73, 0xD8, 0x7E, 0xF7, 0x21, 0xF7, 0x24, 0x42,
|
||||
0x6F, 0x61, 0xFF, 0xF6, 0xF7, 0x1D, 0x42, 0x2E, 0x73, 0xF8, 0xC5, 0xF8, 0xC8, 0x22, 0x6F, 0x61,
|
||||
0xF9, 0xF9, 0x41, 0x2E, 0xEE, 0x81, 0x21, 0x73, 0xFC, 0x21, 0x65, 0xFD, 0x44, 0x6E, 0x72, 0x2E,
|
||||
0x73, 0xFF, 0xF1, 0xFF, 0xFD, 0xF7, 0x72, 0xF7, 0x75, 0x41, 0x61, 0xDE, 0x29, 0x41, 0x72, 0xF8,
|
||||
0xA1, 0x42, 0x2E, 0x73, 0xF7, 0x5D, 0xF7, 0x60, 0x4A, 0x67, 0x64, 0x73, 0x6E, 0x62, 0x63, 0x61,
|
||||
0x74, 0x65, 0x6F, 0xFE, 0x65, 0xFE, 0x84, 0xFE, 0xAB, 0xFF, 0x9A, 0xFF, 0xB9, 0xFF, 0xC7, 0xFF,
|
||||
0xE4, 0xFF, 0xF1, 0xFF, 0xF5, 0xFF, 0xF9, 0x41, 0x69, 0xFB, 0xFC, 0x21, 0x72, 0xFC, 0x21, 0x65,
|
||||
0xFD, 0x41, 0x74, 0xFD, 0x68, 0xA0, 0x11, 0xB2, 0x42, 0x64, 0x74, 0xFF, 0xFD, 0xFC, 0x01, 0x21,
|
||||
0x69, 0xF9, 0x21, 0x73, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x76, 0xFD, 0x21, 0x69,
|
||||
0xFD, 0x23, 0x74, 0x6C, 0x6E, 0xDD, 0xE0, 0xFD, 0x5C, 0x62, 0x2E, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||
0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xC3,
|
||||
0x6F, 0x61, 0x65, 0x69, 0x75, 0xCD, 0x5C, 0xDB, 0x8C, 0xDD, 0xF1, 0xE3, 0xC6, 0xE4, 0x43, 0xE5,
|
||||
0xC4, 0xE7, 0x42, 0xE7, 0x94, 0xE7, 0xDD, 0xE8, 0x9B, 0xE9, 0xD6, 0xEA, 0x67, 0xED, 0x21, 0xED,
|
||||
0x83, 0xEE, 0x2C, 0xF0, 0x08, 0xF2, 0x7F, 0xF2, 0xDD, 0xF3, 0x29, 0xF3, 0x78, 0xF3, 0xC3, 0xF4,
|
||||
0x0C, 0xF6, 0x24, 0xF7, 0x4C, 0xF9, 0x4F, 0xFD, 0x7C, 0xFF, 0xB0, 0xFF, 0xF9,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns es_patterns = {
|
||||
0x34F8u,
|
||||
es_trie_data,
|
||||
sizeof(es_trie_data),
|
||||
};
|
||||
@@ -1,453 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t fr_trie_data[] = {
|
||||
0x02, 0x0C, 0x18, 0x22, 0x16, 0x21, 0x0B, 0x16, 0x21, 0x0E, 0x01, 0x0C, 0x0B, 0x3D, 0x0C, 0x2B,
|
||||
0x0E, 0x0C, 0x0C, 0x33, 0x0C, 0x33, 0x16, 0x34, 0x2A, 0x0D, 0x20, 0x0D, 0x0C, 0x0D, 0x2A, 0x17,
|
||||
0x04, 0x1F, 0x0C, 0x29, 0x0C, 0x20, 0x0B, 0x0C, 0x17, 0x17, 0x0C, 0x3F, 0x35, 0x53, 0x4A, 0x36,
|
||||
0x34, 0x21, 0x2A, 0x0D, 0x0C, 0x2A, 0x0D, 0x16, 0x02, 0x17, 0x15, 0x15, 0x0C, 0x15, 0x16, 0x2C,
|
||||
0x47, 0x0C, 0x49, 0x2B, 0x0C, 0x0D, 0x34, 0x0D, 0x2A, 0x0B, 0x16, 0x2B, 0x0C, 0x17, 0x2A, 0x0B,
|
||||
0x0C, 0x03, 0x0C, 0x16, 0x0D, 0x01, 0x16, 0x0C, 0x0B, 0x0C, 0x3E, 0x48, 0x2C, 0x0B, 0x29, 0x16,
|
||||
0x37, 0x40, 0x1F, 0x16, 0x20, 0x17, 0x36, 0x0D, 0x52, 0x3D, 0x16, 0x1F, 0x0C, 0x16, 0x3E, 0x0D,
|
||||
0x49, 0x0C, 0x03, 0x16, 0x35, 0x0C, 0x22, 0x0F, 0x02, 0x0D, 0x51, 0x0C, 0x21, 0x0C, 0x20, 0x0B,
|
||||
0x16, 0x21, 0x0C, 0x17, 0x21, 0x0C, 0x0D, 0xA0, 0x00, 0x91, 0x21, 0x61, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0xA0, 0x00, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0xA0, 0x01, 0x12, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0,
|
||||
0x01, 0x52, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x68,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x72, 0xA0, 0x01, 0xB1, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x6E, 0xFD, 0xA1, 0x01, 0x72, 0x6E, 0xFD, 0xA0, 0x01, 0x92, 0x21, 0xA9, 0xFD, 0x24, 0x61,
|
||||
0x65, 0xC3, 0x73, 0xE9, 0xF5, 0xFD, 0xE9, 0x21, 0x69, 0xF7, 0x23, 0x61, 0x65, 0x74, 0xC2, 0xDA,
|
||||
0xFD, 0xA0, 0x01, 0xC2, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6F, 0xFD,
|
||||
0xA0, 0x01, 0xE1, 0x21, 0x61, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x2E, 0xFF, 0x5E, 0x21, 0x74, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x67, 0x70, 0xFD, 0xFD, 0xA0, 0x05, 0x72, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xC9, 0x00, 0x61, 0x62, 0x65, 0x6C, 0x6D, 0x6E, 0x70,
|
||||
0x73, 0x72, 0x67, 0xFF, 0x4C, 0xFF, 0x58, 0xFF, 0x67, 0xFF, 0x79, 0xFF, 0xC3, 0xFF, 0xD6, 0xFF,
|
||||
0xDF, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x00, 0x71, 0x27, 0xA2, 0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB,
|
||||
0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x52, 0x22, 0x61, 0x6F, 0xFD, 0xFD, 0xA0,
|
||||
0x02, 0x93, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0x61, 0x6E, 0x75, 0xF2, 0xFD, 0x21,
|
||||
0xA9, 0xAC, 0x42, 0xC3, 0x69, 0xFF, 0xFD, 0xFF, 0xA9, 0x21, 0x6E, 0xF9, 0x41, 0x74, 0xFF, 0x06,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x01, 0xE2, 0x21,
|
||||
0x74, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x72, 0xFF, 0x6B, 0x21, 0x75, 0xFC, 0x21, 0x67, 0xFD, 0xA2,
|
||||
0x02, 0x52, 0x6E, 0x75, 0xF3, 0xFD, 0x41, 0x62, 0xFF, 0x5A, 0x21, 0x61, 0xFC, 0x21, 0x66, 0xFD,
|
||||
0x41, 0x74, 0xFF, 0x50, 0x41, 0x72, 0xFF, 0x4F, 0x21, 0x6F, 0xFC, 0xC4, 0x02, 0x52, 0x66, 0x70,
|
||||
0x72, 0x78, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0x45, 0xFF, 0xFD, 0xA0, 0x06, 0x82, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x74, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x72, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0xA2, 0x06, 0x62, 0x6C, 0x6E, 0xF4, 0xFD, 0x21, 0xA9, 0xF9, 0x41, 0x69, 0xFF, 0xA0,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0xC3, 0x02, 0x52, 0x6D, 0x71, 0x74, 0xFF, 0xFD, 0xFF, 0x96,
|
||||
0xFF, 0x96, 0x41, 0x6C, 0xFF, 0x8A, 0x21, 0x75, 0xFC, 0x41, 0x64, 0xFE, 0xF7, 0xA2, 0x02, 0x52,
|
||||
0x63, 0x6E, 0xF9, 0xFC, 0x41, 0x62, 0xFF, 0x43, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x05,
|
||||
0xF1, 0xA0, 0x06, 0xC1, 0x21, 0xA9, 0xFD, 0xA7, 0x06, 0xA2, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75,
|
||||
0x73, 0xF7, 0xF7, 0xFD, 0xF7, 0xF7, 0xF7, 0xF7, 0x21, 0x72, 0xEF, 0x21, 0x65, 0xFD, 0xC2, 0x02,
|
||||
0x52, 0x69, 0x6C, 0xFF, 0x72, 0xFF, 0x4E, 0x49, 0x66, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x74,
|
||||
0x75, 0xFF, 0x42, 0xFF, 0x58, 0xFF, 0x74, 0xFF, 0xA2, 0xFF, 0xAF, 0xFF, 0xC6, 0xFF, 0xD4, 0xFF,
|
||||
0xF4, 0xFF, 0xF7, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFF, 0x16, 0xFF, 0xE4, 0x41, 0x75, 0xFE, 0xA7,
|
||||
0x21, 0x67, 0xFC, 0x41, 0x65, 0xFF, 0x09, 0x21, 0x74, 0xFC, 0xA0, 0x02, 0x71, 0x21, 0x75, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x02, 0x72, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21,
|
||||
0x69, 0xFD, 0xA4, 0x00, 0x61, 0x6E, 0x63, 0x75, 0x76, 0xDE, 0xE5, 0xF1, 0xFD, 0xA0, 0x00, 0x61,
|
||||
0xC7, 0x00, 0x42, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xFE, 0x87, 0xFE, 0xA8, 0xFE, 0xC8,
|
||||
0xFF, 0xC3, 0xFF, 0xF2, 0xFF, 0xFD, 0xFF, 0xFD, 0x42, 0x61, 0x74, 0xFD, 0xF4, 0xFE, 0x2F, 0x43,
|
||||
0x64, 0x67, 0x70, 0xFE, 0x54, 0xFE, 0x54, 0xFE, 0x54, 0xC8, 0x00, 0x61, 0x62, 0x65, 0x6D, 0x6E,
|
||||
0x70, 0x73, 0x72, 0x67, 0xFD, 0xAA, 0xFD, 0xB6, 0xFD, 0xD7, 0xFF, 0xEF, 0xFE, 0x34, 0xFE, 0x3D,
|
||||
0xFF, 0xF6, 0xFE, 0x5B, 0xA0, 0x03, 0x01, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0x71, 0x6D, 0xFD, 0x47, 0xA2,
|
||||
0xAA, 0xA9, 0xA8, 0xAE, 0xB4, 0xBB, 0xFE, 0x47, 0xFE, 0x47, 0xFF, 0xFB, 0xFE, 0x47, 0xFE, 0x47,
|
||||
0xFE, 0x47, 0xFE, 0x47, 0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x02, 0x51, 0x43,
|
||||
0x63, 0x74, 0x75, 0xFE, 0x28, 0xFE, 0x28, 0xFF, 0xFD, 0x41, 0x61, 0xFF, 0x4D, 0x44, 0x61, 0x6F,
|
||||
0x73, 0x75, 0xFF, 0xF2, 0xFF, 0xFC, 0xFE, 0x25, 0xFE, 0x1A, 0x22, 0x61, 0x69, 0xDF, 0xF3, 0xA0,
|
||||
0x03, 0x42, 0x21, 0x65, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x75,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x66, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
|
||||
0x21, 0xA8, 0xFD, 0xA1, 0x00, 0x71, 0xC3, 0xFD, 0xA0, 0x02, 0x92, 0x21, 0x70, 0xFD, 0x21, 0x6C,
|
||||
0xFD, 0x21, 0x61, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x03, 0x31, 0xA0, 0x04, 0x42, 0x21, 0x63, 0xFD,
|
||||
0xA0, 0x04, 0x61, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xAE, 0xFD, 0x21,
|
||||
0xC3, 0xFD, 0x21, 0x61, 0xFD, 0x22, 0x73, 0x6D, 0xE8, 0xFD, 0x21, 0x65, 0xFB, 0x21, 0x72, 0xFD,
|
||||
0xA2, 0x04, 0x31, 0x73, 0x74, 0xD7, 0xFD, 0x41, 0x65, 0xFD, 0xD5, 0x21, 0x69, 0xFC, 0xA1, 0x02,
|
||||
0x52, 0x6C, 0xFD, 0xA0, 0x01, 0x31, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x23, 0x6E, 0x6F, 0x6D, 0xDB, 0xE9, 0xFD, 0xA0, 0x04, 0x31, 0x21,
|
||||
0x6C, 0xFD, 0x44, 0x68, 0x69, 0x6F, 0x75, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xF3, 0xFF, 0xFD, 0x41,
|
||||
0x61, 0xFF, 0x9B, 0x21, 0x6F, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x63, 0xFD, 0x41,
|
||||
0x6F, 0xFE, 0x7B, 0xA0, 0x04, 0x73, 0x21, 0x72, 0xFD, 0xA0, 0x04, 0xA2, 0x21, 0x6C, 0xF7, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x04, 0x72, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x24, 0x63,
|
||||
0x6D, 0x74, 0x73, 0xE8, 0xEB, 0xF4, 0xFD, 0xA0, 0x04, 0xF3, 0x21, 0x72, 0xFD, 0xA1, 0x04, 0xC3,
|
||||
0x67, 0xFD, 0x21, 0xA9, 0xFB, 0x21, 0x62, 0xE0, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x74,
|
||||
0xD7, 0x21, 0x75, 0xD4, 0x23, 0x6E, 0x72, 0x78, 0xF7, 0xFA, 0xFD, 0x21, 0x6E, 0xB8, 0x21, 0x69,
|
||||
0xB5, 0x21, 0x6F, 0xC4, 0x22, 0x65, 0x76, 0xF7, 0xFD, 0xC6, 0x05, 0x23, 0x64, 0x67, 0x6C, 0x6E,
|
||||
0x72, 0x73, 0xFF, 0xAA, 0xFF, 0xF2, 0xFF, 0xF5, 0xFF, 0xFB, 0xFF, 0xAA, 0xFF, 0xE5, 0x41, 0xA9,
|
||||
0xFF, 0x95, 0x21, 0xC3, 0xFC, 0x41, 0x69, 0xFF, 0x97, 0x42, 0x6D, 0x70, 0xFF, 0x9C, 0xFF, 0x9C,
|
||||
0x41, 0x66, 0xFF, 0x98, 0x45, 0x64, 0x6C, 0x70, 0x72, 0x75, 0xFF, 0xEE, 0xFF, 0x7F, 0xFF, 0xF1,
|
||||
0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x04, 0xC2, 0x21, 0x93, 0xFD, 0xA0, 0x05, 0x23, 0x21, 0x6E, 0xFD,
|
||||
0xCA, 0x01, 0xC1, 0x61, 0x63, 0xC3, 0x65, 0x69, 0x6F, 0xC5, 0x70, 0x74, 0x75, 0xFF, 0x7E, 0xFF,
|
||||
0x75, 0xFF, 0x92, 0xFF, 0xA4, 0xFF, 0xB9, 0xFF, 0xE4, 0xFF, 0xF7, 0xFF, 0x75, 0xFF, 0x75, 0xFF,
|
||||
0xFD, 0x44, 0x61, 0x69, 0x6F, 0x73, 0xFD, 0xC5, 0xFF, 0x3E, 0xFD, 0xC5, 0xFF, 0xDF, 0x21, 0xA9,
|
||||
0xF3, 0x41, 0xA9, 0xFC, 0x86, 0x41, 0x64, 0xFC, 0x82, 0x22, 0xC3, 0x69, 0xF8, 0xFC, 0x41, 0x64,
|
||||
0xFE, 0x4E, 0x41, 0x69, 0xFC, 0x75, 0x41, 0x6D, 0xFC, 0x71, 0x21, 0x6F, 0xFC, 0x24, 0x63, 0x6C,
|
||||
0x6D, 0x74, 0xEC, 0xF1, 0xF5, 0xFD, 0x41, 0x6E, 0xFC, 0x61, 0x41, 0x68, 0xFC, 0x92, 0x23, 0x61,
|
||||
0x65, 0x73, 0xEF, 0xF8, 0xFC, 0xC4, 0x01, 0xE2, 0x61, 0x69, 0x6F, 0x75, 0xFC, 0x5A, 0xFC, 0x5A,
|
||||
0xFC, 0x5A, 0xFC, 0x5A, 0x21, 0x73, 0xF1, 0x41, 0x6C, 0xFB, 0xFC, 0x45, 0x61, 0xC3, 0x69, 0x79,
|
||||
0x6F, 0xFE, 0xE1, 0xFF, 0xB3, 0xFF, 0xE3, 0xFF, 0xF9, 0xFF, 0xFC, 0x48, 0x61, 0x65, 0xC3, 0x69,
|
||||
0x6F, 0x73, 0x74, 0x75, 0xFC, 0x74, 0xFC, 0x90, 0xFC, 0xBE, 0xFC, 0xCB, 0xFC, 0xE2, 0xFC, 0xF0,
|
||||
0xFD, 0x10, 0xFD, 0x13, 0xC2, 0x00, 0x61, 0x67, 0x6E, 0xFC, 0x35, 0xFF, 0xE7, 0x41, 0x64, 0xFE,
|
||||
0x6A, 0x21, 0x69, 0xFC, 0x41, 0x61, 0xFC, 0x3B, 0x21, 0x63, 0xFC, 0x21, 0x69, 0xFD, 0x22, 0x63,
|
||||
0x66, 0xF3, 0xFD, 0x41, 0x6D, 0xFC, 0x29, 0x22, 0x69, 0x75, 0xF7, 0xFC, 0x21, 0x6E, 0xFB, 0x41,
|
||||
0x73, 0xFB, 0x25, 0x21, 0x6F, 0xFC, 0x42, 0x6B, 0x72, 0xFC, 0x16, 0xFF, 0xFD, 0x41, 0x73, 0xFB,
|
||||
0xE2, 0x42, 0x65, 0x6F, 0xFF, 0xFC, 0xFB, 0xDE, 0x21, 0x72, 0xF9, 0x41, 0xA9, 0xFD, 0xED, 0x21,
|
||||
0xC3, 0xFC, 0x21, 0x73, 0xFD, 0x44, 0x64, 0x69, 0x70, 0x76, 0xFF, 0xF3, 0xFF, 0xFD, 0xFD, 0xE3,
|
||||
0xFB, 0xCA, 0x41, 0x6E, 0xFD, 0xD6, 0x41, 0x74, 0xFD, 0xD2, 0x21, 0x6E, 0xFC, 0x42, 0x63, 0x64,
|
||||
0xFD, 0xCB, 0xFB, 0xB2, 0x24, 0x61, 0x65, 0x69, 0x6F, 0xE1, 0xEE, 0xF6, 0xF9, 0x41, 0x78, 0xFD,
|
||||
0xBB, 0x24, 0x67, 0x63, 0x6C, 0x72, 0xAB, 0xB5, 0xF3, 0xFC, 0x41, 0x68, 0xFE, 0xCA, 0x21, 0x6F,
|
||||
0xFC, 0xC1, 0x01, 0xC1, 0x6E, 0xFD, 0xF2, 0x41, 0x73, 0xFE, 0xBD, 0x41, 0x73, 0xFE, 0xBF, 0x44,
|
||||
0x61, 0x65, 0x69, 0x75, 0xFF, 0xF2, 0xFF, 0xF8, 0xFE, 0xB5, 0xFF, 0xFC, 0x41, 0x61, 0xFA, 0xA5,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x23, 0x67, 0x73, 0x74, 0xD5, 0xE6, 0xFD,
|
||||
0x21, 0xA9, 0xF9, 0xA0, 0x01, 0x11, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0xC3, 0xFA, 0xC6, 0x21, 0x64, 0xFC, 0x42, 0xA9, 0xAF, 0xFA,
|
||||
0xBC, 0xFF, 0xFD, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0x73, 0xFA, 0xA4, 0xFA, 0xA4, 0xFF,
|
||||
0xF9, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0xFA, 0xA4, 0x21, 0x6F, 0xEA, 0x21, 0x6E, 0xFD, 0x44,
|
||||
0x61, 0xC3, 0x69, 0x6F, 0xFF, 0x82, 0xFF, 0xC1, 0xFF, 0xD3, 0xFF, 0xFD, 0x41, 0x68, 0xFA, 0xA5,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x06, 0x22, 0x21, 0xA9, 0xFD, 0x41,
|
||||
0xA9, 0xFC, 0x27, 0x21, 0xC3, 0xFC, 0x21, 0x63, 0xFD, 0xA0, 0x07, 0x82, 0x21, 0x68, 0xFD, 0x21,
|
||||
0x64, 0xFD, 0x24, 0x67, 0xC3, 0x73, 0x75, 0xE4, 0xEA, 0xF4, 0xFD, 0x41, 0x61, 0xFD, 0x8E, 0xC2,
|
||||
0x01, 0x72, 0x6C, 0x75, 0xFF, 0xFC, 0xFA, 0x4B, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x73,
|
||||
0xFF, 0xF7, 0xFA, 0x53, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0xFA, 0x3F, 0x21, 0xA9,
|
||||
0xEA, 0x22, 0x6F, 0xC3, 0xD1, 0xFD, 0x41, 0xA9, 0xFA, 0xB9, 0x21, 0xC3, 0xFC, 0x43, 0x66, 0x6D,
|
||||
0x72, 0xFA, 0xB2, 0xFF, 0xFD, 0xFA, 0xB5, 0x41, 0x73, 0xFC, 0xC1, 0x42, 0x68, 0x74, 0xFA, 0xA4,
|
||||
0xFC, 0xBD, 0x21, 0x70, 0xF9, 0x23, 0x61, 0x69, 0x6F, 0xE8, 0xF2, 0xFD, 0x41, 0xA8, 0xFA, 0x93,
|
||||
0x42, 0x65, 0xC3, 0xFA, 0x8F, 0xFF, 0xFC, 0x21, 0x68, 0xF9, 0x42, 0x63, 0x73, 0xFF, 0xFD, 0xF9,
|
||||
0xED, 0x41, 0xA9, 0xFA, 0xAB, 0x21, 0xC3, 0xFC, 0x43, 0x61, 0x68, 0x65, 0xFF, 0xF2, 0xFF, 0xFD,
|
||||
0xFA, 0x28, 0x43, 0x6E, 0x72, 0x74, 0xFF, 0xD3, 0xFF, 0xF6, 0xFA, 0x21, 0xA0, 0x01, 0xC1, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x74, 0xFD, 0xC6, 0x00, 0x71, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x75, 0xFB, 0x81,
|
||||
0xFB, 0x81, 0xFF, 0x57, 0xFB, 0x81, 0xFB, 0x81, 0xFB, 0x81, 0x22, 0x6E, 0x72, 0xE8, 0xEB, 0x41,
|
||||
0x73, 0xFE, 0xE4, 0xA0, 0x07, 0x22, 0x21, 0x61, 0xFD, 0xA2, 0x01, 0x12, 0x73, 0x74, 0xFA, 0xFD,
|
||||
0x43, 0x6F, 0x73, 0x75, 0xFF, 0xEF, 0xFF, 0xF9, 0xF9, 0x61, 0x21, 0x69, 0xF6, 0x21, 0x72, 0xFD,
|
||||
0x21, 0xA9, 0xFD, 0xA0, 0x07, 0x42, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x6C, 0xFD, 0xA1, 0x00, 0x71, 0x61, 0xFD, 0x41, 0x61, 0xFE, 0xA9, 0x21, 0x69,
|
||||
0xFC, 0x21, 0x72, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFF, 0x95, 0x21, 0x65, 0xFC, 0x21, 0x74,
|
||||
0xFD, 0x41, 0x6E, 0xFD, 0x23, 0x45, 0x68, 0x69, 0x6F, 0x72, 0x73, 0xF9, 0x7C, 0xFF, 0xFC, 0xFD,
|
||||
0x25, 0xF9, 0x7C, 0xF9, 0x52, 0x21, 0x74, 0xF0, 0x22, 0x6E, 0x73, 0xE6, 0xFD, 0x41, 0x6E, 0xFB,
|
||||
0xFD, 0x21, 0x61, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD,
|
||||
0x41, 0x6C, 0xFA, 0xE6, 0x21, 0x64, 0xFC, 0x21, 0x64, 0xFD, 0x49, 0x72, 0x61, 0x65, 0xC3, 0x68,
|
||||
0x6C, 0x6F, 0x73, 0x75, 0xFE, 0xF7, 0xFF, 0x48, 0xFF, 0x70, 0xFF, 0x96, 0xFF, 0xAB, 0xFF, 0xBA,
|
||||
0xFF, 0xDE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x6E, 0xF9, 0x2B, 0x21, 0x67, 0xFC, 0x41, 0x6C, 0xFB,
|
||||
0x17, 0x21, 0x6C, 0xFC, 0x22, 0x61, 0x69, 0xF6, 0xFD, 0x41, 0x67, 0xFE, 0x7D, 0x21, 0x6E, 0xFC,
|
||||
0x41, 0x72, 0xFB, 0xF2, 0x41, 0x65, 0xFF, 0x18, 0x21, 0x6C, 0xFC, 0x42, 0x72, 0x75, 0xFB, 0xE7,
|
||||
0xFF, 0xFD, 0x41, 0x68, 0xFB, 0xEA, 0xA0, 0x08, 0x02, 0x21, 0x74, 0xFD, 0xA1, 0x02, 0x93, 0x6C,
|
||||
0xFD, 0xA0, 0x08, 0x53, 0xA1, 0x08, 0x23, 0x72, 0xFD, 0x21, 0xA9, 0xFB, 0x41, 0x6E, 0xF9, 0x80,
|
||||
0x21, 0x69, 0xFC, 0x42, 0x6D, 0x6E, 0xFF, 0xFD, 0xF9, 0x79, 0x42, 0x69, 0x75, 0xFF, 0xF9, 0xF9,
|
||||
0x72, 0x41, 0x72, 0xFB, 0x57, 0x45, 0x61, 0xC3, 0x69, 0x6C, 0x75, 0xFF, 0xD7, 0xFF, 0xE4, 0xFD,
|
||||
0x7D, 0xFF, 0xF5, 0xFF, 0xFC, 0xA0, 0x08, 0x83, 0xA1, 0x02, 0x93, 0x74, 0xFD, 0x21, 0x75, 0xB9,
|
||||
0x21, 0x6C, 0xB6, 0xA3, 0x02, 0x93, 0x61, 0x6C, 0x74, 0xFA, 0xFD, 0xB3, 0xA0, 0x08, 0x23, 0x21,
|
||||
0xA9, 0xFD, 0x42, 0x66, 0x74, 0xFB, 0x26, 0xFB, 0x26, 0x42, 0x6D, 0x6E, 0xF9, 0x06, 0xFF, 0xF9,
|
||||
0x42, 0x66, 0x78, 0xFB, 0x18, 0xFB, 0x18, 0x46, 0x61, 0x65, 0xC3, 0x68, 0x69, 0x6F, 0xFF, 0xD1,
|
||||
0xFF, 0xDC, 0xFF, 0xE8, 0xF9, 0x25, 0xFF, 0xF2, 0xFF, 0xF9, 0x22, 0x62, 0x72, 0xAB, 0xED, 0x41,
|
||||
0x76, 0xFB, 0x50, 0x21, 0x75, 0xFC, 0x48, 0x74, 0x79, 0x61, 0x65, 0x63, 0x68, 0x75, 0x6F, 0xFF,
|
||||
0x4E, 0xFF, 0x57, 0xFF, 0x5A, 0xFF, 0x65, 0xFF, 0x6C, 0xF8, 0xBF, 0xFF, 0xF4, 0xFF, 0xFD, 0xC3,
|
||||
0x00, 0x61, 0x6E, 0x75, 0x76, 0xF9, 0xD1, 0xF9, 0xE4, 0xF9, 0xF0, 0x41, 0x68, 0xF8, 0x9A, 0x43,
|
||||
0x63, 0x6E, 0x74, 0xF9, 0xD7, 0xF9, 0xD7, 0xF9, 0xD7, 0x41, 0x6E, 0xF9, 0xCD, 0x22, 0x61, 0x6F,
|
||||
0xF2, 0xFC, 0x21, 0x69, 0xFB, 0x43, 0x61, 0x68, 0x72, 0xFC, 0x52, 0xF8, 0x80, 0xFF, 0xFD, 0x41,
|
||||
0x2E, 0xFE, 0x2D, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x41, 0x62, 0xFD, 0xD2, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x6F, 0xFD, 0x42, 0x73, 0x74, 0xF7, 0xFF, 0xF7, 0xFF, 0x42, 0x65, 0x69, 0xF7, 0xF8, 0xFF, 0xF9,
|
||||
0x41, 0x78, 0xFD, 0xFC, 0xA2, 0x02, 0x72, 0x6C, 0x75, 0xF5, 0xFC, 0x41, 0x72, 0xFD, 0xF1, 0x42,
|
||||
0xA9, 0xA8, 0xFD, 0x4A, 0xFF, 0xFC, 0xC2, 0x02, 0x72, 0x6C, 0x72, 0xFD, 0xE6, 0xFD, 0xE6, 0x41,
|
||||
0x69, 0xF7, 0xD2, 0xA1, 0x02, 0x72, 0x66, 0xFC, 0x41, 0x73, 0xFD, 0xD4, 0xA1, 0x01, 0xB1, 0x73,
|
||||
0xFC, 0x41, 0x72, 0xFA, 0xC2, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75, 0x74, 0xFF, 0xCF, 0xFF,
|
||||
0xDA, 0xFF, 0xE1, 0xFF, 0xEE, 0xF9, 0x51, 0xFF, 0xF7, 0xFF, 0xFC, 0x21, 0xA9, 0xEA, 0x41, 0x70,
|
||||
0xF8, 0x3E, 0x42, 0x69, 0x6F, 0xF8, 0x3A, 0xF8, 0x3A, 0x21, 0x73, 0xF9, 0x41, 0x75, 0xF8, 0x30,
|
||||
0x44, 0x61, 0x69, 0x6F, 0x72, 0xFF, 0xEE, 0xFF, 0xF9, 0xFF, 0xFC, 0xF8, 0x8C, 0x41, 0x63, 0xF8,
|
||||
0x22, 0x41, 0x72, 0xF8, 0x1B, 0x41, 0x64, 0xF8, 0x17, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x41,
|
||||
0x73, 0xF8, 0x0D, 0x21, 0x6E, 0xFC, 0x24, 0x65, 0x69, 0x6C, 0x6F, 0xE7, 0xEB, 0xF6, 0xFD, 0x41,
|
||||
0x69, 0xF8, 0x73, 0x21, 0x75, 0xFC, 0xC1, 0x01, 0xE2, 0x65, 0xFA, 0x36, 0x41, 0x64, 0xF6, 0xDA,
|
||||
0x44, 0x62, 0x67, 0x6E, 0x74, 0xF6, 0xD6, 0xF6, 0xD6, 0xFF, 0xFC, 0xF6, 0xD6, 0x42, 0x6E, 0x72,
|
||||
0xF6, 0xC9, 0xF6, 0xC9, 0x21, 0xA9, 0xF9, 0x42, 0x6D, 0x70, 0xF6, 0xBF, 0xF6, 0xBF, 0x42, 0x63,
|
||||
0x70, 0xF6, 0xB8, 0xF6, 0xB8, 0xA0, 0x07, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x74,
|
||||
0xF7, 0x22, 0x63, 0x6E, 0xFD, 0xF4, 0xA2, 0x00, 0xC2, 0x65, 0x69, 0xF5, 0xFB, 0xC7, 0x01, 0xE2,
|
||||
0x61, 0xC3, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0xC3, 0xFF, 0xD7, 0xFF, 0xDA, 0xFF, 0xE1, 0xFF,
|
||||
0xF9, 0xF6, 0x99, 0xF6, 0x99, 0xC5, 0x02, 0x52, 0x63, 0x70, 0x71, 0x73, 0x74, 0xFF, 0x6B, 0xFF,
|
||||
0x91, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF, 0xE8, 0x21, 0x73, 0xEE, 0x42, 0xC3, 0x65, 0xFF, 0x41, 0xFF,
|
||||
0xFD, 0x41, 0x74, 0xF7, 0x02, 0x21, 0x61, 0xFC, 0x53, 0x61, 0xC3, 0x62, 0x63, 0x64, 0x65, 0x69,
|
||||
0x6D, 0x70, 0x73, 0x6F, 0x6B, 0x74, 0x67, 0x6E, 0x72, 0x6C, 0x75, 0x79, 0xF8, 0xB1, 0xF8, 0xE6,
|
||||
0xF9, 0x32, 0xF9, 0xCA, 0xFB, 0x03, 0xF7, 0x50, 0xFB, 0x2C, 0xFC, 0x27, 0xFD, 0x92, 0xFE, 0x6E,
|
||||
0xFE, 0x87, 0xFE, 0x93, 0xFE, 0xAD, 0xFE, 0xCA, 0xFE, 0xD7, 0xFF, 0xF2, 0xFF, 0xFD, 0xF8, 0x85,
|
||||
0xF8, 0x85, 0xA0, 0x00, 0x81, 0x41, 0xAE, 0xFE, 0x87, 0xA0, 0x02, 0x31, 0x21, 0x2E, 0xFD, 0x21,
|
||||
0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x74, 0x65, 0xF8, 0x91, 0xFF, 0xFD, 0x23, 0x68, 0xC3, 0x73,
|
||||
0xE6, 0xE9, 0xF9, 0x21, 0x68, 0xDF, 0xA0, 0x00, 0xA2, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21,
|
||||
0x64, 0xFD, 0x21, 0xA8, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6C, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6F,
|
||||
0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x61,
|
||||
0xF1, 0xFD, 0xA0, 0x00, 0xE2, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3,
|
||||
0xFD, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x2E, 0xF6, 0x46, 0x21, 0x74,
|
||||
0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x41, 0x2E, 0xF8, 0xC6, 0x21, 0x74,
|
||||
0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0x23, 0x65, 0x69, 0x74, 0xD1, 0xE1, 0xFD, 0x41, 0x74, 0xFE,
|
||||
0x84, 0x21, 0x73, 0xFC, 0x41, 0x72, 0xF8, 0xDB, 0x21, 0x61, 0xFC, 0x22, 0x6F, 0x70, 0xF6, 0xFD,
|
||||
0x41, 0x73, 0xF5, 0xD8, 0x21, 0x69, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0xA0, 0x06, 0x41, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x41,
|
||||
0x2E, 0xFF, 0x33, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x22, 0x69, 0x65, 0xF3, 0xFD, 0x22, 0x63,
|
||||
0x6D, 0xE5, 0xFB, 0xA0, 0x02, 0x02, 0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xEA, 0x22,
|
||||
0x74, 0x6D, 0xFA, 0xFD, 0x41, 0x65, 0xFF, 0x1E, 0xA0, 0x03, 0x21, 0x21, 0x2E, 0xFD, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x63, 0x71, 0xDE, 0xFD, 0x21, 0x73, 0xC8, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF8, 0x6B, 0x21, 0x69, 0xFC, 0xA0, 0x05, 0xE1, 0x21, 0x2E,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x67, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x6D, 0xFF, 0xA3, 0x4E, 0x62, 0x64,
|
||||
0xC3, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x63, 0x67, 0x76, 0x6D, 0x69, 0x75, 0xFE, 0xCF, 0xFE, 0xD6,
|
||||
0xFE, 0xE5, 0xFF, 0x00, 0xFF, 0x49, 0xFF, 0x5E, 0xFF, 0x91, 0xFF, 0xA2, 0xFF, 0xC9, 0xFF, 0xD4,
|
||||
0xFF, 0xDB, 0xFF, 0xF9, 0xFF, 0xFC, 0xFF, 0xFC, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||
0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xFE, 0xBD, 0xA0, 0x02,
|
||||
0x41, 0x21, 0x2E, 0xFD, 0xA0, 0x00, 0x41, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0xA3, 0x00, 0xE1,
|
||||
0x2E, 0x73, 0x6E, 0xF1, 0xF4, 0xFD, 0x23, 0x2E, 0x73, 0x6E, 0xE8, 0xEB, 0xF4, 0xA1, 0x00, 0xE2,
|
||||
0x65, 0xF9, 0xA0, 0x02, 0xF1, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x74,
|
||||
0x6D, 0xFF, 0xFD, 0xFE, 0xB6, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0xC2, 0x00, 0xE2, 0x65, 0x75, 0xFF,
|
||||
0xDC, 0xFE, 0xAD, 0x49, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x75, 0x79, 0xFE, 0x62, 0xFF,
|
||||
0xA5, 0xFF, 0xCA, 0xFE, 0x62, 0xFF, 0xDA, 0xFF, 0xF2, 0xFF, 0xF7, 0xFE, 0x62, 0xFE, 0x62, 0x43,
|
||||
0x65, 0x69, 0x75, 0xFE, 0x23, 0xFC, 0x9D, 0xFC, 0x9D, 0x41, 0x69, 0xF4, 0xB7, 0xA0, 0x05, 0x92,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x75, 0xFD, 0x22, 0x65, 0x71, 0xF7, 0xFD, 0x21, 0x69, 0xFB, 0x43, 0x65,
|
||||
0x68, 0x72, 0xFE, 0x04, 0xFF, 0xEB, 0xFF, 0xFD, 0x21, 0x72, 0xE5, 0x21, 0x74, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x74, 0xDC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD,
|
||||
0x41, 0x75, 0xF7, 0x4F, 0x21, 0x71, 0xFC, 0x44, 0x65, 0xC3, 0x69, 0x6F, 0xFF, 0xE7, 0xFF, 0xF6,
|
||||
0xFC, 0x55, 0xFF, 0xFD, 0x21, 0x67, 0xB9, 0x21, 0x72, 0xFD, 0x41, 0x74, 0xF7, 0x35, 0x22, 0x65,
|
||||
0x69, 0xF9, 0xFC, 0xC1, 0x01, 0xC2, 0x65, 0xF4, 0x00, 0x21, 0x70, 0xFA, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x41, 0x6C, 0xF6, 0xCF, 0x21, 0x6C, 0xFC, 0x21,
|
||||
0x69, 0xFD, 0x41, 0x6C, 0xFE, 0x92, 0x21, 0x61, 0xFC, 0x41, 0x74, 0xFE, 0x0B, 0x21, 0x6F, 0xFC,
|
||||
0x22, 0x76, 0x70, 0xF6, 0xFD, 0x42, 0x69, 0x65, 0xFF, 0xFB, 0xFD, 0x8D, 0x21, 0x75, 0xF9, 0x48,
|
||||
0x63, 0x64, 0x6C, 0x6E, 0x70, 0x6D, 0x71, 0x72, 0xFF, 0x60, 0xFF, 0x7F, 0xFF, 0xA8, 0xFF, 0xBF,
|
||||
0xFF, 0xD6, 0xFF, 0xE0, 0xFF, 0xFD, 0xFE, 0x65, 0x45, 0xA7, 0xA9, 0xA2, 0xA8, 0xB4, 0xFD, 0x8D,
|
||||
0xFF, 0xE7, 0xFE, 0xA1, 0xFE, 0xA1, 0xFE, 0xA1, 0xA0, 0x02, 0xC3, 0x21, 0x74, 0xFD, 0x21, 0x75,
|
||||
0xFD, 0x41, 0x69, 0xFA, 0xC0, 0x41, 0x2E, 0xF3, 0xB5, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xAA, 0xFD, 0x21, 0xC3, 0xFD, 0xA3, 0x00, 0xE1, 0x6F, 0x70,
|
||||
0x72, 0xE3, 0xE6, 0xFD, 0xA0, 0x06, 0x51, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x44, 0x2E, 0x73, 0x6E, 0x76, 0xFE, 0x9E, 0xFE, 0xA1, 0xFE, 0xAA, 0xFF, 0xFD, 0x42, 0x2E, 0x73,
|
||||
0xFE, 0x91, 0xFE, 0x94, 0xA0, 0x03, 0x63, 0x21, 0x63, 0xFD, 0xA0, 0x03, 0x93, 0x21, 0x74, 0xFD,
|
||||
0x21, 0xA9, 0xFD, 0x22, 0x61, 0xC3, 0xF4, 0xFD, 0x21, 0x72, 0xFB, 0xA2, 0x00, 0x81, 0x65, 0x6F,
|
||||
0xE2, 0xFD, 0xC2, 0x00, 0x81, 0x65, 0x6F, 0xFF, 0xDB, 0xFB, 0x6A, 0x41, 0x64, 0xF5, 0x75, 0x21,
|
||||
0x6E, 0xFC, 0x21, 0x65, 0xFD, 0xCD, 0x00, 0xE2, 0x2E, 0x62, 0x65, 0x67, 0x6C, 0x6D, 0x6E, 0x70,
|
||||
0x72, 0x73, 0x74, 0x77, 0x69, 0xFE, 0x59, 0xFE, 0x5F, 0xFF, 0xBB, 0xFE, 0x5F, 0xFF, 0xE6, 0xFE,
|
||||
0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xED, 0xFE, 0x5F, 0xFE, 0x5F, 0xFE, 0x5F, 0xFF, 0xFD, 0x41,
|
||||
0x6C, 0xF2, 0xB8, 0xA1, 0x00, 0xE1, 0x6C, 0xFC, 0xA0, 0x03, 0xC2, 0xC9, 0x00, 0xE2, 0x2E, 0x62,
|
||||
0x65, 0x66, 0x67, 0x68, 0x70, 0x73, 0x74, 0xFE, 0x23, 0xFE, 0x29, 0xFE, 0x3B, 0xFE, 0x29, 0xFE,
|
||||
0x29, 0xFF, 0xFD, 0xFE, 0x29, 0xFE, 0x29, 0xFE, 0x29, 0xC2, 0x00, 0xE2, 0x65, 0x61, 0xFE, 0x1D,
|
||||
0xFC, 0xEE, 0xA0, 0x03, 0xE1, 0x22, 0x63, 0x71, 0xFD, 0xFD, 0xA0, 0x03, 0xF2, 0x21, 0x63, 0xF5,
|
||||
0x21, 0x72, 0xF2, 0x22, 0x6F, 0x75, 0xFA, 0xFD, 0x21, 0x73, 0xFB, 0x27, 0x63, 0x64, 0x70, 0x72,
|
||||
0x73, 0x75, 0x78, 0xEA, 0xEF, 0xE7, 0xE7, 0xFD, 0xE7, 0xE7, 0xA0, 0x04, 0x12, 0x21, 0xA9, 0xFD,
|
||||
0x23, 0x66, 0x6E, 0x78, 0xD2, 0xD2, 0xD2, 0x41, 0x62, 0xFC, 0x3B, 0x21, 0x72, 0xFC, 0x41, 0x69,
|
||||
0xFF, 0x5D, 0x41, 0x2E, 0xFD, 0xE0, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x42,
|
||||
0x67, 0x65, 0xFF, 0xFD, 0xF4, 0xBE, 0x21, 0x6E, 0xF9, 0x21, 0x69, 0xFD, 0x41, 0x76, 0xF4, 0xB4,
|
||||
0x21, 0x69, 0xFC, 0x24, 0x75, 0x66, 0x74, 0x6E, 0xD8, 0xDB, 0xF6, 0xFD, 0x41, 0x69, 0xF2, 0xCF,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6C, 0xF4, 0x97, 0x21, 0x75, 0xFC,
|
||||
0x21, 0x70, 0xFD, 0x21, 0x74, 0xC9, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0xC7,
|
||||
0x00, 0xE1, 0x61, 0xC3, 0x65, 0x6E, 0x67, 0x72, 0x6D, 0xFF, 0x8C, 0xFF, 0x9E, 0xFF, 0xA1, 0xFF,
|
||||
0xD4, 0xFF, 0xE7, 0xFF, 0xF1, 0xFF, 0xFD, 0x41, 0x93, 0xFB, 0xFE, 0x41, 0x72, 0xF2, 0x88, 0xA1,
|
||||
0x00, 0xE1, 0x72, 0xFC, 0xC1, 0x00, 0xE1, 0x72, 0xFE, 0x7D, 0x41, 0x64, 0xF2, 0x79, 0x21, 0x69,
|
||||
0xFC, 0x4D, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6B, 0x6C, 0x6F, 0xC5, 0x72, 0x75, 0x79, 0x63, 0xFE,
|
||||
0x8A, 0xFD, 0x27, 0xFD, 0x4C, 0xFE, 0xE4, 0xFF, 0x12, 0xFF, 0x1A, 0xFF, 0x38, 0xFF, 0xCE, 0xFF,
|
||||
0xE6, 0xFD, 0x5C, 0xFF, 0xEE, 0xFF, 0xF3, 0xFF, 0xFD, 0x41, 0x63, 0xFC, 0x7B, 0xC3, 0x00, 0xE1,
|
||||
0x61, 0x6B, 0x65, 0xFF, 0xFC, 0xFD, 0x17, 0xFD, 0x29, 0x41, 0x63, 0xFF, 0x53, 0x21, 0x69, 0xFC,
|
||||
0x21, 0x66, 0xFD, 0x21, 0x69, 0xFD, 0xA1, 0x00, 0xE1, 0x6E, 0xFD, 0x41, 0x74, 0xF2, 0x5A, 0xA1,
|
||||
0x00, 0x91, 0x65, 0xFC, 0x21, 0x6C, 0xFB, 0xC3, 0x00, 0xE1, 0x6C, 0x6D, 0x74, 0xFF, 0xFD, 0xFC,
|
||||
0x45, 0xFB, 0x1A, 0x41, 0x6C, 0xFF, 0x29, 0x21, 0x61, 0xFC, 0x21, 0x76, 0xFD, 0x41, 0x61, 0xF2,
|
||||
0xF5, 0x21, 0xA9, 0xFC, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x22, 0x6F, 0x74, 0xF0, 0xFD, 0xA0,
|
||||
0x04, 0xC3, 0x21, 0x67, 0xFD, 0x21, 0xA2, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65,
|
||||
0xFD, 0xA2, 0x00, 0xE1, 0x6E, 0x79, 0xE9, 0xFD, 0x41, 0x6E, 0xFF, 0x2B, 0x21, 0x6F, 0xFC, 0xA1,
|
||||
0x00, 0xE1, 0x63, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFB, 0x41, 0xFF, 0xFB,
|
||||
0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xFB, 0x41, 0xC2, 0x00, 0xE1, 0x2E, 0x73, 0xFC,
|
||||
0x84, 0xFC, 0x87, 0x41, 0x6F, 0xFB, 0x3F, 0x42, 0x6D, 0x73, 0xFF, 0xFC, 0xFB, 0x3E, 0x41, 0x73,
|
||||
0xFB, 0x34, 0x22, 0xA9, 0xA8, 0xF5, 0xFC, 0x21, 0xC3, 0xFB, 0xA0, 0x02, 0xA2, 0x4A, 0x75, 0x69,
|
||||
0x6F, 0x61, 0xC3, 0x65, 0x6E, 0xC5, 0x73, 0x79, 0xFF, 0x69, 0xFF, 0x7A, 0xFF, 0xB4, 0xFB, 0x08,
|
||||
0xFF, 0xC7, 0xFF, 0xDD, 0xFF, 0xFA, 0xFF, 0x0A, 0xFF, 0xFD, 0xFB, 0x08, 0x41, 0x63, 0xF3, 0x54,
|
||||
0x21, 0x69, 0xFC, 0x41, 0x67, 0xFE, 0x89, 0x21, 0x72, 0xFC, 0x21, 0x75, 0xFD, 0x41, 0x61, 0xF3,
|
||||
0x46, 0xC4, 0x00, 0xE1, 0x74, 0x67, 0x73, 0x6D, 0xFF, 0xEF, 0xF1, 0x62, 0xFF, 0xF9, 0xFF, 0xFC,
|
||||
0x47, 0xA9, 0xA2, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xFF, 0xF1, 0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5,
|
||||
0xFA, 0xC5, 0xFA, 0xC5, 0xFA, 0xC5, 0x41, 0x67, 0xF1, 0x3D, 0xC2, 0x00, 0xE1, 0x6E, 0x6D, 0xFF,
|
||||
0xFC, 0xFB, 0x62, 0x42, 0x65, 0x69, 0xFA, 0x7F, 0xF8, 0xF9, 0xC5, 0x00, 0xE1, 0x6C, 0x70, 0x2E,
|
||||
0x73, 0x6E, 0xFF, 0xF9, 0xFB, 0x5A, 0xFB, 0xF4, 0xFB, 0xF7, 0xFC, 0x00, 0xC1, 0x00, 0xE1, 0x6C,
|
||||
0xFB, 0x48, 0x41, 0x6D, 0xF1, 0x11, 0x41, 0x61, 0xF0, 0xC1, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD,
|
||||
0xC3, 0x00, 0xE1, 0x6D, 0x69, 0x64, 0xFB, 0x2C, 0xFF, 0xF2, 0xFF, 0xFD, 0x41, 0x68, 0xF8, 0xC0,
|
||||
0xA1, 0x00, 0xE1, 0x74, 0xFC, 0xA0, 0x07, 0xC2, 0x21, 0x72, 0xFD, 0x43, 0x2E, 0x73, 0x75, 0xFB,
|
||||
0xB3, 0xFB, 0xB6, 0xFF, 0xFD, 0x21, 0x64, 0xF3, 0xA2, 0x00, 0xE2, 0x65, 0x79, 0xF3, 0xFD, 0x4A,
|
||||
0xC3, 0x69, 0x63, 0x6D, 0x65, 0x75, 0x61, 0x79, 0x68, 0x6F, 0xFF, 0x81, 0xFF, 0x9B, 0xFB, 0x39,
|
||||
0xFB, 0x39, 0xFF, 0xAB, 0xFF, 0xBD, 0xFF, 0xD1, 0xFF, 0xE1, 0xFF, 0xF9, 0xFA, 0x46, 0xA0, 0x03,
|
||||
0x11, 0x21, 0x2E, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x22, 0x63, 0x7A,
|
||||
0xFD, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x64, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x76,
|
||||
0xFD, 0x21, 0x6E, 0xE9, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0xA9, 0xFD, 0x42, 0xC3, 0x73,
|
||||
0xFF, 0xFD, 0xF3, 0x42, 0x21, 0xA9, 0xF9, 0x41, 0x6E, 0xFA, 0x3D, 0x21, 0x69, 0xFC, 0x21, 0x6D,
|
||||
0xFD, 0x21, 0xA9, 0xFD, 0x41, 0x74, 0xF4, 0xB0, 0x22, 0xC3, 0x73, 0xF9, 0xFC, 0xC5, 0x00, 0xE2,
|
||||
0x69, 0x75, 0xC3, 0x6F, 0x65, 0xFF, 0xD1, 0xFD, 0xED, 0xFF, 0xE7, 0xFF, 0xFB, 0xFB, 0x49, 0x41,
|
||||
0x65, 0xF0, 0x5C, 0x21, 0x6C, 0xFC, 0x42, 0x62, 0x63, 0xFF, 0xFD, 0xF0, 0x55, 0x21, 0x61, 0xF9,
|
||||
0x21, 0x6E, 0xFD, 0xC3, 0x00, 0xE1, 0x67, 0x70, 0x73, 0xFF, 0xFD, 0xFC, 0x3E, 0xFC, 0x3E, 0x41,
|
||||
0x6D, 0xF2, 0x05, 0x44, 0x61, 0x65, 0x69, 0x6F, 0xF2, 0x01, 0xF2, 0x01, 0xF2, 0x01, 0xFF, 0xFC,
|
||||
0x21, 0x6C, 0xF3, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x06, 0xD2, 0x21, 0xA9, 0xFD, 0x21,
|
||||
0xC3, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00, 0xE1, 0x70, 0x6C,
|
||||
0xEB, 0xFD, 0x42, 0xA9, 0xA8, 0xF5, 0x47, 0xF5, 0x47, 0x48, 0x76, 0x61, 0x65, 0xC3, 0x69, 0x6F,
|
||||
0x73, 0x75, 0xFD, 0xEE, 0xF1, 0x6D, 0xF1, 0x6D, 0xFF, 0xF9, 0xF1, 0x6D, 0xF1, 0x6D, 0xF1, 0x6D,
|
||||
0xF1, 0x6D, 0x21, 0x79, 0xE7, 0x41, 0x65, 0xFC, 0xAD, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0xA2, 0x00, 0xE1, 0x6C, 0x61, 0xF0, 0xFD, 0xC2, 0x00, 0xE2, 0x75, 0x65, 0xF9, 0x7E,
|
||||
0xFA, 0xAD, 0x43, 0x6D, 0x74, 0x68, 0xFE, 0x5B, 0xF1, 0xA4, 0xEF, 0x15, 0xC4, 0x00, 0xE1, 0x72,
|
||||
0x2E, 0x73, 0x6E, 0xFF, 0xF6, 0xFA, 0x82, 0xFA, 0x85, 0xFA, 0x8E, 0x41, 0x6C, 0xEF, 0x95, 0x21,
|
||||
0x75, 0xFC, 0xA0, 0x06, 0xF3, 0x21, 0x71, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0xA2, 0x00,
|
||||
0xE1, 0x6E, 0x72, 0xF1, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF9, 0x00, 0xFF,
|
||||
0xF9, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xF9, 0x00, 0xC1, 0x00, 0x81, 0x65, 0xFB,
|
||||
0xB2, 0x41, 0x73, 0xEF, 0x26, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0xA0, 0x07, 0x62, 0x21, 0xA9,
|
||||
0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x73, 0xF4, 0xA2, 0x00, 0x41, 0x61, 0x69, 0xFA,
|
||||
0xFD, 0xC8, 0x00, 0xE2, 0x2E, 0x65, 0x6C, 0x6E, 0x6F, 0x72, 0x73, 0x74, 0xFA, 0x1D, 0xFA, 0x35,
|
||||
0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xE7, 0xFF, 0xDA, 0xFA, 0x23, 0xFF, 0xF9, 0x41, 0xA9, 0xF8, 0xC6,
|
||||
0x41, 0x75, 0xF8, 0xC2, 0x22, 0xC3, 0x65, 0xF8, 0xFC, 0x41, 0x68, 0xF8, 0xB9, 0x21, 0x63, 0xFC,
|
||||
0x21, 0x79, 0xFD, 0x41, 0x72, 0xF8, 0xAF, 0x22, 0xA8, 0xA9, 0xFC, 0xFC, 0x21, 0xC3, 0xFB, 0x4D,
|
||||
0x72, 0x75, 0x61, 0x69, 0x6F, 0x6C, 0x65, 0xC3, 0x68, 0x6E, 0x73, 0x74, 0x79, 0xFE, 0xAE, 0xFE,
|
||||
0xD4, 0xFF, 0x0C, 0xFC, 0x95, 0xFF, 0x43, 0xFF, 0x4A, 0xFF, 0x5D, 0xFF, 0x86, 0xFF, 0xC2, 0xFF,
|
||||
0xE5, 0xFF, 0xF1, 0xFF, 0xFD, 0xF8, 0x86, 0x41, 0x63, 0xF1, 0xA8, 0x21, 0x6F, 0xFC, 0x41, 0x64,
|
||||
0xF1, 0xA1, 0x21, 0x69, 0xFC, 0x41, 0x67, 0xF1, 0x9A, 0x41, 0x67, 0xF0, 0xB7, 0x21, 0x6C, 0xFC,
|
||||
0x41, 0x6C, 0xF1, 0x8F, 0x23, 0x69, 0x75, 0x6F, 0xF1, 0xF9, 0xFC, 0x41, 0x67, 0xF8, 0x89, 0x21,
|
||||
0x69, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x42, 0x65, 0x69, 0xFF, 0xFD, 0xF6, 0x84, 0x42,
|
||||
0x74, 0x6F, 0xF9, 0xAC, 0xFF, 0xE1, 0x41, 0x74, 0xF8, 0x1F, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x6F, 0xFD, 0x26, 0x6E, 0x63, 0x64, 0x74, 0x73, 0x66, 0xB5, 0xBC, 0xCE,
|
||||
0xE2, 0xE9, 0xFD, 0x41, 0xA9, 0xF8, 0xB0, 0x42, 0x61, 0x6F, 0xF8, 0xAC, 0xF8, 0xAC, 0x22, 0xC3,
|
||||
0x69, 0xF5, 0xF9, 0x42, 0x65, 0x68, 0xF7, 0xCF, 0xFF, 0xFB, 0x41, 0x74, 0xFC, 0xE0, 0x21, 0x61,
|
||||
0xFC, 0x22, 0x63, 0x74, 0xF2, 0xFD, 0x41, 0x2E, 0xF0, 0xE1, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x65, 0xFD, 0x21, 0x63, 0xFD, 0x42, 0x73, 0x6E, 0xFF, 0xFD, 0xF1, 0x19, 0x41, 0x6E, 0xF1,
|
||||
0x12, 0x22, 0x69, 0x61, 0xF5, 0xFC, 0x42, 0x75, 0x6F, 0xFF, 0x68, 0xF9, 0xD4, 0x22, 0x6D, 0x70,
|
||||
0xF4, 0xF9, 0xA0, 0x00, 0xA1, 0x21, 0x69, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x72, 0xF7, 0x21, 0x68,
|
||||
0xFD, 0x21, 0x74, 0xFD, 0x22, 0x6C, 0x72, 0xF4, 0xFD, 0x41, 0x6C, 0xF7, 0x69, 0x41, 0x72, 0xFA,
|
||||
0x24, 0x41, 0x74, 0xFA, 0xF9, 0x21, 0x63, 0xFC, 0x21, 0x79, 0xDA, 0x22, 0x61, 0x78, 0xFA, 0xFD,
|
||||
0x41, 0x61, 0xF2, 0x17, 0x49, 0x6E, 0x73, 0x6D, 0x61, 0xC3, 0x6C, 0x62, 0x6F, 0x76, 0xFF, 0x72,
|
||||
0xFF, 0x9D, 0xFF, 0xC9, 0xFF, 0xE0, 0xF7, 0x7E, 0xFF, 0xE5, 0xFF, 0xE9, 0xFF, 0xF7, 0xFF, 0xFC,
|
||||
0x41, 0x70, 0xF8, 0x13, 0x43, 0x65, 0x6F, 0x68, 0xF7, 0x3E, 0xFF, 0xFC, 0xF8, 0x0F, 0x41, 0x69,
|
||||
0xF5, 0xAE, 0x22, 0x63, 0x74, 0xF2, 0xFC, 0xA0, 0x05, 0xB3, 0x21, 0x72, 0xFD, 0x21, 0x76, 0xFD,
|
||||
0x41, 0x65, 0xFE, 0xF9, 0x21, 0x72, 0xFC, 0x22, 0x69, 0x74, 0xF6, 0xFD, 0x41, 0x61, 0xFF, 0xA5,
|
||||
0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0xC2, 0x01, 0x71, 0x63, 0x69, 0xED, 0x74, 0xED, 0x74, 0x21,
|
||||
0x61, 0xF7, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x45, 0x73, 0x6E, 0x75, 0x78, 0x72, 0xFF, 0xCA,
|
||||
0xFF, 0xDF, 0xFF, 0xEB, 0xFF, 0xFD, 0xF8, 0x31, 0xC1, 0x00, 0xE1, 0x6D, 0xF7, 0xC4, 0x41, 0x61,
|
||||
0xF9, 0xFD, 0x41, 0x6D, 0xFA, 0xAA, 0x21, 0x69, 0xFC, 0x21, 0x72, 0xFD, 0xA2, 0x00, 0xE1, 0x63,
|
||||
0x74, 0xF2, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF6, 0xF2, 0xFF, 0xF9, 0xF6,
|
||||
0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0xF6, 0xF2, 0x41, 0x68, 0xFB, 0xD1, 0x41, 0x70, 0xED,
|
||||
0x6E, 0x21, 0x6F, 0xFC, 0x43, 0x73, 0x63, 0x74, 0xFA, 0x6A, 0xFF, 0xFD, 0xF8, 0x57, 0x41, 0x69,
|
||||
0xFE, 0x77, 0x41, 0x2E, 0xEE, 0x5F, 0x21, 0x74, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x70,
|
||||
0xFD, 0xA3, 0x00, 0xE1, 0x73, 0x6C, 0x61, 0xD3, 0xDD, 0xFD, 0xA0, 0x05, 0x52, 0x21, 0x6C, 0xFD,
|
||||
0x21, 0x64, 0xFA, 0x21, 0x75, 0xFD, 0x22, 0x61, 0x6F, 0xF7, 0xFD, 0x41, 0x6E, 0xF7, 0xEF, 0x21,
|
||||
0x65, 0xFC, 0x4D, 0x27, 0x61, 0xC3, 0x64, 0x65, 0x69, 0x68, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79,
|
||||
0xF6, 0x83, 0xFF, 0x76, 0xFF, 0x91, 0xFF, 0xA7, 0xF7, 0xEB, 0xFF, 0xDF, 0xFF, 0xF4, 0xFF, 0xFD,
|
||||
0xF6, 0x83, 0xF7, 0xFB, 0xFB, 0x78, 0xF6, 0x83, 0xF6, 0x83, 0x41, 0x63, 0xFA, 0x33, 0x41, 0x72,
|
||||
0xF6, 0xA6, 0xA1, 0x01, 0xC2, 0x61, 0xFC, 0x41, 0x73, 0xEF, 0xDE, 0xC2, 0x05, 0x23, 0x63, 0x74,
|
||||
0xF0, 0x03, 0xFF, 0xFC, 0x45, 0x70, 0x61, 0x68, 0x6F, 0x75, 0xFF, 0xEE, 0xFF, 0xF7, 0xEC, 0xAD,
|
||||
0xF0, 0x56, 0xF0, 0x56, 0x21, 0x73, 0xF0, 0x21, 0x6E, 0xFD, 0xC4, 0x00, 0xE2, 0x69, 0x75, 0x61,
|
||||
0x65, 0xFA, 0x40, 0xFF, 0xD0, 0xFF, 0xFD, 0xF7, 0x9C, 0x41, 0x79, 0xFB, 0x9D, 0x21, 0x68, 0xFC,
|
||||
0xC3, 0x00, 0xE1, 0x6E, 0x6D, 0x63, 0xFB, 0x66, 0xF6, 0xCC, 0xFF, 0xFD, 0x41, 0x6D, 0xFB, 0xEE,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x70, 0xFD, 0x41,
|
||||
0x6D, 0xEE, 0x61, 0x21, 0x61, 0xFC, 0x42, 0x74, 0x2E, 0xFF, 0xFD, 0xF7, 0x48, 0xC5, 0x00, 0xE1,
|
||||
0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xFB, 0x39, 0xFF, 0xEF, 0xFF, 0xF9, 0xF7, 0x41, 0xF7, 0x4D, 0xC2,
|
||||
0x00, 0x81, 0x69, 0x65, 0xF3, 0x22, 0xF8, 0x9E, 0x41, 0x73, 0xEB, 0xD9, 0x21, 0x6F, 0xFC, 0x21,
|
||||
0x6D, 0xFD, 0x44, 0x2E, 0x73, 0x72, 0x75, 0xF7, 0x1C, 0xF7, 0x1F, 0xFF, 0xFD, 0xFB, 0x66, 0xC7,
|
||||
0x00, 0xE2, 0x72, 0x2E, 0x65, 0x6C, 0x6D, 0x6E, 0x73, 0xFF, 0xE0, 0xF7, 0x0F, 0xFF, 0xF3, 0xF7,
|
||||
0x15, 0xF7, 0x15, 0xF7, 0x15, 0xF7, 0x15, 0x41, 0x62, 0xF9, 0x76, 0x41, 0x73, 0xEC, 0x06, 0x21,
|
||||
0x67, 0xFC, 0xC3, 0x00, 0xE1, 0x72, 0x6D, 0x6E, 0xFF, 0xF5, 0xF6, 0x4A, 0xFF, 0xFD, 0xC2, 0x00,
|
||||
0xE1, 0x6D, 0x72, 0xF6, 0x3E, 0xF9, 0x8D, 0x42, 0x62, 0x70, 0xEB, 0x8A, 0xEB, 0x8A, 0x44, 0x65,
|
||||
0x69, 0x6F, 0x73, 0xEB, 0x83, 0xEB, 0x83, 0xFF, 0xF9, 0xEB, 0x83, 0x21, 0xA9, 0xF3, 0x21, 0xC3,
|
||||
0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF5,
|
||||
0x5F, 0xF5, 0x5F, 0xFF, 0xFB, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0xF5, 0x5F, 0x41,
|
||||
0x74, 0xF1, 0x2A, 0x21, 0x6E, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x68, 0xFD, 0x41, 0x6C, 0xFA, 0x2E,
|
||||
0x4B, 0x72, 0x61, 0x65, 0x68, 0x75, 0x6F, 0xC3, 0x63, 0x69, 0x74, 0x79, 0xFF, 0x0A, 0xFF, 0x20,
|
||||
0xFF, 0x4D, 0xFF, 0x7F, 0xFF, 0xA2, 0xFF, 0xAE, 0xFF, 0xD6, 0xFF, 0xF9, 0xF5, 0x35, 0xFF, 0xFC,
|
||||
0xF5, 0x35, 0xC1, 0x00, 0xE1, 0x63, 0xF8, 0xEB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||
0xF5, 0x0D, 0xFF, 0xFA, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0xF5, 0x0D, 0x41, 0x75,
|
||||
0xFF, 0x01, 0x21, 0x68, 0xFC, 0xC2, 0x00, 0xE1, 0x72, 0x63, 0xF5, 0x32, 0xFF, 0xFD, 0xC2, 0x00,
|
||||
0xE2, 0x65, 0x61, 0xF6, 0x58, 0xF3, 0x41, 0x41, 0x74, 0xF6, 0x64, 0xC2, 0x00, 0xE2, 0x65, 0x69,
|
||||
0xF6, 0x4B, 0xFF, 0xFC, 0x4A, 0x61, 0xC3, 0x65, 0x69, 0x6C, 0x6F, 0x72, 0x73, 0x75, 0x79, 0xFD,
|
||||
0xC4, 0xFF, 0xC4, 0xF6, 0x39, 0xFF, 0xE1, 0xFF, 0xEA, 0xF4, 0xD1, 0xFF, 0xF7, 0xF9, 0xC6, 0xFD,
|
||||
0xC4, 0xF4, 0xD1, 0x45, 0x61, 0x65, 0x69, 0x6F, 0x79, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4, 0xCF, 0xF4,
|
||||
0xCF, 0xF4, 0xCF, 0x41, 0x75, 0xFA, 0x87, 0x21, 0x71, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x42, 0x6D, 0x6E, 0xF2, 0xE6, 0xFF, 0xFD, 0xC2, 0x00, 0xE2,
|
||||
0x65, 0x61, 0xF5, 0xF9, 0xFF, 0xF9, 0xC1, 0x00, 0xE1, 0x65, 0xF5, 0xF0, 0x4C, 0x61, 0xC3, 0x65,
|
||||
0x68, 0x69, 0x6C, 0x6E, 0x6F, 0x72, 0x75, 0x73, 0x79, 0xF4, 0x79, 0xF5, 0xBC, 0xF5, 0xE1, 0xFF,
|
||||
0xC7, 0xF7, 0xA7, 0xF5, 0xF1, 0xF5, 0xF1, 0xF4, 0x79, 0xFF, 0xF1, 0xFF, 0xFA, 0xF9, 0x6E, 0xF4,
|
||||
0x79, 0x41, 0x69, 0xEF, 0xBB, 0x21, 0x75, 0xFC, 0x42, 0x71, 0x2E, 0xFF, 0xFD, 0xF5, 0xA6, 0xC5,
|
||||
0x00, 0xE1, 0x72, 0x6D, 0x73, 0x2E, 0x6E, 0xEA, 0xD7, 0xF6, 0x80, 0xFF, 0xF9, 0xF5, 0x9F, 0xF5,
|
||||
0xAB, 0x41, 0x69, 0xF6, 0xD1, 0x42, 0x6C, 0x73, 0xFF, 0xFC, 0xEB, 0x02, 0xA0, 0x02, 0xD2, 0x21,
|
||||
0x68, 0xFD, 0x42, 0xC3, 0x61, 0xFA, 0x3F, 0xFF, 0xFD, 0xC2, 0x06, 0x02, 0x6F, 0x73, 0xF5, 0x12,
|
||||
0xF5, 0x12, 0x21, 0x72, 0xF7, 0x21, 0x65, 0xFD, 0xC5, 0x00, 0xE1, 0x63, 0x62, 0x6D, 0x72, 0x70,
|
||||
0xFD, 0xB2, 0xFF, 0xDD, 0xF4, 0xC4, 0xFF, 0xEA, 0xFF, 0xFD, 0x41, 0x6C, 0xFC, 0x26, 0xA1, 0x00,
|
||||
0xE2, 0x75, 0xFC, 0x21, 0x72, 0xFB, 0x41, 0x61, 0xF4, 0x0C, 0x21, 0x69, 0xFC, 0x21, 0x74, 0xFD,
|
||||
0x41, 0x6D, 0xF4, 0x02, 0x21, 0x72, 0xFC, 0x41, 0x6C, 0xF3, 0xFB, 0x41, 0x6F, 0xF8, 0xC3, 0x22,
|
||||
0x65, 0x72, 0xF8, 0xFC, 0x45, 0x6F, 0x61, 0x65, 0x68, 0x69, 0xFF, 0xDF, 0xFF, 0xE9, 0xFF, 0xF0,
|
||||
0xFB, 0x48, 0xFF, 0xFB, 0x41, 0x6F, 0xF6, 0x5E, 0x42, 0x6C, 0x76, 0xFF, 0xFC, 0xF3, 0xDA, 0x41,
|
||||
0x76, 0xF3, 0xD3, 0x22, 0x61, 0x6F, 0xF5, 0xFC, 0x41, 0x70, 0xFB, 0x11, 0x41, 0xA9, 0xFB, 0x17,
|
||||
0x21, 0xC3, 0xFC, 0x41, 0x70, 0xF3, 0xBF, 0xC3, 0x00, 0xE2, 0x2E, 0x65, 0x73, 0xF4, 0xF7, 0xF6,
|
||||
0x66, 0xF4, 0xFD, 0x24, 0x61, 0x6C, 0x6F, 0x68, 0xE5, 0xED, 0xF0, 0xF4, 0x41, 0x6D, 0xF9, 0x29,
|
||||
0xC6, 0x00, 0xE2, 0x2E, 0x65, 0x6D, 0x6F, 0x72, 0x73, 0xF4, 0xDE, 0xF4, 0xF6, 0xF4, 0xE4, 0xFF,
|
||||
0xFC, 0xF4, 0xE4, 0xF4, 0xE4, 0x41, 0x64, 0xF3, 0x8D, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x6E, 0xF3, 0x7D, 0x21, 0x69, 0xFC, 0xA0, 0x07, 0xE2, 0x21,
|
||||
0x73, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0xA9, 0xFD, 0x21, 0xC3, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0xA9,
|
||||
0xFD, 0x41, 0x67, 0xFF, 0x5F, 0x41, 0x6B, 0xF3, 0x5D, 0x42, 0x63, 0x6D, 0xFF, 0xFC, 0xFF, 0x62,
|
||||
0x41, 0x74, 0xFA, 0x90, 0x21, 0x63, 0xFC, 0x42, 0x6F, 0x75, 0xFF, 0x81, 0xFF, 0xFD, 0x41, 0x65,
|
||||
0xF3, 0x44, 0x21, 0x6C, 0xFC, 0x27, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x72, 0x79, 0xBD, 0xC4, 0xD9,
|
||||
0xDC, 0xE4, 0xF2, 0xFD, 0x4D, 0x65, 0x75, 0x70, 0x6C, 0x61, 0xC3, 0x63, 0x68, 0x69, 0x6F, 0xC5,
|
||||
0x74, 0x79, 0xFE, 0xCB, 0xFF, 0x04, 0xFF, 0x40, 0xFF, 0x5F, 0xF3, 0x11, 0xF4, 0x54, 0xFF, 0x7F,
|
||||
0xFF, 0x8C, 0xF3, 0x11, 0xF3, 0x11, 0xF7, 0x13, 0xFF, 0xF1, 0xF3, 0x11, 0x41, 0x69, 0xF3, 0x97,
|
||||
0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x22, 0x6D, 0x73, 0xFD, 0xF6, 0x21, 0x6F, 0xFB, 0x21, 0x6E,
|
||||
0xFD, 0x41, 0x75, 0xED, 0x66, 0x41, 0x73, 0xEC, 0x54, 0x21, 0x64, 0xFC, 0x21, 0x75, 0xFD, 0x41,
|
||||
0x6F, 0xF6, 0xA4, 0x42, 0x73, 0x70, 0xEA, 0xC3, 0xFF, 0xFC, 0x21, 0x69, 0xF9, 0x43, 0x6D, 0x62,
|
||||
0x6E, 0xF3, 0x6F, 0xFF, 0xEF, 0xFF, 0xFD, 0x41, 0x67, 0xF3, 0x5C, 0x21, 0x6E, 0xFC, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFA, 0x82, 0x21, 0x74, 0xFC, 0x41, 0x6E, 0xFA, 0xEA, 0x21,
|
||||
0x6F, 0xFC, 0x42, 0x73, 0x74, 0xF7, 0x88, 0xF7, 0x88, 0x41, 0x6F, 0xF7, 0x81, 0x21, 0x72, 0xFC,
|
||||
0x21, 0xA9, 0xFD, 0x41, 0x6D, 0xF7, 0x77, 0x41, 0x75, 0xF7, 0x73, 0x42, 0x64, 0x74, 0xF7, 0x6F,
|
||||
0xFF, 0xFC, 0x41, 0x6E, 0xF7, 0x68, 0x21, 0x6F, 0xFC, 0x21, 0x69, 0xFD, 0x21, 0x74, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x22, 0x61, 0x69, 0xE9, 0xFD, 0x25, 0x61, 0xC3, 0x69, 0x6F, 0x72, 0xCB, 0xD9, 0xDC,
|
||||
0xDC, 0xFB, 0x21, 0x74, 0xF5, 0x41, 0x61, 0xE9, 0x22, 0x21, 0x79, 0xFC, 0x4B, 0x67, 0x70, 0x6D,
|
||||
0x72, 0x62, 0x63, 0x64, 0xC3, 0x69, 0x73, 0x78, 0xFF, 0x72, 0xFF, 0x75, 0xFF, 0x91, 0xF3, 0x5D,
|
||||
0xFF, 0xA5, 0xFF, 0xAC, 0xFD, 0x10, 0xF2, 0x46, 0xFF, 0xB3, 0xFF, 0xF6, 0xFF, 0xFD, 0x41, 0x6E,
|
||||
0xE8, 0xBD, 0xA1, 0x00, 0xE1, 0x67, 0xFC, 0x46, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x72, 0xFF, 0xFB,
|
||||
0xF3, 0x86, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x1E, 0xF2, 0x3B, 0xA0, 0x01, 0x71, 0x21, 0xA9, 0xFD,
|
||||
0x21, 0xC3, 0xFD, 0x41, 0x74, 0xE8, 0x44, 0x21, 0x70, 0xFC, 0x22, 0x69, 0x6F, 0xF6, 0xFD, 0xA1,
|
||||
0x00, 0xE1, 0x6D, 0xFB, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF1, 0xF1, 0xFF, 0xFB,
|
||||
0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0xF1, 0x41, 0xA9, 0xE9, 0x74, 0xC7, 0x06,
|
||||
0x02, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73, 0x75, 0xF2, 0xCD, 0xF2, 0xCD, 0xFF, 0xFC, 0xF2, 0xCD,
|
||||
0xF2, 0xCD, 0xF2, 0xCD, 0xF2, 0xCD, 0x21, 0x72, 0xE8, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F, 0x73,
|
||||
0x75, 0xE9, 0xBD, 0xE9, 0xBD, 0xED, 0x93, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0xE9, 0xBD, 0x22,
|
||||
0x65, 0x6F, 0xE7, 0xEA, 0xA1, 0x00, 0xE1, 0x70, 0xFB, 0x47, 0x61, 0xC3, 0x65, 0x69, 0x6F, 0x75,
|
||||
0x79, 0xF1, 0x9C, 0xFF, 0xAB, 0xF6, 0x71, 0xF4, 0xCA, 0xF1, 0x9C, 0xFA, 0x8F, 0xFF, 0xFB, 0x41,
|
||||
0x76, 0xF3, 0xC0, 0x41, 0x76, 0xE8, 0x54, 0x41, 0x78, 0xE8, 0x50, 0x22, 0x6F, 0x61, 0xF8, 0xFC,
|
||||
0x21, 0x69, 0xFB, 0x41, 0x72, 0xF2, 0x20, 0x21, 0x74, 0xFC, 0x45, 0x63, 0x65, 0x76, 0x6E, 0x73,
|
||||
0xF2, 0x5E, 0xFF, 0xE5, 0xF2, 0x5E, 0xFF, 0xF6, 0xFF, 0xFD, 0x42, 0x6E, 0x73, 0xE9, 0xBA, 0xE9,
|
||||
0xBA, 0x21, 0x69, 0xF9, 0x21, 0x6C, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0xC2, 0x00, 0xE1,
|
||||
0x63, 0x6E, 0xF3, 0x82, 0xFF, 0xFD, 0xC2, 0x00, 0xE1, 0x6C, 0x64, 0xF4, 0x69, 0xF9, 0xE8, 0x41,
|
||||
0x74, 0xF7, 0x1B, 0x21, 0x6F, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0x42, 0x72, 0x2E, 0xFF,
|
||||
0xFD, 0xF2, 0x88, 0x42, 0x69, 0x74, 0xEF, 0x79, 0xFF, 0xF9, 0xC3, 0x00, 0xE1, 0x6E, 0x2E, 0x73,
|
||||
0xFF, 0xF9, 0xF2, 0x74, 0xF2, 0x77, 0x41, 0x69, 0xE7, 0x51, 0x21, 0x6B, 0xFC, 0x21, 0x73, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x47, 0xA2, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB,
|
||||
0xF0, 0xFD, 0xFF, 0xFB, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0xF0, 0xFD, 0x41, 0x6D,
|
||||
0xE9, 0xDD, 0x21, 0x61, 0xFC, 0x21, 0x74, 0xFD, 0xA1, 0x00, 0xE1, 0x6C, 0xFD, 0x48, 0x61, 0x69,
|
||||
0x65, 0xC3, 0x6F, 0x72, 0x75, 0x79, 0xFF, 0x90, 0xFF, 0x99, 0xFF, 0xBD, 0xFF, 0xDB, 0xFF, 0xFB,
|
||||
0xF2, 0x50, 0xF0, 0xD8, 0xF0, 0xD8, 0xA0, 0x01, 0xD1, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x42,
|
||||
0x69, 0x75, 0xFF, 0xFD, 0xF0, 0xF8, 0x41, 0x72, 0xF6, 0xE9, 0xA1, 0x00, 0xE1, 0x77, 0xFC, 0x48,
|
||||
0xA2, 0xA0, 0xA9, 0xA8, 0xAA, 0xAE, 0xB4, 0xBB, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6,
|
||||
0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0xF0, 0xA6, 0x41, 0x2E, 0xE6, 0x8A, 0x21, 0x74, 0xFC, 0x21,
|
||||
0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x4A, 0x69, 0x6C, 0x61, 0xC3, 0x65, 0x6F, 0x73, 0x75, 0x79, 0x6D,
|
||||
0xF3, 0xAE, 0xFF, 0xCA, 0xFF, 0xD5, 0xFF, 0xDA, 0xF1, 0xE8, 0xF0, 0x80, 0xF8, 0x95, 0xF0, 0x80,
|
||||
0xF0, 0x80, 0xFF, 0xFD, 0x41, 0x6C, 0xF3, 0x8B, 0x42, 0x69, 0x65, 0xFF, 0xFC, 0xF9, 0xD3, 0xC1,
|
||||
0x00, 0xE2, 0x2E, 0xF1, 0xAF, 0x49, 0x61, 0xC3, 0x65, 0x68, 0x69, 0x6F, 0x72, 0x75, 0x79, 0xF0,
|
||||
0x50, 0xF1, 0x93, 0xF1, 0xB8, 0xFF, 0xFA, 0xF0, 0x50, 0xF0, 0x50, 0xF0, 0x6D, 0xF0, 0x50, 0xF0,
|
||||
0x50, 0x42, 0x61, 0x65, 0xF0, 0x76, 0xF1, 0xA5, 0xA1, 0x00, 0xE1, 0x75, 0xF9, 0x41, 0x69, 0xFA,
|
||||
0x32, 0x21, 0x72, 0xFC, 0xA1, 0x00, 0xE1, 0x74, 0xFD, 0xA0, 0x01, 0xF2, 0x21, 0x2E, 0xFD, 0x22,
|
||||
0x2E, 0x73, 0xFA, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x61, 0xFD, 0x4A, 0x75, 0x61, 0xC3, 0x65, 0x69,
|
||||
0x6F, 0xC5, 0x73, 0x78, 0x79, 0xFF, 0xEA, 0xF0, 0x0B, 0xF1, 0x4E, 0xF1, 0x73, 0xF0, 0x0B, 0xF0,
|
||||
0x0B, 0xF4, 0x0D, 0xFF, 0xFD, 0xF8, 0x58, 0xF0, 0x0B, 0x41, 0x68, 0xF8, 0x39, 0x21, 0x74, 0xFC,
|
||||
0x42, 0x73, 0x6C, 0xFF, 0xFD, 0xF8, 0x38, 0x41, 0x6F, 0xFD, 0x5C, 0x21, 0x74, 0xFC, 0x22, 0x61,
|
||||
0x73, 0xF2, 0xFD, 0x42, 0xA9, 0xA8, 0xEF, 0xD2, 0xEF, 0xD2, 0x47, 0x61, 0x65, 0xC3, 0x69, 0x6F,
|
||||
0x75, 0x79, 0xEF, 0xCB, 0xF1, 0x33, 0xFF, 0xF9, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB, 0xEF, 0xCB,
|
||||
0x5D, 0x27, 0x2E, 0x61, 0x62, 0xC3, 0x63, 0x6A, 0x6D, 0x72, 0x70, 0x69, 0x65, 0x64, 0x74, 0x66,
|
||||
0x67, 0x73, 0x6F, 0x77, 0x68, 0x75, 0x76, 0x6C, 0x78, 0x6B, 0x71, 0x6E, 0x79, 0x7A, 0xE7, 0xD0,
|
||||
0xEF, 0x48, 0xF0, 0xCD, 0xF1, 0x53, 0xF2, 0x28, 0xF3, 0xD1, 0xF3, 0xFD, 0xF4, 0xAD, 0xF5, 0x6F,
|
||||
0xF7, 0x2F, 0xF8, 0x34, 0xF8, 0x98, 0xF9, 0x32, 0xFA, 0x80, 0xFA, 0xE4, 0xFB, 0x3C, 0xFC, 0xA4,
|
||||
0xFD, 0x6C, 0xFD, 0x97, 0xFE, 0x19, 0xFE, 0x4A, 0xFE, 0xDD, 0xFF, 0x35, 0xFF, 0x58, 0xFF, 0x65,
|
||||
0xFF, 0x88, 0xFF, 0xAA, 0xFF, 0xDE, 0xFF, 0xEA,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns fr_patterns = {
|
||||
0x1AF0u,
|
||||
fr_trie_data,
|
||||
sizeof(fr_trie_data),
|
||||
};
|
||||
@@ -1,113 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t it_trie_data[] = {
|
||||
0x17, 0x0C, 0x33, 0x35, 0x0C, 0x29, 0x22, 0x0D, 0x3E, 0x0B, 0x47, 0x20, 0x0D, 0x16, 0x0B, 0x34,
|
||||
0x0D, 0x21, 0x0C, 0x3D, 0x1F, 0x0C, 0x2A, 0x17, 0x2A, 0x0B, 0x02, 0x0C, 0x01, 0x02, 0x16, 0x02,
|
||||
0x0D, 0x0C, 0x0C, 0x0D, 0x03, 0x0C, 0x01, 0x0C, 0x0E, 0x0D, 0x04, 0x02, 0x0B, 0xA0, 0x00, 0x42,
|
||||
0x21, 0x6E, 0xFD, 0xA0, 0x00, 0x72, 0x21, 0x6E, 0xFD, 0xA1, 0x00, 0x61, 0x6D, 0xFD, 0x21, 0x69,
|
||||
0xFB, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x6E, 0xEC, 0xFD, 0xA0, 0x00, 0x91, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x69, 0xFD, 0xA0, 0x00, 0xA2, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x6D,
|
||||
0xFD, 0x21, 0x75, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xE1, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0xA3, 0x01, 0x11, 0x61, 0x69, 0x6F, 0xDF,
|
||||
0xEE, 0xFD, 0xA0, 0x00, 0xF2, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0xA1, 0x01, 0x11, 0x69, 0xFD, 0xA0, 0x01, 0x12, 0x21, 0x75, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x78, 0xFD, 0xA0, 0x01, 0x32, 0x21, 0x6B, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x00,
|
||||
0x71, 0x21, 0x65, 0xFD, 0x22, 0x61, 0x65, 0xF7, 0xFD, 0x21, 0x72, 0xFB, 0xA0, 0x01, 0x52, 0x21,
|
||||
0x61, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x70, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x01, 0x71, 0x21, 0x6F,
|
||||
0xFD, 0x21, 0x63, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0xA0, 0x00, 0x61, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x74, 0xFD, 0x41, 0x70, 0xFF, 0x50, 0x21, 0x6F, 0xFC, 0x21, 0x74, 0xFD, 0x22, 0x70, 0x72,
|
||||
0xF3, 0xFD, 0x21, 0x61, 0xE8, 0x21, 0x72, 0xFD, 0xA0, 0x00, 0xF1, 0x22, 0x6C, 0x72, 0xFD, 0xFD,
|
||||
0x21, 0x69, 0xE3, 0x21, 0x6C, 0xFD, 0x41, 0x65, 0xFF, 0x43, 0xA0, 0x01, 0x11, 0x25, 0x61, 0x68,
|
||||
0x6F, 0x72, 0x73, 0xE8, 0xEE, 0xF6, 0xF9, 0xFD, 0xA0, 0x01, 0x82, 0x21, 0x72, 0xFD, 0x21, 0x63,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x65, 0xFD, 0xA0, 0x01, 0xA2, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x72, 0xFD, 0x21, 0x61, 0xFD, 0x41, 0x75, 0xFF, 0x4C, 0x42, 0x6C, 0x72, 0xFF, 0xFC, 0xFF,
|
||||
0x48, 0x21, 0x62, 0xF9, 0x22, 0x68, 0x75, 0xEF, 0xFD, 0x47, 0x63, 0x64, 0x6C, 0x6E, 0x70, 0x72,
|
||||
0x74, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x5C, 0x21,
|
||||
0x73, 0xEA, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x01, 0x11, 0x72, 0xFD, 0x41, 0x6E, 0xFF,
|
||||
0x15, 0x21, 0x67, 0xFC, 0xA0, 0x01, 0xC2, 0x21, 0x74, 0xFD, 0x21, 0x6C, 0xFD, 0x22, 0x61, 0x65,
|
||||
0xF4, 0xFD, 0x52, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x6C, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
|
||||
0x77, 0x68, 0x6A, 0x6B, 0x7A, 0xFE, 0xC2, 0xFE, 0xCD, 0xFE, 0xF7, 0xFF, 0x12, 0xFF, 0x20, 0xFF,
|
||||
0x37, 0xFF, 0x46, 0xFF, 0x55, 0xFF, 0x6B, 0xFF, 0x8B, 0xFF, 0xA5, 0xFF, 0xC2, 0xFF, 0xE6, 0xFF,
|
||||
0xFB, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xFF, 0x88, 0xA0, 0x01, 0xE2, 0xA0, 0x00, 0xD1, 0x24,
|
||||
0x61, 0x65, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF4, 0x21, 0x61, 0xF1, 0xA0, 0x01,
|
||||
0xE1, 0x21, 0x2E, 0xFD, 0x24, 0x69, 0x75, 0x79, 0x74, 0xEB, 0xF4, 0xF7, 0xFD, 0x21, 0x75, 0xDF,
|
||||
0xA0, 0x00, 0x51, 0x22, 0x69, 0x77, 0xFA, 0xFD, 0x21, 0x69, 0xD7, 0xAE, 0x02, 0x01, 0x62, 0x63,
|
||||
0x64, 0x66, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0x6C, 0x72, 0x2E, 0x27, 0xE3, 0xE3, 0xE3, 0xE3,
|
||||
0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xF5, 0xF5, 0xE3, 0xE3, 0x22, 0x2E, 0x27, 0xC4, 0xC7, 0xC6,
|
||||
0x00, 0x51, 0x68, 0x2E, 0x27, 0x62, 0x72, 0x6E, 0xFF, 0xBF, 0xFF, 0xBF, 0xFF, 0xFB, 0xFF, 0xBF,
|
||||
0xFE, 0xFB, 0xFF, 0xBF, 0xD0, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x6B, 0x6D, 0x6E, 0x71, 0x73,
|
||||
0x74, 0x7A, 0x68, 0x6C, 0x72, 0x2E, 0x27, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF,
|
||||
0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xAA, 0xFF, 0xEB, 0xFF,
|
||||
0xBC, 0xFF, 0xBC, 0xFF, 0xAA, 0xFF, 0xAA, 0xCE, 0x02, 0x01, 0x62, 0x64, 0x67, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
|
||||
0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x89, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77, 0xFF, 0x77,
|
||||
0xFF, 0x77, 0xFF, 0x77, 0xCA, 0x02, 0x01, 0x62, 0x67, 0x66, 0x6E, 0x6C, 0x72, 0x73, 0x74, 0x2E,
|
||||
0x27, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x5C, 0xFF, 0x5C, 0xFF, 0x4A, 0xFF,
|
||||
0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xA0, 0x02, 0x12, 0xA1, 0x00, 0x51, 0x74, 0xFD, 0xD1, 0x02, 0x01,
|
||||
0x62, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E,
|
||||
0x27, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0xFB, 0xFF, 0x33, 0xFF, 0x21, 0xFF,
|
||||
0x33, 0xFF, 0x21, 0xFF, 0x33, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF, 0x21, 0xFF,
|
||||
0x21, 0xFF, 0x21, 0x41, 0x70, 0xFD, 0x4D, 0xCB, 0x02, 0x01, 0x62, 0x64, 0x68, 0x69, 0x6C, 0x6D,
|
||||
0x6E, 0x72, 0x76, 0x2E, 0x27, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFF, 0xFC, 0xFE, 0xF9, 0xFE,
|
||||
0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xFE, 0xE7, 0xC2, 0x02, 0x01, 0x2E, 0x27,
|
||||
0xFE, 0xC3, 0xFE, 0xC3, 0xCB, 0x02, 0x01, 0x67, 0x66, 0x68, 0x6B, 0x6C, 0x6D, 0x72, 0x73, 0x74,
|
||||
0x2E, 0x27, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC, 0xFE, 0xBA, 0xFE, 0xCC,
|
||||
0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xFE, 0xBA, 0xA0, 0x02, 0x33, 0x42, 0x2E, 0x27, 0xFE, 0x93,
|
||||
0xFE, 0x93, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A, 0x2E, 0x27, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||
0xFF, 0xF6, 0xFE, 0x8C, 0xFE, 0x9E, 0xFE, 0x9E, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||
0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C, 0xFE, 0x8C,
|
||||
0xFE, 0x8C, 0xFF, 0xF9, 0xCF, 0x02, 0x01, 0x62, 0x63, 0x66, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||
0x73, 0x74, 0x76, 0x77, 0x2E, 0x27, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||
0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A, 0xFE, 0x4A,
|
||||
0xFE, 0x4A, 0xFE, 0x4A, 0xA0, 0x02, 0x62, 0xA1, 0x01, 0xE1, 0x6E, 0xFD, 0x21, 0x72, 0xF8, 0x21,
|
||||
0x65, 0xFD, 0xA1, 0x01, 0xE1, 0x66, 0xFD, 0x41, 0x74, 0xFE, 0x07, 0x21, 0x69, 0xFC, 0x21, 0x65,
|
||||
0xFD, 0xD3, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x71, 0x72,
|
||||
0x73, 0x74, 0x76, 0x7A, 0x68, 0x2E, 0x27, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
|
||||
0xE6, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF,
|
||||
0xF1, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xFF, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0xA0, 0x02, 0x82,
|
||||
0xA1, 0x01, 0xE1, 0x65, 0xFD, 0x21, 0x63, 0xF8, 0xA1, 0x01, 0xE1, 0x69, 0xFD, 0xCB, 0x02, 0x01,
|
||||
0x64, 0x68, 0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x2E, 0x27, 0xFD, 0xB1, 0xFD, 0xC3, 0xFD,
|
||||
0xC3, 0xFF, 0xF3, 0xFD, 0xB1, 0xFD, 0xC3, 0xFF, 0xFB, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD, 0xB1, 0xFD,
|
||||
0xB1, 0xC3, 0x02, 0x01, 0x71, 0x2E, 0x27, 0xFD, 0x8D, 0xFD, 0x8D, 0xFD, 0x8D, 0xA0, 0x02, 0x53,
|
||||
0xA1, 0x01, 0xE1, 0x73, 0xFD, 0xD5, 0x02, 0x01, 0x62, 0x63, 0x64, 0x66, 0x68, 0x67, 0x6B, 0x6C,
|
||||
0x6D, 0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x78, 0x77, 0x7A, 0x2E, 0x27, 0xFD, 0x79, 0xFD,
|
||||
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x8B, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFF, 0xFB, 0xFD, 0x79, 0xFD, 0x79, 0xFD,
|
||||
0x79, 0xFD, 0x79, 0xFD, 0x79, 0xFD, 0x79, 0x43, 0x6D, 0x2E, 0x27, 0xFD, 0x37, 0xFD, 0x37, 0xFD,
|
||||
0x37, 0xA0, 0x02, 0xC2, 0xA1, 0x02, 0x32, 0x6D, 0xFD, 0x41, 0x6E, 0xFE, 0x8F, 0x4B, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x76, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD,
|
||||
0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xFD, 0x21, 0xA0,
|
||||
0x02, 0xE1, 0x22, 0x2E, 0x27, 0xFD, 0xFD, 0xC7, 0x02, 0xA2, 0x68, 0x73, 0x70, 0x74, 0x7A, 0x2E,
|
||||
0x27, 0xFF, 0xC0, 0xFF, 0xCD, 0xFF, 0xD2, 0xFF, 0xD6, 0xFC, 0xF7, 0xFF, 0xF8, 0xFF, 0xFB, 0xC1,
|
||||
0x00, 0x51, 0x2E, 0xFC, 0xDF, 0x41, 0x68, 0xFF, 0x18, 0xA1, 0x00, 0x51, 0x63, 0xFC, 0xC1, 0x01,
|
||||
0xE1, 0x73, 0xFE, 0xB6, 0xC2, 0x00, 0x51, 0x6B, 0x73, 0xFC, 0xCA, 0xFC, 0x06, 0xD2, 0x02, 0x01,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x7A,
|
||||
0x2E, 0x27, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFF, 0xE2, 0xFC, 0xD3,
|
||||
0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xC1, 0xFC, 0xD3, 0xFF, 0xEC, 0xFF, 0xF1, 0xFC, 0xC1, 0xFC, 0xC1,
|
||||
0xFF, 0xF7, 0xFC, 0xC1, 0xFE, 0x2E, 0xC6, 0x02, 0x01, 0x63, 0x6C, 0x72, 0x76, 0x2E, 0x27, 0xFC,
|
||||
0x88, 0xFC, 0x9A, 0xFC, 0x9A, 0xFC, 0x88, 0xFC, 0x88, 0xFD, 0xF5, 0x41, 0x72, 0xFB, 0xAF, 0xA0,
|
||||
0x02, 0xF2, 0xC5, 0x02, 0x01, 0x68, 0x61, 0x79, 0x2E, 0x27, 0xFC, 0x7E, 0xFF, 0xF9, 0xFF, 0xFD,
|
||||
0xFC, 0x6C, 0xFC, 0x6C, 0xCA, 0x02, 0x01, 0x62, 0x63, 0x66, 0x68, 0x6D, 0x70, 0x74, 0x77, 0x2E,
|
||||
0x27, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC,
|
||||
0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0x42, 0x6F, 0x69, 0xFC, 0x48, 0xFC, 0x27, 0xCB, 0x02, 0x01, 0x62,
|
||||
0x64, 0x6C, 0x6E, 0x70, 0x74, 0x73, 0x76, 0x7A, 0x2E, 0x27, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32,
|
||||
0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFC, 0x32, 0xFD, 0x9F,
|
||||
0x5A, 0x2E, 0x27, 0x61, 0x65, 0x6F, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x6E, 0x70, 0x71, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xFB, 0xC2, 0xFB, 0xF9, 0xFC,
|
||||
0x14, 0xFC, 0x23, 0xFC, 0x28, 0xFC, 0x2B, 0xFC, 0x64, 0xFC, 0x97, 0xFC, 0xC4, 0xFC, 0xED, 0xFD,
|
||||
0x27, 0xFD, 0x4B, 0xFD, 0x54, 0xFD, 0x82, 0xFD, 0xC4, 0xFE, 0x11, 0xFE, 0x5D, 0xFE, 0x81, 0xFE,
|
||||
0x95, 0xFF, 0x17, 0xFF, 0x4D, 0xFF, 0x86, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, 0xD5, 0xFF, 0xDC,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns it_patterns = {
|
||||
0x5C0u,
|
||||
it_trie_data,
|
||||
sizeof(it_trie_data),
|
||||
};
|
||||
@@ -1,987 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "Epub/hyphenation/SerializedHyphenationTrie.h"
|
||||
|
||||
// Auto-generated by generate_hyphenation_trie.py. Do not edit manually.
|
||||
alignas(4) constexpr uint8_t pl_trie_data[] = {
|
||||
0x3A, 0x26, 0x30, 0x48, 0x17, 0x3E, 0x0F, 0x52, 0x67, 0x0C, 0x44, 0x16, 0x0C, 0x0D, 0x16, 0x0D,
|
||||
0x22, 0x23, 0x21, 0x35, 0x16, 0x35, 0x0C, 0x21, 0x0C, 0x5C, 0x0D, 0x1C, 0x20, 0x0D, 0x21, 0x0E,
|
||||
0x34, 0x34, 0x0B, 0x3F, 0x16, 0x3F, 0x0C, 0x49, 0x0C, 0x53, 0x16, 0x70, 0x0D, 0x49, 0x16, 0x3E,
|
||||
0x0D, 0x3F, 0x0E, 0x21, 0x0E, 0x0D, 0x21, 0x16, 0x22, 0x17, 0x22, 0x0D, 0x23, 0x0E, 0x0C, 0x34,
|
||||
0x0D, 0x35, 0x0E, 0x7B, 0x16, 0x84, 0x0D, 0x5C, 0x17, 0x16, 0x0D, 0x0C, 0x0F, 0x16, 0x16, 0x0B,
|
||||
0x0C, 0x0B, 0x2C, 0x17, 0x0E, 0x18, 0x0F, 0x0C, 0x2B, 0x0C, 0x52, 0x0D, 0x66, 0x2A, 0x0C, 0x0D,
|
||||
0x2B, 0x18, 0x2B, 0x18, 0x0D, 0x2C, 0x19, 0x0C, 0x53, 0x0C, 0x2A, 0x17, 0x20, 0x0C, 0x21, 0x20,
|
||||
0x21, 0x0E, 0x0C, 0x22, 0x0D, 0x0C, 0x0C, 0x22, 0x0F, 0x5F, 0x2B, 0x16, 0x2D, 0x16, 0x2D, 0x0C,
|
||||
0x36, 0x0D, 0x2B, 0x0C, 0x16, 0x2B, 0x0C, 0x0C, 0x2B, 0x16, 0x0D, 0x41, 0x16, 0x34, 0x0C, 0x2C,
|
||||
0x0D, 0x2C, 0x21, 0x3E, 0x0C, 0x3F, 0x20, 0x40, 0x0D, 0x0C, 0x4B, 0x52, 0x0C, 0x53, 0x20, 0x53,
|
||||
0x0E, 0x53, 0x0E, 0x16, 0x53, 0x0C, 0x0C, 0x0D, 0x54, 0x0F, 0x54, 0x0F, 0x16, 0x54, 0x0F, 0x0C,
|
||||
0x5D, 0x16, 0x5D, 0x0C, 0x7A, 0x17, 0x0E, 0x16, 0x17, 0x10, 0x17, 0x0C, 0x18, 0x11, 0x21, 0x0C,
|
||||
0x0C, 0x2B, 0x0D, 0x23, 0x16, 0x23, 0x0C, 0x2A, 0x0D, 0x20, 0x16, 0x22, 0x0F, 0x0C, 0x2A, 0x16,
|
||||
0x34, 0x0B, 0x20, 0x34, 0x17, 0x21, 0x0C, 0x0C, 0x0D, 0x22, 0x0D, 0x0E, 0x0C, 0x0D, 0x22, 0x0F,
|
||||
0x16, 0x22, 0x17, 0x0C, 0x2B, 0x34, 0x0F, 0x35, 0x20, 0x2C, 0x0B, 0x2C, 0x0C, 0x35, 0x0E, 0x0D,
|
||||
0x35, 0x0E, 0x16, 0x35, 0x0C, 0x0C, 0x0D, 0x36, 0x0F, 0x36, 0x0F, 0x16, 0x36, 0x0F, 0x0E, 0x48,
|
||||
0x0D, 0x49, 0x0E, 0x3F, 0x2B, 0x0C, 0x21, 0x0E, 0x0D, 0x16, 0x21, 0x0E, 0x0D, 0x0C, 0x22, 0x0F,
|
||||
0x0E, 0x0D, 0x36, 0x0F, 0x0C, 0x41, 0x0C, 0x48, 0x0B, 0x66, 0x0D, 0x2A, 0x16, 0x17, 0x20, 0x11,
|
||||
0x4A, 0x0F, 0x18, 0x0D, 0x0C, 0x18, 0x17, 0x22, 0x0D, 0x20, 0x48, 0x21, 0x3E, 0x17, 0x21, 0x0C,
|
||||
0x0D, 0x68, 0x34, 0x2B, 0x2B, 0x0E, 0x22, 0x0D, 0x0C, 0x0B, 0x0C, 0x52, 0x17, 0x36, 0x21, 0x15,
|
||||
0x01, 0x16, 0x02, 0x15, 0x02, 0x0B, 0x02, 0x1F, 0x03, 0x0C, 0x03, 0x0C, 0x16, 0x03, 0x0C, 0x0C,
|
||||
0x04, 0x17, 0x04, 0x0D, 0x08, 0x16, 0x0D, 0x16, 0x16, 0x0D, 0x16, 0x0D, 0x3E, 0x0B, 0x20, 0x0B,
|
||||
0x20, 0x0C, 0x0D, 0x20, 0x17, 0x29, 0x16, 0x11, 0x16, 0x0F, 0x20, 0x0B, 0x0C, 0x2A, 0x0B, 0x16,
|
||||
0x15, 0x18, 0x0E, 0x0D, 0x20, 0x0F, 0x16, 0x11, 0x3F, 0x18, 0x16, 0x0D, 0x16, 0x16, 0x0D, 0x17,
|
||||
0x0C, 0x20, 0x15, 0x17, 0x0C, 0x0D, 0x16, 0x0C, 0x0B, 0xA0, 0x00, 0x41, 0x21, 0x87, 0xFD, 0x25,
|
||||
0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xA0, 0x00, 0x61, 0xA0, 0x00, 0x72,
|
||||
0x21, 0x87, 0xFD, 0x21, 0xC4, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x61, 0x69,
|
||||
0xFD, 0xB5, 0x00, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xDB, 0xDE, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9,
|
||||
0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xE9, 0xFB, 0xE9, 0xE9, 0x21, 0x87,
|
||||
0xD3, 0xD5, 0x00, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFF, 0xAB, 0xFF, 0xAE, 0xFF, 0xB9, 0xFF,
|
||||
0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF,
|
||||
0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF, 0xB9, 0xFF,
|
||||
0xB9, 0xFF, 0xB9, 0xA0, 0x00, 0xB1, 0xA1, 0x00, 0x92, 0x72, 0xFD, 0x21, 0x64, 0xFB, 0x21, 0xB3,
|
||||
0xFD, 0xA1, 0x00, 0x61, 0xC3, 0xFD, 0xA0, 0x00, 0xC2, 0x21, 0x77, 0xFD, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x82, 0xFD, 0x21, 0xC5, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x61, 0xFD, 0xA1, 0x00, 0x61, 0x69, 0xFD,
|
||||
0xD5, 0x00, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFF, 0x3C, 0xFF, 0x3F, 0xFF, 0x4A, 0xFF, 0x4A,
|
||||
0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A,
|
||||
0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0xE1, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0x4A, 0xFF, 0xFB, 0xFF, 0x4A,
|
||||
0xFF, 0x4A, 0xA0, 0x00, 0xE1, 0x21, 0xBA, 0xFD, 0xA1, 0x00, 0x61, 0xC5, 0xFD, 0xD5, 0x00, 0x51,
|
||||
0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73,
|
||||
0x74, 0x76, 0x77, 0x78, 0x7A, 0xFE, 0xEF, 0xFE, 0xF2, 0xFE, 0xFD, 0xFE, 0xFD, 0xFF, 0xFB, 0xFE,
|
||||
0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE,
|
||||
0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0xFE, 0xFD, 0x45,
|
||||
0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0x02, 0xFF, 0x02, 0xFF, 0x71, 0xFF, 0xBE, 0xFF, 0x02, 0xA0,
|
||||
0x00, 0xF3, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x01, 0x51, 0x21, 0x74, 0xFD, 0xA1, 0x01,
|
||||
0x41, 0x70, 0xFD, 0xA0, 0x01, 0x41, 0xA1, 0x01, 0x41, 0x75, 0xF2, 0xA2, 0x01, 0x41, 0x70, 0x72,
|
||||
0xED, 0xED, 0xA6, 0x01, 0x22, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xEC, 0xF1, 0xF4, 0xF9, 0xF1,
|
||||
0xF1, 0xA0, 0x00, 0x91, 0xA5, 0x01, 0x61, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD,
|
||||
0xFD, 0x21, 0x6F, 0xF3, 0x21, 0x72, 0xFD, 0x25, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xEA, 0xEA, 0xEA,
|
||||
0xEA, 0xEA, 0x21, 0x79, 0xF5, 0x21, 0x74, 0xFD, 0xA0, 0x01, 0x72, 0x21, 0x82, 0xFD, 0xA1, 0x01,
|
||||
0x92, 0x7A, 0xFA, 0xA0, 0x01, 0x92, 0x29, 0xC5, 0x62, 0x6B, 0x6D, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0xF5, 0xF8, 0xFD, 0xFD, 0xCB, 0xCB, 0xCB, 0xCB, 0xCB, 0x21, 0x79, 0xED, 0x21, 0x63, 0xFD, 0xA0,
|
||||
0x01, 0xB2, 0x21, 0x68, 0xD6, 0xA0, 0x01, 0xD2, 0x21, 0x73, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x61,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x47, 0x63, 0x61, 0x65, 0x69, 0x6F, 0x74, 0x75, 0xFF, 0xEE, 0xFF, 0x9D,
|
||||
0xFF, 0x9D, 0xFF, 0x9D, 0xFF, 0x9D, 0xFF, 0xFD, 0xFF, 0x9D, 0xA1, 0x01, 0xB2, 0x6F, 0xEA, 0x23,
|
||||
0x67, 0x6B, 0x74, 0xD0, 0xD0, 0xFB, 0x46, 0x62, 0x64, 0x65, 0x6E, 0x72, 0x75, 0xFF, 0x4F, 0xFF,
|
||||
0x6C, 0xFF, 0x8E, 0xFF, 0x9F, 0xFF, 0xC6, 0xFF, 0xF9, 0x41, 0x87, 0xFD, 0xE1, 0x45, 0x82, 0x84,
|
||||
0x9B, 0xBA, 0xBC, 0xFD, 0xDD, 0xFD, 0xDD, 0xFD, 0xDD, 0xFD, 0xDD, 0xFD, 0xDD, 0xA0, 0x00, 0x51,
|
||||
0xA0, 0x02, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x63, 0xFA, 0x21, 0x6B, 0xF7, 0x41, 0x68, 0xFF, 0x45,
|
||||
0xA0, 0x02, 0x41, 0xA0, 0x02, 0x52, 0x21, 0x6A, 0xFD, 0xA1, 0x02, 0x41, 0x62, 0xFD, 0x41, 0x77,
|
||||
0xFF, 0x33, 0xA8, 0x02, 0x02, 0x61, 0x65, 0x69, 0x63, 0x6D, 0x6F, 0x77, 0x7A, 0xE1, 0xE4, 0xE7,
|
||||
0xEA, 0xEE, 0xF7, 0xEE, 0xFC, 0x21, 0x7A, 0xED, 0xC1, 0x00, 0x51, 0x7A, 0xFD, 0x92, 0xB6, 0x01,
|
||||
0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70,
|
||||
0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xAB, 0xAF, 0xBF, 0xBF, 0xBF, 0xF7, 0xBF, 0xBF, 0xBF,
|
||||
0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xFA, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xBF, 0xA0, 0x02, 0x72,
|
||||
0x21, 0x9B, 0xFD, 0xA0, 0x02, 0x92, 0x22, 0xC5, 0x6B, 0xFA, 0xFD, 0x21, 0x6F, 0xFB, 0x21, 0x82,
|
||||
0xFD, 0x21, 0xC5, 0xFD, 0x41, 0x7A, 0xFD, 0x35, 0xA1, 0x00, 0x51, 0x72, 0xFC, 0xA0, 0x02, 0xB2,
|
||||
0x21, 0x77, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6B, 0xFD, 0xA0, 0x02, 0xD2, 0x21, 0x72, 0xFD, 0x21,
|
||||
0x6B, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x82, 0xFD, 0x21, 0xC5, 0xFD, 0x22, 0x6E, 0x70, 0xEB, 0xFD,
|
||||
0x21, 0x65, 0xFB, 0x21, 0x6B, 0xDA, 0x21, 0x6F, 0xFD, 0x21, 0x6E, 0xFD, 0x21, 0x72, 0xFD, 0xA0,
|
||||
0x02, 0xF2, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0xA1, 0x00, 0x72, 0x73, 0xFD, 0x21, 0x68, 0xFB,
|
||||
0x21, 0x63, 0xFD, 0xA0, 0x03, 0x12, 0x21, 0x9B, 0xFD, 0x21, 0xC5, 0xFD, 0x22, 0x65, 0x6F, 0xF4,
|
||||
0xFD, 0x21, 0x72, 0xFB, 0x21, 0x65, 0xFD, 0xA0, 0x03, 0x52, 0x22, 0x85, 0x99, 0xFD, 0xFD, 0xA4,
|
||||
0x03, 0x32, 0xC4, 0x61, 0x65, 0x6F, 0xFB, 0xF8, 0xF8, 0xF8, 0x21, 0x72, 0xF5, 0x21, 0xB3, 0xFD,
|
||||
0x21, 0xC3, 0xFD, 0xC4, 0x00, 0x51, 0x61, 0x6B, 0x74, 0x77, 0xFF, 0xB9, 0xFC, 0xC7, 0xFF, 0xE1,
|
||||
0xFF, 0xFD, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFE, 0xD7, 0xFE, 0xDB,
|
||||
0xFF, 0x6F, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFF, 0x76, 0xFF, 0x9E,
|
||||
0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB,
|
||||
0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFE, 0xEB, 0xFF, 0xF1, 0x41, 0x72, 0xFF, 0x79, 0x42, 0x74,
|
||||
0x77, 0xFF, 0xFC, 0xFF, 0x2F, 0x21, 0x6F, 0xF9, 0x21, 0x67, 0xFD, 0xA1, 0x00, 0x61, 0x75, 0xFD,
|
||||
0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0xFB, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A, 0xFC, 0x5A,
|
||||
0x41, 0x65, 0xFF, 0x16, 0x21, 0x6C, 0xFC, 0xA0, 0x03, 0x73, 0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD,
|
||||
0x21, 0x75, 0xF7, 0xA2, 0x02, 0x41, 0x62, 0x77, 0xFA, 0xFD, 0x42, 0x6C, 0x72, 0xFE, 0x66, 0xFE,
|
||||
0x66, 0xC4, 0x02, 0x02, 0x61, 0x65, 0x79, 0x6F, 0xFF, 0xF2, 0xFF, 0xF9, 0xFE, 0x5F, 0xFE, 0x6F,
|
||||
0x21, 0x7A, 0xF1, 0xA0, 0x03, 0xA2, 0x21, 0x87, 0xFD, 0xA0, 0x03, 0xC2, 0xA1, 0x03, 0xA2, 0x6B,
|
||||
0xFD, 0x24, 0x82, 0x9B, 0xBA, 0xBC, 0xFB, 0xF2, 0xF2, 0xF2, 0xC1, 0x02, 0x41, 0x7A, 0xFD, 0x87,
|
||||
0xA1, 0x01, 0xB2, 0x72, 0xFA, 0xC2, 0x01, 0xB2, 0x68, 0x7A, 0xFE, 0x3B, 0xFE, 0x3B, 0x42, 0xBA,
|
||||
0xBC, 0xFD, 0x73, 0xFD, 0x73, 0xC2, 0x01, 0xB2, 0xC5, 0x7A, 0xFF, 0xF9, 0xFE, 0x2B, 0x41, 0x7A,
|
||||
0xFD, 0x63, 0xA1, 0x01, 0xB2, 0x72, 0xFC, 0xA0, 0x03, 0xE2, 0xA1, 0x01, 0xB2, 0x74, 0xFD, 0xA1,
|
||||
0x01, 0xB2, 0x6E, 0xF8, 0xA0, 0x04, 0x03, 0xA1, 0x03, 0xE2, 0x6E, 0xFD, 0xA1, 0x01, 0xB2, 0x6B,
|
||||
0xFB, 0xC2, 0x01, 0xB2, 0x63, 0x72, 0xFD, 0xFB, 0xFF, 0xDD, 0x41, 0xBC, 0xFD, 0x37, 0xC3, 0x01,
|
||||
0xB2, 0x73, 0xC5, 0x7A, 0xFF, 0xD9, 0xFF, 0xFC, 0xFD, 0xF2, 0xC3, 0x01, 0xB2, 0x63, 0x6D, 0x7A,
|
||||
0xFD, 0xE2, 0xFD, 0xE6, 0xFD, 0xE6, 0xC2, 0x01, 0xB2, 0x6B, 0x72, 0xFD, 0xDA, 0xFD, 0xDA, 0xA1,
|
||||
0x01, 0xB2, 0x63, 0xB8, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D,
|
||||
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0x72, 0xFF, 0x7D, 0xFF, 0x8C, 0xFF, 0x91, 0xFF, 0xA1,
|
||||
0xFD, 0x5B, 0xFF, 0xAE, 0xFD, 0x5B, 0xFF, 0xB6, 0xFF, 0xBB, 0xFF, 0xC8, 0xFF, 0xCD, 0xFF, 0xDA,
|
||||
0xFF, 0xE6, 0xFF, 0xF2, 0xFF, 0xFB, 0xFD, 0x5B, 0x41, 0x77, 0xFD, 0xFB, 0x21, 0x6F, 0xFC, 0x21,
|
||||
0x67, 0xFD, 0xC2, 0x00, 0x51, 0x6F, 0x7A, 0xFF, 0xFD, 0xFB, 0x48, 0xA0, 0x04, 0x52, 0x22, 0x85,
|
||||
0x99, 0xFD, 0xFD, 0xA4, 0x04, 0x32, 0xC4, 0x61, 0x65, 0x6F, 0xFB, 0xF8, 0xF8, 0xF8, 0x21, 0x6A,
|
||||
0xF5, 0x21, 0xB3, 0xFD, 0xA1, 0x00, 0x51, 0xC3, 0xFD, 0x41, 0x61, 0xFD, 0x57, 0xA0, 0x01, 0x61,
|
||||
0xC5, 0x02, 0x02, 0x65, 0x6F, 0x74, 0x79, 0x7A, 0xFD, 0x50, 0xFD, 0x50, 0xFF, 0xF9, 0xFD, 0x50,
|
||||
0xFF, 0xFD, 0xC2, 0x02, 0x02, 0x65, 0x75, 0xFD, 0x3E, 0xFD, 0x4E, 0x22, 0x73, 0x7A, 0xE5, 0xF7,
|
||||
0xA0, 0x04, 0x72, 0x21, 0x9B, 0xFD, 0x21, 0xC5, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x99, 0xFD, 0x21, 0xC4, 0xFD, 0x21, 0x69, 0xFD, 0xA0, 0x04, 0x92, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0xA1, 0x04, 0xB2, 0x73, 0xFD, 0x21, 0x87, 0xFB, 0x22, 0xC4, 0x63, 0xFD,
|
||||
0xE0, 0x21, 0x99, 0xFB, 0x21, 0xC4, 0xFD, 0x21, 0x69, 0xFD, 0x22, 0x73, 0x77, 0xDE, 0xFD, 0x21,
|
||||
0x65, 0xFB, 0xA1, 0x00, 0x51, 0x69, 0xFD, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64,
|
||||
0x65, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77,
|
||||
0x78, 0x79, 0x7A, 0xFC, 0xD2, 0xFE, 0x59, 0xFE, 0x6D, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFE,
|
||||
0x99, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC,
|
||||
0xE6, 0xFF, 0x2D, 0xFC, 0xE6, 0xFF, 0x6B, 0xFC, 0xE6, 0xFC, 0xE6, 0xFC, 0xE6, 0xFF, 0x8D, 0xFC,
|
||||
0xE6, 0xFF, 0xB4, 0xFF, 0xFB, 0xA0, 0x05, 0x61, 0xA1, 0x00, 0xF3, 0x79, 0xFD, 0x21, 0x73, 0xFB,
|
||||
0xA0, 0x04, 0xD5, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0xA0, 0x05, 0x24, 0x21, 0x61, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x73, 0xFD, 0xA0, 0x05,
|
||||
0x22, 0x21, 0x61, 0xFD, 0x23, 0x65, 0x68, 0x74, 0xF7, 0xFD, 0xFA, 0x45, 0x61, 0x65, 0x69, 0x6F,
|
||||
0x75, 0xFC, 0x75, 0xFC, 0x75, 0xFC, 0x75, 0xFC, 0x75, 0xFC, 0x75, 0x21, 0x6F, 0xF0, 0xA0, 0x01,
|
||||
0xD1, 0x25, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xFD, 0xFD, 0xFD, 0xFD, 0xFD, 0x21, 0x6F, 0xF5, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x65, 0xFD, 0x45, 0x6B, 0x6D, 0x73, 0x67,
|
||||
0x6C, 0xFF, 0xA2, 0xFF, 0xB4, 0xFF, 0xC9, 0xFF, 0xE0, 0xFF, 0xFD, 0xD5, 0x01, 0xF1, 0xC4, 0xC5,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x7A, 0xFC, 0x0E, 0xFC, 0x12, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC,
|
||||
0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC,
|
||||
0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xFC, 0x22, 0xA0, 0x02, 0x02,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x75, 0xFA, 0x41, 0x62, 0xFE, 0x56, 0xC2, 0x00, 0x51, 0x75, 0x7A, 0xFF,
|
||||
0xFC, 0xF9, 0xA0, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFB, 0xB6, 0xFB,
|
||||
0xBA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFF, 0xED, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB,
|
||||
0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFF, 0xF0, 0xFB, 0xCA, 0xFF, 0xF7, 0xFB,
|
||||
0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xFB, 0xCA, 0xA0, 0x00, 0x71, 0xC3, 0x04,
|
||||
0x32, 0x6F, 0x61, 0x65, 0xFE, 0x0D, 0xFF, 0xFD, 0xFF, 0xFD, 0x21, 0x72, 0xF4, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x70, 0xFD, 0xD6, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xFB, 0x56, 0xFB, 0x5A,
|
||||
0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFF, 0xFD, 0xFB, 0x6A,
|
||||
0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A,
|
||||
0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xFB, 0x6A, 0xA0, 0x05, 0x72, 0x21, 0x63, 0xFD, 0xA0, 0x05,
|
||||
0x93, 0xA0, 0x05, 0xC2, 0xA2, 0x05, 0x72, 0x6B, 0x77, 0xFA, 0xFD, 0x41, 0x6E, 0xFD, 0x0C, 0x21,
|
||||
0x61, 0xFC, 0x21, 0x6C, 0xFD, 0xA4, 0x01, 0x22, 0x69, 0x6F, 0x75, 0x66, 0xE6, 0xEF, 0xE3, 0xFD,
|
||||
0xA0, 0x01, 0x22, 0x21, 0x6C, 0xFD, 0x43, 0x6E, 0x73, 0x7A, 0xFF, 0xEF, 0xFF, 0xFD, 0xFE, 0xA5,
|
||||
0x41, 0x82, 0xFB, 0x83, 0x21, 0xC5, 0xFC, 0x21, 0x64, 0xFD, 0xD6, 0x01, 0xF1, 0xC4, 0xC5, 0x61,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76,
|
||||
0x77, 0x78, 0x7A, 0xFA, 0xCF, 0xFA, 0xD3, 0xFF, 0xFD, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA,
|
||||
0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA,
|
||||
0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xFA, 0xE3, 0xA0,
|
||||
0x06, 0x01, 0xA1, 0x05, 0xE2, 0x6F, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD,
|
||||
0x21, 0x75, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x82, 0xFD, 0x03, 0xA0, 0x06, 0x93,
|
||||
0x21, 0x69, 0xFD, 0x22, 0x65, 0x79, 0xFA, 0xFA, 0x21, 0x61, 0xF5, 0xA0, 0x06, 0x92, 0x21, 0x6D,
|
||||
0xFD, 0x21, 0x63, 0xFA, 0x21, 0x74, 0xF7, 0x21, 0x67, 0xF4, 0xA7, 0x06, 0x42, 0x67, 0x73, 0x74,
|
||||
0x64, 0x6B, 0x6C, 0x72, 0xE6, 0xE9, 0xEE, 0xF4, 0xF7, 0xFA, 0xFD, 0xA0, 0x06, 0x42, 0xA0, 0x06,
|
||||
0x63, 0xA2, 0x00, 0x71, 0x6C, 0x77, 0xFD, 0xFD, 0xC5, 0x06, 0x13, 0x61, 0x65, 0x6F, 0x79, 0x75,
|
||||
0xFF, 0xE2, 0xFF, 0xF3, 0xFF, 0xF9, 0xFF, 0xF3, 0xFE, 0xC3, 0x21, 0x72, 0xEE, 0x21, 0x74, 0xFD,
|
||||
0x42, 0xC5, 0x6E, 0xFF, 0xA9, 0xFF, 0xFD, 0xA0, 0x06, 0xC2, 0x42, 0x74, 0x77, 0xFA, 0xC2, 0xFF,
|
||||
0xFD, 0x21, 0x6F, 0xF9, 0x21, 0x6B, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0xB3, 0xFD, 0xA0, 0x06, 0xE2,
|
||||
0x21, 0x87, 0xFD, 0x21, 0xC4, 0xFD, 0xC3, 0x00, 0x51, 0xC3, 0x6F, 0x7A, 0xFF, 0xF4, 0xFF, 0xFD,
|
||||
0xF7, 0xD4, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xF9, 0xE7, 0xF9, 0xEB,
|
||||
0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xFF, 0x74, 0xF9, 0xFB,
|
||||
0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xFF, 0xCE, 0xF9, 0xFB, 0xFF, 0xF4, 0xF9, 0xFB,
|
||||
0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0xF9, 0xFB, 0x41, 0x77, 0xF9, 0x39, 0x21, 0x6F,
|
||||
0xFC, 0x21, 0x64, 0xFD, 0xD6, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x7A, 0xF9, 0x95, 0xF9,
|
||||
0x99, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9,
|
||||
0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xFF,
|
||||
0xFD, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xF9, 0xA9, 0xA0, 0x01, 0x71, 0x21, 0x70, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x6E, 0xF9, 0xE8, 0x21,
|
||||
0xBC, 0xFC, 0x21, 0xC5, 0xFD, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||
0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xF9,
|
||||
0x34, 0xF9, 0x38, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xFF,
|
||||
0xF3, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xFF, 0xFD, 0xF9, 0x48, 0xF9,
|
||||
0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xF9, 0x48, 0xA0, 0x07, 0xE2,
|
||||
0x43, 0x85, 0x99, 0x87, 0xFB, 0xAD, 0xFB, 0xAD, 0xFF, 0xFD, 0xA0, 0x07, 0x22, 0x41, 0x62, 0xF8,
|
||||
0xF3, 0xA1, 0x07, 0xE2, 0x75, 0xFC, 0xA0, 0x07, 0xD1, 0x21, 0x6D, 0xFD, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x69, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6F, 0xFD, 0xA1, 0x07, 0xE2, 0x72, 0xFD,
|
||||
0xA0, 0x08, 0x02, 0x24, 0xBA, 0x82, 0x9B, 0xBC, 0xD7, 0xDE, 0xF8, 0xFD, 0xC1, 0x05, 0xC2, 0x72,
|
||||
0xF8, 0xC4, 0x41, 0x68, 0xF8, 0xBE, 0x21, 0x63, 0xFC, 0xA1, 0x05, 0xC2, 0x75, 0xFD, 0x41, 0x7A,
|
||||
0xF8, 0xB2, 0x21, 0x63, 0xFC, 0x21, 0x99, 0xFD, 0xA0, 0x07, 0x43, 0x42, 0x63, 0x74, 0xFF, 0xF3,
|
||||
0xF8, 0xA5, 0x21, 0x70, 0xF9, 0xA0, 0x07, 0xB2, 0x22, 0x85, 0x99, 0xFD, 0xFD, 0x21, 0x7A, 0xF8,
|
||||
0x21, 0x63, 0xFD, 0xA3, 0x02, 0x22, 0xC4, 0x65, 0x79, 0xF5, 0xFD, 0xF2, 0xC5, 0x05, 0xC2, 0xC4,
|
||||
0x77, 0x65, 0x75, 0x7A, 0xFF, 0xD9, 0xFF, 0xDC, 0xFF, 0xE6, 0xF8, 0x8D, 0xFF, 0xF7, 0x41, 0x72,
|
||||
0xF8, 0x75, 0xA1, 0x05, 0xC2, 0x6F, 0xFC, 0x41, 0x6A, 0xFB, 0x16, 0x41, 0x74, 0xFD, 0x46, 0xA1,
|
||||
0x01, 0x92, 0x61, 0xFC, 0x41, 0x87, 0xF7, 0xD4, 0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xF7, 0xD0, 0xF7,
|
||||
0xD0, 0xF7, 0xD0, 0xF7, 0xD0, 0xC2, 0x01, 0x92, 0x68, 0x7A, 0xFC, 0xC6, 0xFC, 0xC6, 0x42, 0xBA,
|
||||
0xBC, 0xF6, 0x95, 0xF6, 0x95, 0xC2, 0x01, 0x92, 0xC5, 0x7A, 0xFF, 0xF9, 0xFC, 0xB6, 0xA0, 0x08,
|
||||
0x42, 0xA3, 0x01, 0x92, 0x63, 0x6E, 0x74, 0xFD, 0xFD, 0xFD, 0x41, 0xBC, 0xF6, 0x79, 0xC2, 0x01,
|
||||
0x92, 0xC5, 0x7A, 0xFF, 0xFC, 0xFC, 0x9D, 0xC1, 0x01, 0x92, 0x7A, 0xFC, 0x94, 0xD1, 0x01, 0x61,
|
||||
0x74, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x77,
|
||||
0x7A, 0xFF, 0xB2, 0xFF, 0xB7, 0xFF, 0xBB, 0xF7, 0x96, 0xFF, 0xC8, 0xFF, 0xD8, 0xF7, 0x96, 0xF7,
|
||||
0x96, 0xF7, 0x96, 0xF7, 0x96, 0xF7, 0x96, 0xF7, 0x96, 0xFF, 0xE4, 0xFF, 0xF1, 0xFF, 0xFA, 0xF7,
|
||||
0x96, 0xF7, 0x96, 0xA0, 0x07, 0x74, 0x21, 0x82, 0xFD, 0xA0, 0x07, 0x73, 0x21, 0x7A, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x69, 0xF7, 0x22, 0x6A, 0x77, 0xFA, 0xFD, 0x21, 0x74, 0xEF, 0x21, 0x6F, 0xFD,
|
||||
0x23, 0xC5, 0x6F, 0x72, 0xE6, 0xF5, 0xFD, 0x21, 0x72, 0xE5, 0x21, 0x6E, 0xDF, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x41, 0x7A, 0xF9, 0xA4, 0x21, 0x72, 0xFC, 0x21, 0x62, 0xFD, 0xA4, 0x01, 0x61,
|
||||
0x62, 0x64, 0x6B, 0x6C, 0xE3, 0xEA, 0xF3, 0xFD, 0x41, 0x6D, 0xFE, 0xFD, 0xA1, 0x01, 0x61, 0x65,
|
||||
0xFC, 0xA0, 0x08, 0x01, 0x21, 0xB3, 0xFD, 0xA0, 0x08, 0x22, 0x21, 0x73, 0xFD, 0x21, 0x79, 0xFD,
|
||||
0x21, 0x7A, 0xF1, 0x21, 0x63, 0xFD, 0x21, 0x79, 0xFD, 0xA5, 0x02, 0x41, 0x69, 0xC3, 0x6D, 0x6F,
|
||||
0x77, 0xE3, 0xEB, 0xF4, 0xE8, 0xFD, 0xC2, 0x05, 0xC2, 0x68, 0x7A, 0xF6, 0xBB, 0xF6, 0xBB, 0x41,
|
||||
0xBA, 0xFB, 0xDC, 0xA1, 0x05, 0xC2, 0xC5, 0xFC, 0xC1, 0x05, 0xC2, 0x7A, 0xF6, 0xA9, 0xC1, 0x05,
|
||||
0xC2, 0x72, 0xF6, 0xA3, 0xD9, 0x07, 0x02, 0xC4, 0xC5, 0x69, 0x6D, 0x72, 0x77, 0x61, 0x65, 0x6F,
|
||||
0x79, 0x7A, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6E, 0x70, 0x73, 0x74, 0x75,
|
||||
0xFE, 0x4C, 0xFE, 0x7F, 0xFE, 0x88, 0xFE, 0x95, 0xFE, 0xC8, 0xFE, 0xDE, 0xFE, 0xE3, 0xFF, 0x39,
|
||||
0xFF, 0xA9, 0xF9, 0xF9, 0xFF, 0xD5, 0xFC, 0x2D, 0xFF, 0xE2, 0xFF, 0xEF, 0xFC, 0x2D, 0xFC, 0x2D,
|
||||
0xFC, 0x2D, 0xFC, 0x2D, 0xFC, 0x2D, 0xFC, 0x2D, 0xFC, 0x2D, 0xFC, 0x2D, 0xFF, 0xF4, 0xFF, 0xFA,
|
||||
0xFC, 0x2D, 0x41, 0x64, 0xF9, 0xAB, 0xA0, 0x05, 0xC1, 0x21, 0x74, 0xFD, 0x23, 0x7A, 0x6B, 0x75,
|
||||
0xF6, 0xFD, 0xFA, 0x42, 0x73, 0x6F, 0xFF, 0x10, 0xFF, 0x16, 0x41, 0x66, 0xF8, 0xDD, 0xC9, 0x01,
|
||||
0x61, 0x66, 0x67, 0x6B, 0x6E, 0x72, 0x73, 0x77, 0x6D, 0x75, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0xF5,
|
||||
0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0xFC, 0xF8, 0xD9, 0x44, 0x82, 0x9B, 0xBA,
|
||||
0xBC, 0xFD, 0xC1, 0xFD, 0xC1, 0xFD, 0xC1, 0xFD, 0xC1, 0xC1, 0x03, 0x32, 0x77, 0xF5, 0x0A, 0x21,
|
||||
0x7A, 0xFA, 0xA1, 0x05, 0xC2, 0x65, 0xFD, 0x42, 0xBA, 0xBC, 0xFB, 0x24, 0xFB, 0x24, 0x41, 0x87,
|
||||
0xF7, 0x0F, 0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xF7, 0x0B, 0xF7, 0x0B, 0xF7, 0x0B, 0xF7, 0x0B, 0xC2,
|
||||
0x02, 0x92, 0x68, 0x7A, 0xF4, 0xE4, 0xF4, 0xE4, 0x42, 0xBA, 0xBC, 0xFA, 0x36, 0xFA, 0x36, 0xC2,
|
||||
0x02, 0x92, 0xC5, 0x7A, 0xFF, 0xF9, 0xF4, 0xD4, 0xC1, 0x02, 0x92, 0x7A, 0xF4, 0xCB, 0xC1, 0x02,
|
||||
0x92, 0x6B, 0xF4, 0xC5, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D,
|
||||
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xCA, 0xFF, 0xCE, 0xF6, 0xDF, 0xFF, 0xDB, 0xFF, 0xEB,
|
||||
0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xF6, 0xDF, 0xFF, 0xF4,
|
||||
0xFF, 0xF4, 0xFF, 0xFA, 0xF6, 0xDF, 0xF6, 0xDF, 0xC3, 0x05, 0xC2, 0xC5, 0x6F, 0x7A, 0xFF, 0x8F,
|
||||
0xFF, 0xCC, 0xF5, 0x89, 0xA0, 0x08, 0x63, 0x21, 0x87, 0xFD, 0x24, 0x82, 0x9B, 0xBA, 0xBC, 0xFA,
|
||||
0xFA, 0xFA, 0xFA, 0x41, 0x7A, 0xF4, 0x70, 0xD1, 0x08, 0x93, 0xC4, 0xC5, 0x63, 0x64, 0x66, 0x67,
|
||||
0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x72, 0xFF, 0x77, 0xFF, 0x7B, 0xFF,
|
||||
0x88, 0xFF, 0x98, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xF6,
|
||||
0x8C, 0xF6, 0x8C, 0xF6, 0x8C, 0xFF, 0xA1, 0xF6, 0x8C, 0xF6, 0x8C, 0xFF, 0xFC, 0xC2, 0x08, 0x93,
|
||||
0x68, 0x7A, 0xFA, 0x5E, 0xFA, 0x5E, 0xA0, 0x08, 0xF2, 0x43, 0xBA, 0x9B, 0xBC, 0xF4, 0x2A, 0xF6,
|
||||
0x44, 0xFF, 0xFD, 0xD1, 0x08, 0x93, 0xC5, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C,
|
||||
0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xF6, 0xFF, 0x2B, 0xFF, 0x3C, 0xFF, 0x4C, 0xF6,
|
||||
0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6, 0x40, 0xF6,
|
||||
0x40, 0xFF, 0x55, 0xF6, 0x40, 0xF6, 0x40, 0xFA, 0x48, 0xA0, 0x08, 0x93, 0xC1, 0x08, 0x93, 0x7A,
|
||||
0xFA, 0x0F, 0xD1, 0x05, 0xC2, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D,
|
||||
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0x65, 0xFF, 0x68, 0xFF, 0x75, 0xFF, 0xAB, 0xFF, 0xC1,
|
||||
0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xFA,
|
||||
0xFF, 0xFA, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xC1, 0x08, 0xC3, 0x75, 0xF6, 0x3F, 0x21, 0x7A,
|
||||
0xFA, 0xC2, 0x05, 0xC2, 0x6F, 0x7A, 0xFF, 0xFD, 0xF4, 0xA0, 0xC2, 0x05, 0xC2, 0x6D, 0x7A, 0xF4,
|
||||
0x97, 0xF4, 0x97, 0xC2, 0x05, 0xC2, 0x6B, 0x72, 0xF4, 0x8E, 0xF4, 0x8E, 0x41, 0x7A, 0xF9, 0xAF,
|
||||
0xA1, 0x05, 0xC2, 0x63, 0xFC, 0xC1, 0x05, 0xC2, 0x77, 0xF4, 0x7C, 0xD6, 0x02, 0x01, 0xC4, 0x61,
|
||||
0x65, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6B, 0x6C, 0x6D, 0x6F, 0x70, 0x72, 0x73,
|
||||
0x74, 0x75, 0x77, 0x7A, 0xFC, 0x25, 0xFE, 0x31, 0xFE, 0x43, 0xFE, 0x61, 0xFE, 0x77, 0xFD, 0xBB,
|
||||
0xFE, 0xED, 0xFA, 0x06, 0xFA, 0x06, 0xFA, 0x06, 0xFA, 0x06, 0xFD, 0xD3, 0xFA, 0x06, 0xFA, 0x06,
|
||||
0xFF, 0x97, 0xFA, 0x06, 0xFF, 0xD6, 0xFF, 0xDF, 0xFF, 0xE8, 0xFF, 0xF5, 0xFA, 0x06, 0xFF, 0xFA,
|
||||
0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xF6, 0x93, 0xF6, 0x93, 0xF6, 0x93, 0xF6, 0x93, 0xC2, 0x01, 0xB2,
|
||||
0x63, 0x74, 0xF6, 0xCA, 0xF6, 0xCA, 0xC1, 0x01, 0xB2, 0x72, 0xF4, 0xDA, 0xA0, 0x02, 0x71, 0xC2,
|
||||
0x09, 0x12, 0xC5, 0x64, 0xFC, 0x7F, 0xFF, 0xFD, 0xC2, 0x01, 0x92, 0x70, 0x6B, 0xFC, 0x86, 0xF9,
|
||||
0x33, 0x51, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73,
|
||||
0x74, 0x77, 0x7A, 0xFF, 0xEE, 0xFC, 0x53, 0xFC, 0x57, 0xF4, 0x32, 0xFC, 0x64, 0xF4, 0x32, 0xF4,
|
||||
0x32, 0xF4, 0x32, 0xF4, 0x32, 0xF4, 0x32, 0xFF, 0xF7, 0xF4, 0x32, 0xFC, 0x96, 0xFC, 0x96, 0xF4,
|
||||
0x32, 0xF4, 0x32, 0xF4, 0x32, 0xC2, 0x01, 0xB2, 0x6F, 0x72, 0xFF, 0xCC, 0xF6, 0x69, 0xA0, 0x04,
|
||||
0x32, 0x21, 0x7A, 0xFD, 0xC8, 0x01, 0xB2, 0x63, 0x64, 0x6B, 0x72, 0x74, 0xC5, 0x6F, 0x7A, 0xF6,
|
||||
0x63, 0xF6, 0x63, 0xF6, 0x63, 0xF6, 0x63, 0xF6, 0x63, 0xF6, 0x86, 0xFF, 0xFD, 0xF4, 0x7C, 0xC2,
|
||||
0x01, 0xB2, 0x6D, 0x7A, 0xF4, 0x61, 0xF4, 0x61, 0xC2, 0x01, 0xB2, 0x63, 0x6B, 0xF4, 0x54, 0xF4,
|
||||
0x58, 0xC2, 0x01, 0xB2, 0x6D, 0x77, 0xF9, 0x20, 0xF4, 0x4F, 0x53, 0x64, 0x6A, 0xC4, 0xC5, 0x62,
|
||||
0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x75, 0x77, 0x7A, 0xFC, 0xEA,
|
||||
0xFF, 0x11, 0xF5, 0xEC, 0xFF, 0x56, 0xF6, 0x28, 0xF6, 0x0B, 0xFF, 0x63, 0xF6, 0x28, 0xF3, 0xD5,
|
||||
0xFF, 0x6C, 0xF3, 0xD5, 0xF3, 0xD5, 0xFF, 0xBB, 0xFF, 0xCA, 0xFF, 0xE5, 0xFF, 0xEE, 0xF3, 0xD5,
|
||||
0xF3, 0xD5, 0xFF, 0xF7, 0x41, 0x87, 0xFA, 0xF9, 0xA0, 0x09, 0x32, 0xC4, 0x05, 0xC2, 0x63, 0x6B,
|
||||
0x68, 0x7A, 0xFF, 0xFD, 0xFF, 0xFD, 0xF3, 0x46, 0xF3, 0x46, 0xA0, 0x09, 0x52, 0x42, 0xBA, 0xBC,
|
||||
0xFF, 0xFD, 0xF8, 0x5E, 0x41, 0x7A, 0xF7, 0x8A, 0xA1, 0x02, 0x92, 0x72, 0xFC, 0xC1, 0x02, 0x92,
|
||||
0x72, 0xF2, 0x26, 0x41, 0x68, 0xF7, 0x7B, 0xA1, 0x02, 0x92, 0x63, 0xFC, 0x51, 0xC4, 0xC5, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFD, 0x22,
|
||||
0xFD, 0x26, 0xFF, 0xEC, 0xFD, 0x33, 0xFD, 0x43, 0xF4, 0x37, 0xF4, 0x37, 0xF4, 0x37, 0xFF, 0xF1,
|
||||
0xF4, 0x37, 0xFD, 0x52, 0xFF, 0xFB, 0xFD, 0x4C, 0xFD, 0x4C, 0xFD, 0x52, 0xF4, 0x37, 0xF4, 0x37,
|
||||
0xC3, 0x05, 0xC2, 0xC5, 0x6F, 0x7A, 0xFF, 0xAD, 0xFF, 0xCC, 0xF2, 0xE1, 0xC2, 0x05, 0xC2, 0x63,
|
||||
0x6B, 0xFF, 0x8C, 0xFF, 0x8C, 0xA0, 0x09, 0x92, 0xC1, 0x03, 0x12, 0x75, 0xF4, 0x5F, 0xA0, 0x09,
|
||||
0xE1, 0x44, 0xBA, 0x82, 0x9B, 0xBC, 0xFF, 0xF4, 0xFF, 0xF7, 0xF4, 0x42, 0xFF, 0xFD, 0x41, 0x68,
|
||||
0xF4, 0x49, 0x21, 0x63, 0xFC, 0xA1, 0x02, 0xB2, 0x75, 0xFD, 0x41, 0x7A, 0xF4, 0x3D, 0x21, 0x63,
|
||||
0xFC, 0x21, 0x99, 0xFD, 0x41, 0xBC, 0xF4, 0x33, 0x42, 0xC5, 0x70, 0xFF, 0xFC, 0xF4, 0x2F, 0x42,
|
||||
0x63, 0x74, 0xFF, 0xEB, 0xF4, 0x28, 0x21, 0x70, 0xF9, 0xA3, 0x02, 0xB2, 0xC4, 0x61, 0x65, 0xE8,
|
||||
0xEF, 0xFD, 0x41, 0x6A, 0xF4, 0x15, 0xA2, 0x02, 0xB2, 0x61, 0x6F, 0xFC, 0xFC, 0xA0, 0x09, 0xB3,
|
||||
0x21, 0x63, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x7A, 0xF4, 0x21, 0x72, 0xFD, 0x21,
|
||||
0x74, 0xFD, 0xA2, 0x02, 0x71, 0x63, 0x73, 0xF4, 0xFD, 0x41, 0x87, 0xF3, 0xDA, 0xC2, 0x02, 0xB2,
|
||||
0x68, 0x7A, 0xF6, 0xB1, 0xF6, 0xB1, 0x42, 0xBA, 0xBC, 0xF8, 0x59, 0xF8, 0x59, 0xA1, 0x02, 0xB2,
|
||||
0xC5, 0xF9, 0xC2, 0x02, 0xB2, 0x6D, 0x7A, 0xF6, 0x9C, 0xF6, 0x9C, 0xD5, 0x09, 0x72, 0xC5, 0x6D,
|
||||
0x72, 0x77, 0x6F, 0x75, 0x7A, 0xC4, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6E,
|
||||
0x70, 0x73, 0x74, 0xFF, 0x76, 0xFF, 0x8A, 0xFF, 0xAE, 0xFF, 0xBB, 0xFF, 0xD7, 0xFE, 0x21, 0xF3,
|
||||
0x52, 0xFF, 0xDE, 0xF3, 0x72, 0xFF, 0xE2, 0xFF, 0xF2, 0xF3, 0x72, 0xF3, 0x72, 0xF3, 0x72, 0xF3,
|
||||
0x72, 0xF3, 0x72, 0xF3, 0x72, 0xF3, 0x72, 0xF3, 0x72, 0xFF, 0xF7, 0xF3, 0x72, 0xC2, 0x02, 0x92,
|
||||
0x6D, 0x7A, 0xF0, 0xF6, 0xF0, 0xF6, 0x51, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6B,
|
||||
0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xB5, 0xFB, 0xF8, 0xFB, 0xFC, 0xF3, 0x0D,
|
||||
0xFC, 0x09, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D,
|
||||
0xFC, 0x22, 0xFF, 0xF7, 0xF3, 0x0D, 0xF3, 0x0D, 0xF3, 0x0D, 0xA0, 0x02, 0xF1, 0xA1, 0x02, 0xD2,
|
||||
0x6B, 0xFD, 0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xFF, 0xFB, 0xF2, 0xF7, 0xF2, 0xF7, 0xF2, 0xF7, 0xA0,
|
||||
0x0A, 0x12, 0xA0, 0x0A, 0xC2, 0x21, 0x63, 0xFD, 0xA1, 0x0A, 0x32, 0x79, 0xFD, 0x21, 0xBC, 0xFB,
|
||||
0x21, 0xC5, 0xFD, 0xA1, 0x0B, 0x42, 0x75, 0xFD, 0xA0, 0x0A, 0xE3, 0xA0, 0x0B, 0x42, 0x24, 0xBA,
|
||||
0x82, 0xBC, 0x9B, 0xE1, 0xF5, 0xFA, 0xFD, 0xA0, 0x0A, 0x32, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD,
|
||||
0xA1, 0x0B, 0x62, 0x75, 0xFD, 0x21, 0x74, 0xF2, 0x21, 0x61, 0xFD, 0x21, 0x6D, 0xFD, 0x21, 0x6B,
|
||||
0xE9, 0x21, 0x6C, 0xE6, 0xA0, 0x0A, 0x53, 0xA4, 0x0B, 0x62, 0x61, 0x75, 0x79, 0x7A, 0xF4, 0xF7,
|
||||
0xFA, 0xFD, 0xA1, 0x0B, 0x62, 0x6D, 0xD5, 0xA0, 0x02, 0xD1, 0xA0, 0x0B, 0x13, 0x41, 0x6D, 0xFF,
|
||||
0xA5, 0x41, 0x6A, 0xFF, 0xA1, 0x22, 0x72, 0x6F, 0xB3, 0xFC, 0xC6, 0x02, 0xD2, 0x61, 0x67, 0x69,
|
||||
0x6A, 0x6C, 0x77, 0xFF, 0xF0, 0xFF, 0xAE, 0xFF, 0xF3, 0xFF, 0x98, 0xFF, 0x98, 0xFF, 0xFB, 0x41,
|
||||
0x87, 0xFF, 0x9C, 0xC2, 0x0B, 0x62, 0x68, 0x7A, 0xFF, 0x67, 0xFF, 0x67, 0xA0, 0x0B, 0x81, 0x22,
|
||||
0xBA, 0xBC, 0xFD, 0xFD, 0xC2, 0x0B, 0x62, 0xC5, 0x7A, 0xFF, 0xFB, 0xFF, 0x56, 0xA0, 0x0B, 0x62,
|
||||
0xC1, 0x0B, 0x62, 0x7A, 0xFF, 0x7B, 0xD5, 0x09, 0xF2, 0xC5, 0x6D, 0x72, 0x75, 0x79, 0x7A, 0xC4,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6E, 0x70, 0x73, 0x74, 0x77, 0xFF, 0x78,
|
||||
0xFF, 0x8A, 0xFF, 0xA1, 0xFF, 0xAC, 0xFF, 0xB1, 0xFF, 0xC4, 0xFF, 0xD9, 0xFF, 0xDD, 0xFF, 0xEE,
|
||||
0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7, 0xFF, 0xF7,
|
||||
0xFF, 0xF7, 0xFF, 0xFA, 0xFF, 0xF7, 0xFF, 0xF7, 0x41, 0x87, 0xF2, 0x01, 0x41, 0x7A, 0xFF, 0x13,
|
||||
0xA1, 0x06, 0xC2, 0x72, 0xFC, 0xC2, 0x06, 0xC2, 0x68, 0x7A, 0xF6, 0xBA, 0xF6, 0xBA, 0xA0, 0x0A,
|
||||
0x84, 0x21, 0x73, 0xFD, 0x21, 0x6B, 0xFD, 0x41, 0x7A, 0xFE, 0xE3, 0xA1, 0x06, 0xC2, 0x72, 0xFC,
|
||||
0xC2, 0x06, 0xC2, 0x6C, 0x72, 0xF6, 0x9F, 0xF6, 0x9F, 0x41, 0x68, 0xFE, 0xD1, 0xA1, 0x06, 0xC2,
|
||||
0x63, 0xFC, 0x41, 0xBC, 0xFE, 0xC8, 0xC2, 0x06, 0xC2, 0xC5, 0x7A, 0xFF, 0xFC, 0xF6, 0x89, 0xC3,
|
||||
0x06, 0xC2, 0x63, 0x6D, 0x7A, 0xFF, 0xEA, 0xF6, 0x80, 0xF6, 0x80, 0xC2, 0x06, 0xC2, 0x6B, 0x72,
|
||||
0xF6, 0x74, 0xF6, 0x74, 0x53, 0xC5, 0x64, 0xC4, 0x62, 0x63, 0x65, 0x66, 0x67, 0x68, 0x6B, 0x6C,
|
||||
0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFE, 0xAE, 0xFF, 0x62, 0xFF, 0xA4, 0xFF, 0xAC,
|
||||
0xFF, 0xB1, 0xFF, 0xC0, 0xF6, 0xE3, 0xFF, 0xC7, 0xF6, 0xE3, 0xFF, 0xCC, 0xF6, 0xE3, 0xF6, 0xE3,
|
||||
0xF6, 0xE3, 0xFF, 0xD9, 0xFF, 0xE2, 0xFF, 0xEB, 0xFF, 0xF7, 0xF6, 0xE3, 0xF6, 0xE3, 0x21, 0x65,
|
||||
0xC6, 0x21, 0x7A, 0xFD, 0xC2, 0x05, 0xC2, 0x6F, 0x72, 0xFE, 0x32, 0xFF, 0xFD, 0xC1, 0x03, 0x52,
|
||||
0x72, 0xF6, 0x9A, 0x41, 0x9B, 0xF4, 0x6B, 0x41, 0x7A, 0xF6, 0x18, 0x21, 0x72, 0xFC, 0x41, 0x72,
|
||||
0xF4, 0x60, 0xC6, 0x03, 0x32, 0x65, 0x75, 0xC5, 0x62, 0x6D, 0x74, 0xFF, 0xEB, 0xF1, 0x95, 0xFF,
|
||||
0xF1, 0xFF, 0xF9, 0xEF, 0x01, 0xFF, 0xFC, 0x21, 0x7A, 0xEB, 0xC2, 0x05, 0xC2, 0x6F, 0x7A, 0xFF,
|
||||
0xFD, 0xEF, 0xE7, 0x41, 0x65, 0xF1, 0x74, 0xA1, 0x03, 0x32, 0x69, 0xFC, 0x21, 0x62, 0xFB, 0xC2,
|
||||
0x05, 0xC2, 0x75, 0x7A, 0xFF, 0xFD, 0xEF, 0xD2, 0xC2, 0x05, 0xC2, 0x63, 0x77, 0xFB, 0x44, 0xEF,
|
||||
0xC9, 0xC2, 0x02, 0x92, 0x6B, 0x72, 0xEE, 0xC2, 0xEE, 0xC2, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64,
|
||||
0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xF9, 0xC4, 0xF9, 0xC8,
|
||||
0xFC, 0x8E, 0xF9, 0xD5, 0xF9, 0xE5, 0xF0, 0xD9, 0xF0, 0xD9, 0xF0, 0xD9, 0xF0, 0xD9, 0xF0, 0xD9,
|
||||
0xF0, 0xD9, 0xF0, 0xD9, 0xF9, 0xEE, 0xF9, 0xEE, 0xFF, 0xF7, 0xF0, 0xD9, 0xF0, 0xD9, 0xA1, 0x05,
|
||||
0xC2, 0x79, 0xCC, 0x41, 0x87, 0xF4, 0xA8, 0x44, 0x82, 0x9B, 0xBA, 0xBC, 0xF4, 0xA4, 0xF4, 0xA4,
|
||||
0xF4, 0xA4, 0xF4, 0xA4, 0x43, 0x9B, 0xBA, 0xBC, 0xF0, 0x89, 0xF0, 0x89, 0xF0, 0x89, 0xCD, 0x00,
|
||||
0x91, 0xC4, 0xC5, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x70, 0x73, 0x77, 0xF9, 0x70,
|
||||
0xFF, 0xF6, 0xF9, 0x81, 0xF9, 0x91, 0xF0, 0x85, 0xF0, 0x85, 0xF0, 0x85, 0xF0, 0x85, 0xF0, 0x85,
|
||||
0xF0, 0x85, 0xF0, 0x85, 0xF9, 0x9A, 0xF0, 0x85, 0xC2, 0x00, 0x91, 0x68, 0x7A, 0xF4, 0x63, 0xF4,
|
||||
0x63, 0x44, 0xBA, 0x82, 0x9B, 0xBC, 0xEE, 0x32, 0xF0, 0x4C, 0xF0, 0x4C, 0xFA, 0x05, 0xC1, 0x00,
|
||||
0x71, 0x72, 0xFB, 0x0E, 0xD0, 0x00, 0x91, 0xC5, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6C, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xED, 0xF9, 0x2A, 0xF9, 0x3B, 0xF9, 0x4B, 0xF0,
|
||||
0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF9,
|
||||
0x54, 0xF0, 0x3F, 0xFF, 0xFA, 0xF4, 0x47, 0xC1, 0x00, 0x91, 0x7A, 0xF4, 0x14, 0xD1, 0x02, 0x41,
|
||||
0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77,
|
||||
0x7A, 0xFF, 0x66, 0xFF, 0x6A, 0xFF, 0x81, 0xFF, 0xAB, 0xFF, 0xC7, 0xEE, 0xE4, 0xEE, 0xE4, 0xEE,
|
||||
0xE4, 0xEE, 0xE4, 0xEE, 0xE4, 0xEE, 0xE4, 0xEE, 0xE4, 0xFF, 0xFA, 0xFF, 0xFA, 0xEE, 0xE4, 0xEE,
|
||||
0xE4, 0xEE, 0xE4, 0x53, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70,
|
||||
0x72, 0x73, 0x74, 0x75, 0x77, 0x7A, 0x6F, 0xFB, 0x61, 0xF8, 0x99, 0xF4, 0x3E, 0xFB, 0x68, 0xFB,
|
||||
0xCD, 0xF4, 0x3E, 0xF4, 0x3E, 0xF4, 0x3E, 0xF4, 0x3E, 0xF4, 0x3E, 0xFB, 0xD9, 0xFE, 0x91, 0xFE,
|
||||
0xC7, 0xFE, 0xDC, 0xFA, 0x20, 0xFE, 0xE5, 0xFF, 0x2B, 0xFA, 0x32, 0xFF, 0xCA, 0x21, 0x65, 0xC6,
|
||||
0xD8, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0xEF, 0x09, 0xEF, 0x0D, 0xFA,
|
||||
0xEA, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xF3, 0x40, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xFF,
|
||||
0xFD, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF,
|
||||
0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xEF, 0x1D, 0xA0, 0x06, 0x11, 0x21, 0x87,
|
||||
0xFD, 0x41, 0x6F, 0xEF, 0xA8, 0x21, 0x69, 0xFC, 0xA1, 0x06, 0x11, 0x6D, 0xFD, 0x23, 0x9B, 0xBA,
|
||||
0xBC, 0xFB, 0xEE, 0xEE, 0x42, 0x85, 0x99, 0xF3, 0x94, 0xF3, 0x94, 0xA0, 0x05, 0x92, 0xA1, 0x05,
|
||||
0x72, 0x7A, 0xFD, 0x21, 0x63, 0xFB, 0xA2, 0x03, 0xA2, 0xC4, 0x6F, 0xEE, 0xFD, 0x44, 0x82, 0x9B,
|
||||
0xBA, 0xBC, 0xFF, 0xF9, 0xF0, 0x56, 0xF0, 0x56, 0xF0, 0x56, 0x41, 0x67, 0xF3, 0x6E, 0x21, 0x7A,
|
||||
0xFC, 0xA1, 0x01, 0xB2, 0x75, 0xFD, 0x41, 0x87, 0xF3, 0x62, 0x41, 0x6F, 0xF3, 0x5E, 0x22, 0xC4,
|
||||
0x73, 0xF8, 0xFC, 0x41, 0x84, 0xF3, 0x55, 0x42, 0xC5, 0x6E, 0xFF, 0xFC, 0xF3, 0x51, 0x41, 0xBA,
|
||||
0xF3, 0x4A, 0x42, 0xC5, 0x7A, 0xFF, 0xFC, 0xF3, 0x46, 0x21, 0x64, 0xB2, 0x22, 0x85, 0x99, 0xAF,
|
||||
0xFD, 0x21, 0x7A, 0xAA, 0x22, 0x63, 0x74, 0xA7, 0xA7, 0x41, 0x6E, 0xFF, 0xA2, 0xA4, 0x0B, 0x93,
|
||||
0xC4, 0x65, 0x75, 0x79, 0xEF, 0xF4, 0xF7, 0xFC, 0xA4, 0x01, 0x61, 0x61, 0x6F, 0x79, 0x7A, 0xC6,
|
||||
0xCF, 0xDA, 0xF5, 0xC3, 0x05, 0xC2, 0x6E, 0x68, 0x7A, 0xFA, 0x45, 0xED, 0x8E, 0xED, 0x8E, 0xC2,
|
||||
0x05, 0xC2, 0xC5, 0x7A, 0xF7, 0x88, 0xED, 0x82, 0xA0, 0x0C, 0x52, 0x41, 0xBC, 0xF2, 0xA0, 0xC4,
|
||||
0x05, 0xC2, 0x74, 0xC5, 0x6D, 0x7A, 0xFF, 0xF9, 0xFF, 0xFC, 0xF4, 0xBA, 0xED, 0x72, 0x41, 0x68,
|
||||
0xF2, 0x8D, 0xC2, 0x05, 0xC2, 0x63, 0x7A, 0xFF, 0xFC, 0xED, 0x5F, 0xC1, 0x05, 0xC2, 0x6B, 0xED,
|
||||
0x56, 0xC1, 0x05, 0xC2, 0x77, 0xFA, 0x07, 0xD1, 0x00, 0x81, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xF9, 0xFD, 0xF7, 0x35, 0xF2,
|
||||
0xDA, 0xFF, 0xBC, 0xFF, 0xC8, 0xF2, 0xDA, 0xF2, 0xDA, 0xF2, 0xDA, 0xF2, 0xDA, 0xF2, 0xDA, 0xF2,
|
||||
0xDA, 0xF2, 0xDA, 0xFF, 0xD8, 0xFF, 0xEB, 0xFF, 0xF4, 0xF2, 0xDA, 0xFF, 0xFA, 0xC1, 0x00, 0x81,
|
||||
0x62, 0xF2, 0xA4, 0x41, 0x7A, 0xED, 0x5C, 0x21, 0x72, 0xFC, 0x21, 0x74, 0xFD, 0x21, 0x73, 0xFD,
|
||||
0xC1, 0x01, 0xB2, 0x7A, 0xED, 0xC0, 0xA0, 0x0C, 0x23, 0x21, 0x6D, 0xFD, 0xD5, 0x00, 0xF2, 0xC5,
|
||||
0x6C, 0x72, 0x65, 0x69, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E, 0x6F, 0x70,
|
||||
0x73, 0x74, 0x75, 0x77, 0xFF, 0x01, 0xFF, 0x15, 0xFF, 0x5C, 0xFF, 0xAB, 0xFF, 0xE1, 0xEF, 0x5A,
|
||||
0xEF, 0x79, 0xEF, 0x89, 0xED, 0x43, 0xED, 0x43, 0xED, 0x43, 0xED, 0x43, 0xED, 0x43, 0xED, 0x43,
|
||||
0xED, 0x43, 0xFF, 0xF1, 0xED, 0x43, 0xFF, 0xF4, 0xED, 0x43, 0xFF, 0xFD, 0xED, 0x43, 0x41, 0x6F,
|
||||
0xED, 0x01, 0xC2, 0x01, 0xB2, 0x75, 0x7A, 0xF2, 0x36, 0xFF, 0xFC, 0x41, 0x74, 0xF2, 0x2D, 0x21,
|
||||
0x99, 0xFC, 0x41, 0x70, 0xF2, 0x26, 0x41, 0x6E, 0xF2, 0x22, 0x21, 0x69, 0xFC, 0x21, 0x62, 0xFD,
|
||||
0x41, 0x69, 0xF2, 0x18, 0x41, 0xC4, 0xFE, 0xB2, 0xC2, 0x02, 0x41, 0x65, 0x77, 0xFF, 0xFC, 0xF2,
|
||||
0x10, 0xA6, 0x01, 0xB2, 0xC4, 0x61, 0x6F, 0x75, 0x77, 0x7A, 0xDE, 0xE1, 0xEC, 0xDA, 0xEF, 0xF7,
|
||||
0xA0, 0x0C, 0x02, 0xA1, 0x0B, 0xC2, 0x72, 0xFD, 0xA1, 0x0B, 0xC2, 0x6D, 0xF8, 0x22, 0x61, 0x65,
|
||||
0xF6, 0xFB, 0xA1, 0x0C, 0x92, 0x69, 0xFB, 0xC1, 0x05, 0xC2, 0x6B, 0xF4, 0x06, 0xC1, 0x05, 0xC2,
|
||||
0x63, 0xFE, 0xF1, 0xC2, 0x05, 0xC2, 0xC5, 0x7A, 0xFE, 0xD8, 0xEC, 0x4E, 0xC2, 0x05, 0xC2, 0x63,
|
||||
0x6B, 0xFE, 0xE2, 0xEC, 0x45, 0xD1, 0x00, 0x81, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68,
|
||||
0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xF8, 0xEF, 0xF6, 0x27, 0xF1, 0xCC, 0xF5,
|
||||
0x81, 0xFE, 0xBA, 0xF1, 0xCC, 0xF1, 0xCC, 0xF1, 0xCC, 0xF1, 0xCC, 0xF1, 0xCC, 0xFF, 0xE2, 0xFF,
|
||||
0xE8, 0xFF, 0xEE, 0xF5, 0x93, 0xFF, 0xF7, 0xF1, 0xCC, 0xF1, 0xCC, 0xA0, 0x0C, 0x72, 0x43, 0xBA,
|
||||
0x9B, 0xBC, 0xEC, 0xC2, 0xEE, 0x65, 0xFF, 0xFD, 0x41, 0x75, 0xEF, 0x55, 0xC1, 0x01, 0xB2, 0x65,
|
||||
0xF8, 0x35, 0x41, 0x73, 0xEC, 0x3D, 0x42, 0x63, 0x6D, 0xFE, 0xDD, 0xFE, 0xF0, 0xD8, 0x00, 0xF2,
|
||||
0x69, 0x72, 0x7A, 0x65, 0xC5, 0xC4, 0x61, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C,
|
||||
0x6D, 0x6E, 0x6F, 0x70, 0x73, 0x74, 0x75, 0x77, 0xFF, 0x35, 0xFF, 0x64, 0xFF, 0x85, 0xFF, 0xA8,
|
||||
0xFF, 0xE1, 0xEE, 0x49, 0xFF, 0xEB, 0xFF, 0xEF, 0xEE, 0x68, 0xEE, 0x78, 0xEC, 0x32, 0xEC, 0x32,
|
||||
0xEC, 0x32, 0xEC, 0x32, 0xEE, 0x85, 0xEC, 0x32, 0xEC, 0x32, 0xEC, 0x32, 0xFF, 0xF5, 0xEC, 0x32,
|
||||
0xFE, 0xE3, 0xEC, 0x32, 0xFF, 0xF9, 0xEC, 0x32, 0x41, 0x82, 0xF1, 0x48, 0x21, 0xC5, 0xFC, 0x21,
|
||||
0x68, 0xFD, 0xA1, 0x02, 0x01, 0x63, 0xFD, 0xA0, 0x0B, 0xE2, 0x21, 0x6E, 0xFD, 0x21, 0x9B, 0xFD,
|
||||
0xA0, 0x0C, 0xB2, 0x21, 0x6F, 0xFD, 0x22, 0xC5, 0x65, 0xF7, 0xFD, 0x41, 0x7A, 0xEE, 0xD2, 0xC1,
|
||||
0x06, 0x11, 0x72, 0xEE, 0x0F, 0xC2, 0x02, 0x01, 0x68, 0x7A, 0xFF, 0xFA, 0xFD, 0x46, 0xA0, 0x02,
|
||||
0x01, 0x41, 0x6B, 0xF1, 0xE6, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0xB3,
|
||||
0xFD, 0x41, 0x72, 0xEC, 0x6C, 0x21, 0x74, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x69, 0xFD, 0xA2, 0x02,
|
||||
0x01, 0xC3, 0x6E, 0xF0, 0xFD, 0x41, 0x6D, 0xF0, 0xCC, 0xC2, 0x02, 0x01, 0x61, 0x72, 0xFF, 0xFC,
|
||||
0xFD, 0x12, 0x41, 0x68, 0xEB, 0xEE, 0xA1, 0x02, 0x01, 0x63, 0xFC, 0xA0, 0x0C, 0xD2, 0x21, 0xBC,
|
||||
0xFD, 0x21, 0x99, 0xBD, 0x21, 0xC4, 0xFD, 0xA0, 0x07, 0x02, 0x23, 0xC5, 0x74, 0x7A, 0xF4, 0xFA,
|
||||
0xFD, 0xA0, 0x05, 0xE2, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0xA1, 0x04, 0x32, 0x73, 0xFD, 0x21,
|
||||
0x6D, 0xFB, 0x21, 0x65, 0xFD, 0xC2, 0x02, 0x01, 0x69, 0x7A, 0xFF, 0xFD, 0xFC, 0xD6, 0x52, 0xC4,
|
||||
0xC5, 0x62, 0x64, 0x74, 0x6C, 0x61, 0x63, 0x66, 0x67, 0x68, 0x6B, 0x6D, 0x70, 0x72, 0x73, 0x77,
|
||||
0x7A, 0xFC, 0xD0, 0xFC, 0xDF, 0xFD, 0xFE, 0xFF, 0x0F, 0xFF, 0x64, 0xFF, 0x78, 0xFF, 0x7D, 0xFF,
|
||||
0x87, 0xFF, 0x90, 0xFF, 0xB0, 0xFF, 0x90, 0xFF, 0xBB, 0xFF, 0x90, 0xFF, 0xC8, 0xFF, 0xDC, 0xFF,
|
||||
0xF7, 0xFF, 0x90, 0xFF, 0x90, 0x41, 0x77, 0xF1, 0x42, 0x21, 0x6F, 0xFC, 0x21, 0x6B, 0xFD, 0x21,
|
||||
0x73, 0xFD, 0xA1, 0x00, 0x61, 0x61, 0xFD, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0xFB, 0xE9,
|
||||
0x23, 0xE9, 0x23, 0xE9, 0x23, 0xE9, 0x23, 0xC1, 0x00, 0x71, 0x72, 0xE9, 0x9C, 0x41, 0x72, 0xF9,
|
||||
0x4A, 0x41, 0x64, 0xE9, 0x92, 0xA2, 0x00, 0x71, 0x62, 0x6D, 0xF8, 0xFC, 0x41, 0x6B, 0xE9, 0x9A,
|
||||
0x21, 0x6D, 0xFC, 0x21, 0x79, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x42, 0x85, 0x99, 0xF0,
|
||||
0xCF, 0xF0, 0xCF, 0x41, 0x7A, 0xF0, 0xC8, 0x21, 0x63, 0xFC, 0xC7, 0x06, 0xE2, 0x6B, 0x6D, 0x6F,
|
||||
0x70, 0xC4, 0x65, 0x79, 0xFF, 0xCD, 0xEF, 0x91, 0xFF, 0xDB, 0xFF, 0xEF, 0xFF, 0xF2, 0xFF, 0xFD,
|
||||
0xF0, 0xC1, 0x21, 0x82, 0xE8, 0x21, 0xC5, 0xFD, 0x21, 0xB3, 0xFD, 0xC1, 0x00, 0x51, 0x68, 0xE8,
|
||||
0xBF, 0x41, 0x6B, 0xEC, 0x49, 0x21, 0x6F, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x82, 0xFD, 0xA0, 0x0C,
|
||||
0xF3, 0xA1, 0x02, 0x22, 0x6E, 0xFD, 0xC5, 0x02, 0x02, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xEA, 0xDA,
|
||||
0xFF, 0xFB, 0xEA, 0xDA, 0xEA, 0xDA, 0xEA, 0xDA, 0x41, 0x6B, 0xEA, 0x67, 0xA0, 0x0D, 0x22, 0x21,
|
||||
0x6C, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x73, 0xFD, 0x42, 0x72, 0x73, 0xEA, 0x09, 0xEA, 0x09, 0x21,
|
||||
0x65, 0xF9, 0x21, 0x65, 0xFD, 0xA0, 0x0D, 0x43, 0x21, 0x72, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x65,
|
||||
0xFD, 0x21, 0x70, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD, 0x26, 0xC5, 0x72, 0x63, 0x65, 0x70,
|
||||
0x7A, 0xB1, 0xBC, 0xCE, 0xDB, 0xE8, 0xFD, 0x41, 0x74, 0xEA, 0x2E, 0x21, 0x65, 0xFC, 0xA1, 0x0D,
|
||||
0x72, 0x73, 0xFD, 0x21, 0x87, 0xFB, 0x41, 0x9B, 0xEB, 0x13, 0x21, 0xC5, 0xFC, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x22, 0xC4, 0x63, 0xF0, 0xFD, 0x21, 0x99, 0xFB, 0x41, 0x77, 0xEA, 0xF8, 0x21,
|
||||
0x72, 0xFC, 0x41, 0x6E, 0xEC, 0xDA, 0x23, 0xC4, 0x65, 0x6F, 0xF2, 0xF9, 0xFC, 0xC1, 0x03, 0xA2,
|
||||
0x6B, 0xE9, 0xA4, 0x41, 0x63, 0xEC, 0x06, 0x45, 0x82, 0x9B, 0xBA, 0xBC, 0x84, 0xFF, 0xF6, 0xEB,
|
||||
0xFC, 0xEB, 0xFC, 0xEB, 0xFC, 0xFF, 0xFC, 0xC1, 0x07, 0xE2, 0x75, 0xEA, 0x39, 0xA0, 0x0E, 0x83,
|
||||
0x21, 0x64, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0xA1, 0x07, 0xE2, 0x72, 0xFD, 0x44, 0xBA,
|
||||
0x82, 0x9B, 0xBC, 0xF1, 0x2C, 0xFF, 0xE9, 0xFF, 0xFB, 0xF1, 0x52, 0x41, 0x77, 0xEA, 0x15, 0xA1,
|
||||
0x05, 0xC2, 0x61, 0xFC, 0xC1, 0x02, 0x22, 0x6E, 0xF1, 0x71, 0x21, 0x7A, 0xFA, 0x22, 0x63, 0x74,
|
||||
0xFD, 0xEE, 0x21, 0x99, 0xFB, 0x41, 0xBC, 0xE9, 0xFB, 0x21, 0xC5, 0xFC, 0x21, 0xB3, 0xFD, 0x41,
|
||||
0x69, 0xF1, 0x49, 0x42, 0xC5, 0x70, 0xFF, 0xF2, 0xE9, 0xED, 0x41, 0xB3, 0xE9, 0xE6, 0x44, 0xC3,
|
||||
0x61, 0x6F, 0x79, 0xFF, 0xFC, 0xE9, 0xE2, 0xE9, 0xE2, 0xE9, 0xE2, 0xA0, 0x03, 0x32, 0x44, 0xC5,
|
||||
0x62, 0x63, 0x7A, 0xFF, 0xD7, 0xFF, 0xF0, 0xF1, 0x20, 0xFF, 0xFD, 0x41, 0x67, 0xE9, 0xC5, 0x21,
|
||||
0x7A, 0xFC, 0x41, 0x65, 0xE9, 0xBE, 0xC9, 0x05, 0xC2, 0xC4, 0xC3, 0x77, 0x61, 0x65, 0x6F, 0x75,
|
||||
0x79, 0x7A, 0xFF, 0xBC, 0xFF, 0xC6, 0xFF, 0xC9, 0xFF, 0xCD, 0xF1, 0x1C, 0xFF, 0xE8, 0xFF, 0xF9,
|
||||
0xFF, 0xF5, 0xFF, 0xFC, 0x42, 0x6A, 0x72, 0xE9, 0x9F, 0xE9, 0x9C, 0x21, 0xB3, 0xF9, 0x41, 0x6A,
|
||||
0xE9, 0x92, 0x43, 0x69, 0x6A, 0x72, 0xE9, 0x8E, 0xE9, 0x8E, 0xF0, 0xDC, 0xA3, 0x05, 0xC2, 0xC3,
|
||||
0x61, 0x6F, 0xEF, 0xF2, 0xF6, 0x41, 0x70, 0xF2, 0x81, 0xA1, 0x01, 0x61, 0x6C, 0xFC, 0xA0, 0x0D,
|
||||
0xD5, 0xA1, 0x01, 0x92, 0x73, 0xFD, 0x41, 0x68, 0xE7, 0xBD, 0xA1, 0x01, 0x92, 0x63, 0xFC, 0xC2,
|
||||
0x01, 0x92, 0x63, 0x7A, 0xFF, 0xF7, 0xED, 0xDC, 0xC1, 0x01, 0x92, 0x6B, 0xE8, 0xD0, 0xD2, 0x01,
|
||||
0x61, 0x6A, 0x6B, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6C, 0x6D, 0x70, 0x72, 0x73,
|
||||
0x74, 0x77, 0x7A, 0xEB, 0xFF, 0xFF, 0xE3, 0xF0, 0xF6, 0xF0, 0xFA, 0xE8, 0xD5, 0xF1, 0x07, 0xF1,
|
||||
0x17, 0xE8, 0xD5, 0xE8, 0xD5, 0xE8, 0xD5, 0xE8, 0xD5, 0xE8, 0xD5, 0xFF, 0xEC, 0xF1, 0x30, 0xFF,
|
||||
0xF1, 0xFF, 0xFA, 0xE8, 0xD5, 0xE8, 0xD5, 0x42, 0x75, 0x6E, 0xEB, 0xC6, 0xED, 0xFA, 0x41, 0xB3,
|
||||
0xF1, 0x3E, 0x41, 0x64, 0xF1, 0x37, 0x21, 0x61, 0xFC, 0x41, 0x6A, 0xF1, 0x36, 0x41, 0x61, 0xF1,
|
||||
0x2F, 0x45, 0xC3, 0x69, 0x6F, 0x72, 0x73, 0xFF, 0xED, 0xFF, 0xF5, 0xFF, 0xF8, 0xFF, 0xFC, 0xF1,
|
||||
0x52, 0x41, 0x63, 0xF1, 0x18, 0x21, 0x6F, 0xFC, 0x21, 0x68, 0xFD, 0x42, 0x6D, 0x64, 0xF1, 0x0E,
|
||||
0xEA, 0xDC, 0x41, 0x67, 0xF1, 0x07, 0x21, 0x99, 0xFC, 0x41, 0x73, 0xF1, 0x00, 0x22, 0xC4, 0x65,
|
||||
0xF9, 0xFC, 0x43, 0x69, 0x6E, 0x72, 0xF1, 0x1B, 0xF0, 0xF7, 0xFF, 0xFB, 0x41, 0x65, 0xF0, 0xF3,
|
||||
0x21, 0x69, 0xFC, 0x41, 0x77, 0xF0, 0xE6, 0x21, 0x79, 0xFC, 0x41, 0x6B, 0xF0, 0xDF, 0x21, 0x61,
|
||||
0xFC, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x42, 0x69, 0x74, 0xFF, 0xFD, 0xF0, 0xF0, 0xCA, 0x01,
|
||||
0x61, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x70, 0x72, 0x73, 0x6C, 0xFF, 0xA3, 0xFF, 0xBA, 0xFF,
|
||||
0xBD, 0xF0, 0xCB, 0xF0, 0xCB, 0xFF, 0xD4, 0xFF, 0xE2, 0xFF, 0xE9, 0xFF, 0xF9, 0xF0, 0xFC, 0x41,
|
||||
0x61, 0xF0, 0xAA, 0x41, 0x6C, 0xF0, 0xA6, 0x21, 0x61, 0xFC, 0xA0, 0x04, 0x02, 0x21, 0x6E, 0xFD,
|
||||
0xA1, 0x07, 0x73, 0x79, 0xFD, 0x21, 0x7A, 0xFB, 0x21, 0x63, 0xFD, 0x22, 0x74, 0x7A, 0xE4, 0xFD,
|
||||
0xC5, 0x01, 0x61, 0x63, 0x64, 0x70, 0x72, 0x73, 0xF0, 0x8C, 0xF0, 0x8C, 0xFF, 0xDF, 0xFF, 0xE7,
|
||||
0xFF, 0xFB, 0x41, 0x72, 0xEF, 0xB3, 0x41, 0x74, 0xEF, 0xAF, 0x43, 0x6B, 0x6D, 0x73, 0xFF, 0xF8,
|
||||
0xEF, 0xAB, 0xFF, 0xFC, 0x41, 0x69, 0xEF, 0xA1, 0x43, 0x85, 0x87, 0x99, 0xEF, 0x9D, 0xEF, 0x9D,
|
||||
0xEF, 0x9D, 0x41, 0x82, 0xEF, 0x93, 0x41, 0x6E, 0xEF, 0x8F, 0x48, 0xC4, 0xC5, 0x63, 0x65, 0x6C,
|
||||
0x6D, 0x6F, 0x73, 0xFF, 0xEE, 0xFF, 0xF8, 0xEF, 0x8B, 0xFF, 0xFC, 0xEF, 0x8B, 0xEF, 0x8B, 0xEF,
|
||||
0x8B, 0xEF, 0x8B, 0x21, 0x69, 0xE7, 0x21, 0x6E, 0xFD, 0x21, 0x65, 0xFD, 0x42, 0x6C, 0x6D, 0xFF,
|
||||
0xFD, 0xEF, 0x69, 0x42, 0x65, 0x6F, 0xFF, 0xF9, 0xF0, 0x65, 0xA0, 0x0E, 0x23, 0x21, 0x72, 0xFD,
|
||||
0xC5, 0x03, 0xA2, 0x61, 0x62, 0x65, 0x69, 0x77, 0xFF, 0xAA, 0xFF, 0xB4, 0xEF, 0x55, 0xFF, 0xF3,
|
||||
0xFF, 0xFD, 0x41, 0x77, 0xE9, 0xD5, 0x21, 0xB3, 0xFC, 0xC1, 0x05, 0xC2, 0xC5, 0xF1, 0x2E, 0xDA,
|
||||
0x07, 0x02, 0xC4, 0xC5, 0x6D, 0x6E, 0x72, 0x77, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x7A, 0xC3,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x70, 0x73, 0x74, 0xEE, 0xD1, 0xFD, 0xAF,
|
||||
0xEF, 0x1A, 0xFD, 0xC0, 0xFE, 0x17, 0xFE, 0x4D, 0xFE, 0x5A, 0xFE, 0x7F, 0xFE, 0xB8, 0xFF, 0x2F,
|
||||
0xFF, 0x71, 0xEA, 0x7E, 0xFF, 0xE1, 0xFF, 0xF7, 0xEC, 0xB2, 0xF0, 0x67, 0xFF, 0xFA, 0xEC, 0xB2,
|
||||
0xEC, 0xB2, 0xEC, 0xB2, 0xEC, 0xB2, 0xEC, 0xB2, 0xEC, 0xB2, 0xEC, 0xB2, 0xF2, 0x8B, 0xEC, 0xB2,
|
||||
0xC2, 0x02, 0x41, 0x64, 0x74, 0xE9, 0x39, 0xE9, 0x39, 0xC1, 0x02, 0x41, 0x72, 0xF2, 0x43, 0xA2,
|
||||
0x01, 0xB2, 0x7A, 0x68, 0xF1, 0xFA, 0xA0, 0x0D, 0x94, 0x21, 0x73, 0xFD, 0x21, 0x6B, 0xFD, 0xC2,
|
||||
0x01, 0xB2, 0x6C, 0x72, 0xE7, 0x71, 0xE7, 0x71, 0xC1, 0x01, 0xB2, 0x73, 0xE9, 0x4F, 0xC1, 0x03,
|
||||
0xE2, 0x6B, 0xEA, 0xB7, 0xC2, 0x01, 0xB2, 0x70, 0x6B, 0xFF, 0xFA, 0xE7, 0x5C, 0xC3, 0x01, 0xB2,
|
||||
0x63, 0x6F, 0x72, 0xE7, 0x4F, 0xED, 0x8D, 0xE9, 0x31, 0x41, 0xBC, 0xE9, 0xE4, 0x41, 0x99, 0xFE,
|
||||
0xBD, 0xA0, 0x0E, 0xD2, 0xC4, 0x03, 0xE2, 0xC4, 0x77, 0x66, 0x6D, 0xFF, 0xF9, 0xFF, 0xFD, 0xEA,
|
||||
0x91, 0xEA, 0x91, 0xC1, 0x04, 0x32, 0x75, 0xE9, 0xA8, 0x21, 0x7A, 0xFA, 0xC7, 0x01, 0xB2, 0xC5,
|
||||
0x63, 0x66, 0x6E, 0x74, 0x6F, 0x7A, 0xFF, 0xDD, 0xE9, 0x0B, 0xE9, 0x0B, 0xE9, 0x0B, 0xFF, 0xE8,
|
||||
0xFF, 0xFD, 0xE7, 0x24, 0xA0, 0x0E, 0x53, 0x41, 0x6D, 0xE8, 0xB2, 0x21, 0x6F, 0xFC, 0x4B, 0x64,
|
||||
0x66, 0x67, 0x68, 0x69, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0xE8, 0xAB, 0xE8, 0xAB, 0xE8, 0xAB,
|
||||
0xE8, 0xAB, 0xFF, 0xF6, 0xE8, 0xAB, 0xE8, 0xAB, 0xE8, 0xAB, 0xE8, 0xAB, 0xFF, 0xFD, 0xE8, 0xAB,
|
||||
0xC4, 0x01, 0xB2, 0x74, 0x63, 0x6D, 0x7A, 0xFF, 0xDE, 0xE6, 0xDC, 0xE6, 0xE0, 0xE6, 0xE0, 0xA0,
|
||||
0x0E, 0xF2, 0x41, 0x75, 0xE6, 0x41, 0xC3, 0x01, 0xB2, 0x6D, 0x61, 0x77, 0xFF, 0xF9, 0xFF, 0xFC,
|
||||
0xE6, 0xCA, 0xA0, 0x0E, 0xB2, 0x42, 0xBA, 0x9B, 0xFF, 0xFD, 0xE7, 0x18, 0xC2, 0x02, 0x92, 0x68,
|
||||
0x7A, 0xE7, 0x11, 0xE7, 0x11, 0x41, 0xBA, 0xF3, 0x20, 0x21, 0xC5, 0xFC, 0xCF, 0x09, 0x12, 0xC5,
|
||||
0x6F, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x70, 0x73, 0x74, 0x7A, 0xFF, 0xE9,
|
||||
0xED, 0x2D, 0xEF, 0xF2, 0xFF, 0xF0, 0xFF, 0xFD, 0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07,
|
||||
0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07, 0xE7, 0x07, 0xEB, 0x0F, 0xC1, 0x01, 0x92, 0x72,
|
||||
0xF0, 0x47, 0xC2, 0x01, 0x92, 0x7A, 0x68, 0xE5, 0xD6, 0xEA, 0xD9, 0xC1, 0x01, 0x92, 0x74, 0xEE,
|
||||
0x23, 0xC2, 0x01, 0x92, 0x6D, 0x7A, 0xEA, 0xCA, 0xEA, 0xCA, 0xC2, 0x01, 0x92, 0x6D, 0x77, 0xE6,
|
||||
0xB9, 0xEA, 0xC1, 0x51, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70,
|
||||
0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xA9, 0xED, 0xE1, 0xED, 0xE5, 0xFF, 0xD9, 0xFF, 0xDF, 0xFF,
|
||||
0xE8, 0xE5, 0xC0, 0xE5, 0xC0, 0xE5, 0xC0, 0xE5, 0xC0, 0xE5, 0xC0, 0xE5, 0xC0, 0xEE, 0x24, 0xFF,
|
||||
0xEE, 0xE5, 0xC0, 0xE5, 0xC0, 0xFF, 0xF7, 0x42, 0x6B, 0x77, 0xE6, 0x7C, 0xE6, 0x7C, 0x21, 0x65,
|
||||
0xF9, 0x22, 0x61, 0x69, 0xC2, 0xFD, 0x53, 0xC5, 0x64, 0xC4, 0x62, 0x63, 0x65, 0x66, 0x67, 0x68,
|
||||
0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6E, 0xFB, 0xB1, 0xFE, 0x29, 0xE7, 0xB0,
|
||||
0xE7, 0xCA, 0xFE, 0x89, 0xFE, 0x96, 0xE5, 0x99, 0xE7, 0xEC, 0xE5, 0x99, 0xFE, 0x99, 0xFE, 0xA2,
|
||||
0xFE, 0xAE, 0xFE, 0xB7, 0xFE, 0xE6, 0xFF, 0x2A, 0xE8, 0x30, 0xE5, 0x99, 0xFF, 0x40, 0xFF, 0xFB,
|
||||
0x41, 0x75, 0xEA, 0xA1, 0x42, 0x6E, 0x7A, 0xFF, 0xFC, 0xEA, 0x9D, 0x41, 0x75, 0xE6, 0x42, 0x21,
|
||||
0x6E, 0xFC, 0x21, 0x77, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x72, 0xFD, 0x43, 0x73, 0x77, 0x70, 0xEA,
|
||||
0x86, 0xFF, 0xE9, 0xFF, 0xFD, 0x41, 0x6F, 0xFB, 0xC6, 0x21, 0x70, 0xFC, 0x21, 0x73, 0xFD, 0x21,
|
||||
0x79, 0xFD, 0x21, 0x64, 0xFD, 0xC1, 0x01, 0x72, 0x6B, 0xE3, 0xDE, 0x44, 0x82, 0x9B, 0xBA, 0xBC,
|
||||
0xFF, 0xFA, 0xE4, 0xFD, 0xE4, 0xFD, 0xE4, 0xFD, 0x43, 0x85, 0x99, 0x87, 0xEC, 0x11, 0xEC, 0x11,
|
||||
0xE5, 0xE5, 0xA0, 0x0F, 0xB2, 0x21, 0x63, 0xFD, 0xA1, 0x04, 0x52, 0x79, 0xFD, 0x21, 0xBC, 0xFB,
|
||||
0x21, 0xC5, 0xFD, 0xA1, 0x02, 0x72, 0x75, 0xFD, 0xA0, 0x0F, 0xD3, 0x44, 0xBA, 0x82, 0xBC, 0x9B,
|
||||
0xFE, 0xA7, 0xFF, 0xF8, 0xFF, 0xFD, 0xE5, 0xC2, 0x41, 0x68, 0xE7, 0xD3, 0x21, 0x63, 0xFC, 0xA1,
|
||||
0x02, 0x92, 0x75, 0xFD, 0xA0, 0x0F, 0x13, 0x21, 0x72, 0xFD, 0xA0, 0x10, 0x03, 0x21, 0x74, 0xFD,
|
||||
0xA1, 0x0F, 0x13, 0x61, 0xFD, 0x21, 0x74, 0xFB, 0x21, 0x6F, 0xEC, 0xA3, 0x02, 0x92, 0x62, 0x73,
|
||||
0x7A, 0xEC, 0xFA, 0xFD, 0x41, 0x74, 0xE7, 0xA7, 0x21, 0x61, 0xFC, 0x21, 0x6D, 0xFD, 0x41, 0x6B,
|
||||
0xE7, 0x9D, 0x41, 0x6C, 0xE7, 0x99, 0xA0, 0x0F, 0x43, 0xA4, 0x02, 0x92, 0x61, 0x75, 0x79, 0x7A,
|
||||
0xF2, 0xF5, 0xF9, 0xFD, 0xC1, 0x02, 0x92, 0x6D, 0xE7, 0x87, 0xA0, 0x10, 0x32, 0x21, 0x75, 0xFD,
|
||||
0x21, 0x6B, 0xFD, 0xA1, 0x01, 0x71, 0x73, 0xFD, 0x41, 0x6D, 0xFF, 0x7A, 0x41, 0x6A, 0xFF, 0x76,
|
||||
0x42, 0x72, 0x6F, 0xFF, 0x88, 0xFF, 0xFC, 0xC6, 0x01, 0x72, 0x61, 0x67, 0x69, 0x6A, 0x6C, 0x77,
|
||||
0xFF, 0x6B, 0xFF, 0x81, 0xFF, 0xF1, 0xFF, 0x6B, 0xFF, 0x6B, 0xFF, 0xF9, 0x41, 0x6E, 0xE5, 0x37,
|
||||
0x21, 0x6F, 0xFC, 0x41, 0x63, 0xE5, 0x30, 0x41, 0x70, 0xEF, 0xF5, 0x23, 0x67, 0x6B, 0x6C, 0xF5,
|
||||
0xF8, 0xFC, 0x41, 0x7A, 0xE5, 0x21, 0x41, 0x72, 0xE5, 0x1D, 0x21, 0x65, 0xFC, 0x22, 0x67, 0x6D,
|
||||
0xF5, 0xFD, 0xA0, 0x10, 0x73, 0x21, 0x77, 0xFD, 0x21, 0x99, 0xFD, 0x21, 0xC4, 0xFD, 0xC2, 0x02,
|
||||
0x92, 0x69, 0x7A, 0xFF, 0xFD, 0xE4, 0xFF, 0xD9, 0x09, 0x12, 0xC4, 0xC5, 0x6D, 0x6F, 0x72, 0x75,
|
||||
0x79, 0x7A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6E, 0x70,
|
||||
0x73, 0x74, 0x77, 0xFF, 0x11, 0xFF, 0x34, 0xFF, 0x48, 0xFF, 0x64, 0xFF, 0x82, 0xFF, 0x8D, 0xFF,
|
||||
0x9C, 0xFF, 0xB0, 0xFF, 0xD4, 0xE4, 0xFC, 0xED, 0xF8, 0xEE, 0x08, 0xFF, 0xE6, 0xE4, 0xFC, 0xF0,
|
||||
0xB1, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xE4, 0xFC, 0xFF,
|
||||
0xF7, 0xE4, 0xFC, 0xE4, 0xFC, 0xC1, 0x00, 0x71, 0x7A, 0xE2, 0x8E, 0xA1, 0x01, 0x92, 0x72, 0xFA,
|
||||
0xC1, 0x00, 0x71, 0x72, 0xF0, 0x54, 0xA0, 0x10, 0x52, 0x21, 0x65, 0xFD, 0xC3, 0x10, 0x32, 0x69,
|
||||
0x61, 0x77, 0xFF, 0xFD, 0xE7, 0xD2, 0xE7, 0xD2, 0x21, 0x77, 0xF4, 0xC3, 0x01, 0x92, 0x68, 0x69,
|
||||
0x7A, 0xFF, 0xE5, 0xFF, 0xFD, 0xE8, 0x90, 0xA0, 0x0F, 0x74, 0x21, 0x73, 0xFD, 0x21, 0x6B, 0xFD,
|
||||
0xC2, 0x01, 0x92, 0x6C, 0x72, 0xE8, 0x7B, 0xE8, 0x7B, 0xC1, 0x01, 0x92, 0x6B, 0xE8, 0x72, 0xC3,
|
||||
0x01, 0x92, 0x63, 0x6D, 0x7A, 0xFA, 0x87, 0xE8, 0x6C, 0xE8, 0x6C, 0xC1, 0x00, 0x71, 0x61, 0xE3,
|
||||
0x90, 0xC2, 0x01, 0x92, 0x6B, 0x72, 0xE8, 0x5A, 0xFF, 0xFA, 0x54, 0xC5, 0x64, 0xC4, 0x62, 0x63,
|
||||
0x65, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x75, 0x77, 0x7A, 0xFE,
|
||||
0x51, 0xFF, 0x4D, 0xEB, 0x7A, 0xFF, 0xA1, 0xFF, 0xC1, 0xFF, 0xD3, 0xE3, 0x59, 0xFD, 0x72, 0xE3,
|
||||
0x59, 0xFF, 0xD6, 0xE3, 0x59, 0xFF, 0xDF, 0xE3, 0x59, 0xFA, 0x70, 0xEB, 0xB4, 0xFF, 0xE5, 0xFF,
|
||||
0xF7, 0xE3, 0x59, 0xE3, 0x59, 0xE3, 0x59, 0xC1, 0x01, 0x92, 0x72, 0xE8, 0x14, 0x52, 0xC4, 0xC5,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6F,
|
||||
0xEB, 0x37, 0xEB, 0x3B, 0xFF, 0xFA, 0xEB, 0x48, 0xEB, 0x58, 0xE3, 0x16, 0xFD, 0x2F, 0xE3, 0x16,
|
||||
0xE3, 0x16, 0xE3, 0x16, 0xFF, 0x9C, 0xFA, 0x2D, 0xEB, 0x71, 0xFA, 0x32, 0xFF, 0x9C, 0xE3, 0x16,
|
||||
0xE3, 0x16, 0xEE, 0x5F, 0xC2, 0x00, 0x61, 0x65, 0x79, 0xFF, 0x86, 0xFF, 0xC9, 0xC3, 0x00, 0x51,
|
||||
0x61, 0x65, 0x7A, 0xFD, 0xAE, 0xFD, 0xC5, 0xFF, 0xF7, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0xC3, 0x62,
|
||||
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73,
|
||||
0x74, 0x76, 0x77, 0x78, 0x7A, 0xE3, 0x30, 0xF7, 0xEE, 0xF8, 0x4F, 0xE3, 0x44, 0xF8, 0x52, 0xE3,
|
||||
0x44, 0xF8, 0xB1, 0xE3, 0x44, 0xE3, 0x44, 0xE3, 0x44, 0xF8, 0xED, 0xE3, 0x44, 0xE3, 0x44, 0xE3,
|
||||
0x44, 0xE3, 0x44, 0xE3, 0x44, 0xFD, 0x4D, 0xE3, 0x44, 0xFF, 0xF4, 0xE3, 0x44, 0xE3, 0x44, 0xE3,
|
||||
0x44, 0xE3, 0x44, 0xE3, 0x44, 0xE3, 0x44, 0x41, 0x73, 0xFE, 0x03, 0x21, 0x6E, 0xFC, 0x21, 0x61,
|
||||
0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x69, 0xF8, 0x07, 0x41, 0x65, 0xF8, 0x03, 0x21,
|
||||
0x69, 0xFC, 0xA2, 0x02, 0x22, 0x67, 0x6E, 0xF5, 0xFD, 0xA0, 0x10, 0xA4, 0x21, 0x87, 0xFD, 0x24,
|
||||
0x82, 0x9B, 0xBA, 0xBC, 0xFA, 0xFA, 0xFA, 0xFA, 0xA0, 0x10, 0xE4, 0xC2, 0x10, 0xE4, 0x68, 0x7A,
|
||||
0xE7, 0x40, 0xE7, 0x40, 0xC2, 0x10, 0xE4, 0xC5, 0x7A, 0xEA, 0x7A, 0xE7, 0x37, 0xA0, 0x11, 0x24,
|
||||
0xA1, 0x10, 0xE4, 0x7A, 0xFD, 0xC1, 0x10, 0xE4, 0x6F, 0xF7, 0xC9, 0xC1, 0x10, 0xE4, 0x63, 0xF9,
|
||||
0x3B, 0xC2, 0x10, 0xE4, 0xC5, 0x7A, 0xEA, 0x79, 0xE7, 0x1A, 0xC2, 0x10, 0xE4, 0x63, 0x7A, 0xF9,
|
||||
0x2C, 0xE7, 0x11, 0x21, 0x74, 0xDA, 0xD3, 0x02, 0x22, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67,
|
||||
0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6A, 0x6E, 0xFF, 0xB6, 0xFF, 0xB9,
|
||||
0xFF, 0xC2, 0xFF, 0xC5, 0xFF, 0xCE, 0xFF, 0xC2, 0xFF, 0xDA, 0xFF, 0xC2, 0xFF, 0xC2, 0xFF, 0xC2,
|
||||
0xFF, 0xDF, 0xFF, 0xE5, 0xFF, 0xEB, 0xFF, 0xF4, 0xFF, 0xC2, 0xFF, 0xC2, 0xFF, 0xC2, 0xE2, 0x8A,
|
||||
0xFF, 0xFD, 0x41, 0x9B, 0xE1, 0x9F, 0x41, 0x72, 0xED, 0x16, 0xC1, 0x02, 0x41, 0x72, 0xE1, 0x97,
|
||||
0xCC, 0x02, 0x02, 0x61, 0x65, 0x75, 0xC5, 0x62, 0x64, 0x69, 0x6D, 0x6F, 0x70, 0x74, 0x77, 0xFF,
|
||||
0x62, 0xFF, 0xB6, 0xE2, 0x40, 0xFF, 0xF2, 0xFF, 0xF6, 0xE2, 0x50, 0xE2, 0x50, 0xE2, 0x50, 0xE2,
|
||||
0x50, 0xE4, 0xBC, 0xFF, 0xFA, 0xE2, 0x50, 0x21, 0x7A, 0xD9, 0xD7, 0x01, 0xF1, 0xC4, 0xC5, 0x62,
|
||||
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
|
||||
0x76, 0x77, 0x78, 0x7A, 0xE1, 0xFF, 0xE2, 0x03, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xFF, 0x2A,
|
||||
0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13,
|
||||
0xFF, 0xFD, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13, 0xE2, 0x13,
|
||||
0xE2, 0x13, 0x41, 0x7A, 0xFC, 0xD8, 0x21, 0x6F, 0xFC, 0x45, 0x63, 0x6B, 0x70, 0x77, 0x72, 0xE1,
|
||||
0x69, 0xE1, 0x4A, 0xE1, 0x4A, 0xE1, 0x4A, 0xFF, 0xFD, 0x21, 0x6F, 0xF0, 0x21, 0x6D, 0xFD, 0xC1,
|
||||
0x03, 0x32, 0x73, 0xF7, 0x2C, 0x21, 0x6D, 0xFA, 0x42, 0x65, 0x6D, 0xFF, 0xFD, 0xF7, 0x38, 0x21,
|
||||
0x64, 0xF9, 0x21, 0x65, 0xFD, 0xA0, 0x0E, 0x92, 0x21, 0x65, 0xFD, 0x21, 0xBC, 0xFD, 0x21, 0xC5,
|
||||
0xFD, 0x21, 0x64, 0xFD, 0x21, 0x85, 0xFD, 0xC3, 0x00, 0x51, 0xC4, 0x6C, 0x72, 0xFF, 0xFD, 0xDF,
|
||||
0x53, 0xDF, 0x53, 0x41, 0x62, 0xE7, 0xCB, 0xA0, 0x0D, 0x72, 0x21, 0x82, 0xFD, 0x21, 0xC5, 0xFD,
|
||||
0x21, 0xB3, 0xFD, 0xC3, 0x06, 0x12, 0x7A, 0xC5, 0x64, 0xE8, 0x6A, 0xEA, 0xC4, 0xE8, 0x06, 0xC3,
|
||||
0x05, 0xC2, 0x6E, 0x74, 0x7A, 0xED, 0x69, 0xED, 0x69, 0xE0, 0xB2, 0x51, 0x64, 0xC4, 0xC5, 0x62,
|
||||
0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xE8, 0xED,
|
||||
0x59, 0xEA, 0x91, 0xE6, 0x36, 0xE9, 0xEB, 0xE6, 0x36, 0xE6, 0x36, 0xE6, 0x36, 0xE6, 0x36, 0xE6,
|
||||
0x36, 0xE6, 0x36, 0xE6, 0x36, 0xFF, 0xF4, 0xE9, 0xFD, 0xE6, 0x36, 0xE6, 0x36, 0xE6, 0x36, 0xA2,
|
||||
0x00, 0x51, 0xC3, 0x6F, 0xB1, 0xCC, 0xC5, 0x03, 0x32, 0x61, 0x65, 0x69, 0x6F, 0x75, 0xDF, 0x6D,
|
||||
0xDF, 0x6D, 0xDF, 0x6D, 0xDF, 0x6D, 0xDF, 0x6D, 0x21, 0x6F, 0xEE, 0x21, 0x65, 0xFD, 0x21, 0x72,
|
||||
0xFD, 0xA1, 0x00, 0x51, 0x65, 0xFD, 0x41, 0x74, 0xE0, 0xFD, 0xC2, 0x02, 0x02, 0x69, 0x6F, 0xF7,
|
||||
0x38, 0xFF, 0xFC, 0xA0, 0x11, 0x63, 0xA1, 0x04, 0x52, 0x72, 0xFD, 0x21, 0x74, 0xFB, 0x41, 0x6F,
|
||||
0xE3, 0x6D, 0xA0, 0x11, 0x92, 0x21, 0x62, 0xFD, 0xA0, 0x11, 0xB2, 0x21, 0x74, 0xFD, 0x21, 0x75,
|
||||
0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0xC5, 0x04, 0x32, 0x61, 0x69, 0x7A,
|
||||
0x65, 0x6F, 0xFF, 0xE1, 0xFF, 0xE4, 0xFF, 0xEB, 0xE5, 0x41, 0xFF, 0xFD, 0x21, 0x72, 0xEE, 0x21,
|
||||
0x65, 0xFD, 0x22, 0x62, 0x70, 0xB8, 0xFD, 0xA0, 0x11, 0xD2, 0x21, 0x74, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0xA1, 0x11, 0xF3, 0x73, 0xFD, 0x21, 0x87, 0xFB, 0x41, 0x9B, 0xEE, 0x63, 0x21, 0xC5, 0xFC, 0x21,
|
||||
0x6F, 0xFD, 0x21, 0x69, 0xFD, 0x22, 0xC4, 0x63, 0xF0, 0xFD, 0x21, 0x9B, 0xFB, 0x42, 0xC5, 0x73,
|
||||
0xFF, 0xFD, 0xF4, 0x53, 0xA1, 0x00, 0x51, 0x65, 0xF9, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74,
|
||||
0x75, 0x76, 0x77, 0x78, 0x7A, 0xE0, 0x60, 0xE0, 0x64, 0xFE, 0xC3, 0xE0, 0x74, 0xF5, 0x82, 0xE0,
|
||||
0x74, 0xE0, 0x74, 0xE0, 0x74, 0xE0, 0x74, 0xFE, 0xD9, 0xE0, 0x74, 0xFE, 0xEE, 0xE0, 0x74, 0xE0,
|
||||
0x74, 0xE0, 0x74, 0xFE, 0xFA, 0xFF, 0x56, 0xE0, 0x74, 0xE0, 0x74, 0xFF, 0x78, 0xFF, 0xC9, 0xE0,
|
||||
0x74, 0xE0, 0x74, 0xE0, 0x74, 0xFF, 0xFB, 0xA0, 0x12, 0x22, 0x21, 0x6E, 0xFD, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x7A, 0xFD, 0x42, 0x6F, 0x72, 0xE4, 0x3A, 0xFF, 0xFD, 0x41, 0x61, 0xE9, 0x1C, 0x41, 0x7A,
|
||||
0xE2, 0x9D, 0x21, 0x63, 0xFC, 0xC2, 0x04, 0x32, 0xC4, 0x65, 0xE2, 0x99, 0xFF, 0xFD, 0x21, 0x6A,
|
||||
0xF7, 0x21, 0xB3, 0xFD, 0xC1, 0x04, 0x52, 0x75, 0xFA, 0x8E, 0x41, 0x65, 0xE2, 0x81, 0xC6, 0x04,
|
||||
0x32, 0x65, 0x69, 0x79, 0x7A, 0x61, 0x6F, 0xFF, 0xF6, 0xFF, 0xFC, 0xE2, 0x7D, 0xE6, 0x8B, 0xE4,
|
||||
0x6D, 0xE4, 0x6D, 0x21, 0x73, 0xEB, 0x21, 0x6E, 0xFD, 0x41, 0x68, 0xF5, 0x55, 0x21, 0x63, 0xFC,
|
||||
0xA1, 0x00, 0x61, 0x65, 0xFD, 0xA3, 0x00, 0x51, 0xC3, 0x61, 0x7A, 0xCC, 0xF1, 0xFB, 0xA0, 0x12,
|
||||
0x42, 0x21, 0x82, 0xFD, 0xA1, 0x02, 0xB1, 0xC5, 0xFD, 0xC3, 0x10, 0x32, 0x61, 0x65, 0x7A, 0xFB,
|
||||
0x8D, 0xFB, 0x8D, 0xFF, 0xFB, 0x21, 0x63, 0xF4, 0x21, 0x85, 0xFD, 0x21, 0xC4, 0xFD, 0x21, 0x69,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
|
||||
0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A,
|
||||
0xDF, 0x85, 0xDF, 0x89, 0xFF, 0x7F, 0xDF, 0x99, 0xF4, 0xA7, 0xDF, 0x99, 0xE3, 0xBC, 0xDF, 0x99,
|
||||
0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xFF, 0x86,
|
||||
0xDF, 0x99, 0xFF, 0xD1, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xDF, 0x99, 0xFF, 0xFD,
|
||||
0xDF, 0x99, 0xA0, 0x04, 0xC2, 0x44, 0x9B, 0x82, 0xBA, 0xBC, 0xF0, 0x76, 0xFF, 0xFD, 0xFF, 0xFD,
|
||||
0xFF, 0xFD, 0xC1, 0x0C, 0xB2, 0x77, 0xDE, 0x8F, 0x21, 0x7A, 0xFA, 0xC2, 0x0B, 0xE2, 0x65, 0x72,
|
||||
0xFF, 0xFD, 0xF0, 0x60, 0xC2, 0x0B, 0xE2, 0x68, 0x7A, 0xF0, 0x57, 0xF0, 0x57, 0xA0, 0x06, 0x72,
|
||||
0x42, 0xBA, 0xBC, 0xDF, 0x30, 0xDF, 0x30, 0xC3, 0x0B, 0xE2, 0x6B, 0xC5, 0x7A, 0xFF, 0xF6, 0xFF,
|
||||
0xF9, 0xF0, 0x44, 0xA1, 0x0B, 0xE2, 0x6E, 0xEA, 0x21, 0x6A, 0xE5, 0x21, 0x65, 0xFD, 0xC2, 0x0B,
|
||||
0xE2, 0x6C, 0x72, 0xFF, 0xFD, 0xF0, 0x2D, 0xA2, 0x0B, 0xE2, 0x73, 0x74, 0xD6, 0xD6, 0x21, 0x72,
|
||||
0xCF, 0xC2, 0x0B, 0xE2, 0x62, 0x6B, 0xFF, 0xFD, 0xF0, 0x1A, 0xA2, 0x0B, 0xE2, 0x63, 0x64, 0xC3,
|
||||
0xC3, 0xA0, 0x12, 0x63, 0x21, 0x73, 0xFD, 0xC3, 0x06, 0x12, 0x61, 0xC5, 0x64, 0xE7, 0xDF, 0xE8,
|
||||
0x30, 0xE5, 0x72, 0x51, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x70,
|
||||
0x72, 0x73, 0x74, 0x77, 0x7A, 0xFF, 0xF4, 0xEA, 0xD1, 0xE8, 0x09, 0xE3, 0xAE, 0xE7, 0x63, 0xE3,
|
||||
0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xE7, 0x75, 0xE7,
|
||||
0x75, 0xE3, 0xAE, 0xE3, 0xAE, 0xE3, 0xAE, 0xC3, 0x0B, 0xE2, 0x70, 0x63, 0x6F, 0xFF, 0xBD, 0xF2,
|
||||
0xBB, 0xFF, 0xCC, 0x41, 0xBC, 0xDE, 0x9D, 0x41, 0x7A, 0xF2, 0x59, 0xC4, 0x0B, 0xE2, 0x73, 0xC5,
|
||||
0x6F, 0x7A, 0xFF, 0x62, 0xFF, 0xF8, 0xFF, 0xFC, 0xEF, 0xB0, 0xA0, 0x12, 0x92, 0xA0, 0x05, 0x91,
|
||||
0x23, 0x6E, 0x63, 0x6B, 0xFA, 0xFD, 0xFD, 0xC3, 0x0B, 0xE2, 0x74, 0x63, 0x7A, 0xFF, 0xF9, 0xF2,
|
||||
0x8B, 0xEF, 0x94, 0xC2, 0x0B, 0xE2, 0x6B, 0x72, 0xEF, 0x88, 0xEF, 0x88, 0x41, 0x65, 0xFF, 0x31,
|
||||
0x41, 0x77, 0xE3, 0x31, 0xA2, 0x0B, 0xE2, 0x62, 0x65, 0xF8, 0xFC, 0x52, 0xC4, 0xC5, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xEF, 0x73,
|
||||
0xFE, 0xFA, 0xFF, 0x10, 0xFF, 0x19, 0xFF, 0x2C, 0xFF, 0x38, 0xF2, 0x0C, 0xF2, 0x0C, 0xFF, 0x43,
|
||||
0xFF, 0x4C, 0xFF, 0x56, 0xFF, 0x5F, 0xFF, 0xAC, 0xFF, 0xC0, 0xFF, 0xDC, 0xFF, 0xE8, 0xF2, 0x0C,
|
||||
0xFF, 0xF9, 0xC1, 0x03, 0xA2, 0x6E, 0xDF, 0xC7, 0x43, 0x82, 0x9B, 0xBC, 0xFF, 0xFA, 0xDF, 0xBB,
|
||||
0xDF, 0xBB, 0xC1, 0x01, 0xB2, 0x77, 0xDF, 0xF5, 0xC1, 0x01, 0xB2, 0x6B, 0xDE, 0x08, 0xC2, 0x01,
|
||||
0xB2, 0x64, 0x74, 0xDF, 0xE9, 0xDF, 0xE9, 0xC1, 0x01, 0xB2, 0x63, 0xDD, 0xF5, 0xC6, 0x01, 0xB2,
|
||||
0x62, 0x64, 0x6E, 0x73, 0x74, 0x7A, 0xDF, 0xDA, 0xDF, 0xDA, 0xDF, 0xDA, 0xDF, 0xDA, 0xDF, 0xDA,
|
||||
0xDD, 0xF3, 0xA0, 0x12, 0xB3, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0xC3, 0x01, 0xB2, 0x70, 0x74,
|
||||
0x7A, 0xE6, 0x1C, 0xFF, 0xFD, 0xDD, 0xD5, 0xA0, 0x12, 0xE2, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD,
|
||||
0x21, 0x74, 0xFD, 0x21, 0x85, 0xFD, 0x21, 0xC4, 0xFD, 0xA1, 0x01, 0xB2, 0x6E, 0xFD, 0x41, 0x72,
|
||||
0xDF, 0x99, 0x41, 0x82, 0xDF, 0x95, 0x21, 0xC5, 0xFC, 0xA2, 0x01, 0xB2, 0x62, 0x67, 0xF5, 0xFD,
|
||||
0x52, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73,
|
||||
0x74, 0x77, 0x7A, 0xDF, 0x46, 0xFF, 0x88, 0xDD, 0x2F, 0xDF, 0x65, 0xDF, 0x75, 0xDD, 0x2F, 0xDD,
|
||||
0x2F, 0xDD, 0x2F, 0xDF, 0x8A, 0xFF, 0x92, 0xFF, 0x98, 0xFF, 0x9E, 0xFF, 0xA7, 0xFF, 0xAD, 0xFF,
|
||||
0xCB, 0xFF, 0x98, 0xFF, 0xE9, 0xFF, 0xF9, 0x43, 0xC5, 0x64, 0x6B, 0xDD, 0xC9, 0xDD, 0xCC, 0xDD,
|
||||
0xCC, 0x41, 0x73, 0xF1, 0x96, 0x22, 0x6F, 0x75, 0xF2, 0xFC, 0x21, 0x6C, 0xFB, 0x41, 0x6D, 0xDD,
|
||||
0xD0, 0x21, 0x6F, 0xFC, 0x21, 0x7A, 0xFD, 0x21, 0x63, 0xFD, 0x22, 0x65, 0x6C, 0xF0, 0xFD, 0x41,
|
||||
0x62, 0xDD, 0xC4, 0x21, 0x65, 0xFC, 0xA1, 0x00, 0x51, 0x69, 0xFD, 0x41, 0x77, 0xDC, 0xCA, 0x21,
|
||||
0x62, 0xFC, 0xC4, 0x13, 0x02, 0x69, 0x6F, 0x75, 0x77, 0xE0, 0xCC, 0xFF, 0xFD, 0xE0, 0xCC, 0xE0,
|
||||
0xCC, 0x21, 0x82, 0xF1, 0x21, 0xC5, 0xFD, 0x21, 0xB3, 0xFD, 0x21, 0xC3, 0xFD, 0xC2, 0x0D, 0x72,
|
||||
0x6F, 0x77, 0xDB, 0x56, 0xDB, 0x56, 0x21, 0x68, 0xF7, 0x21, 0x63, 0xFD, 0x21, 0x65, 0xFD, 0xA2,
|
||||
0x00, 0x51, 0x70, 0x7A, 0xEB, 0xFD, 0x41, 0x7A, 0xDE, 0xA3, 0xA1, 0x03, 0xA2, 0x73, 0xFC, 0x44,
|
||||
0x82, 0x9B, 0xBA, 0xBC, 0xDE, 0x94, 0xDE, 0x94, 0xDE, 0x94, 0xFF, 0xFB, 0x41, 0x61, 0xDE, 0x8D,
|
||||
0xA1, 0x02, 0x41, 0x68, 0xFC, 0xC2, 0x01, 0xB2, 0x7A, 0x68, 0xFF, 0xFB, 0xDC, 0xDB, 0xC3, 0x01,
|
||||
0xB2, 0xC5, 0x72, 0x7A, 0xDE, 0xA0, 0xDC, 0xD2, 0xDC, 0xD2, 0xC2, 0x01, 0xB2, 0x6C, 0x72, 0xDC,
|
||||
0xC6, 0xDE, 0x80, 0xA0, 0x13, 0x23, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD, 0xC2, 0x01, 0xB2, 0xC5,
|
||||
0x7A, 0xDE, 0xBE, 0xDC, 0xB4, 0xC3, 0x01, 0xB2, 0x63, 0x6B, 0x72, 0xDC, 0xA7, 0xDC, 0xAB, 0xDC,
|
||||
0xAB, 0xA0, 0x06, 0x13, 0x21, 0x73, 0xFD, 0x21, 0x6B, 0xFD, 0x41, 0x6F, 0xED, 0xB1, 0x21, 0x7A,
|
||||
0xFC, 0x54, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6F, 0x70, 0x72,
|
||||
0x73, 0x74, 0x77, 0x7A, 0x65, 0x69, 0xDE, 0x35, 0xFF, 0x9E, 0xDE, 0x4F, 0xFF, 0xB4, 0xFF, 0xBD,
|
||||
0xDC, 0x1E, 0xDE, 0x71, 0xDC, 0x1E, 0xFF, 0xC9, 0xDC, 0x1E, 0xFE, 0x87, 0xFF, 0xD8, 0xDE, 0x90,
|
||||
0xFF, 0xDB, 0xDE, 0xA9, 0xFF, 0xE4, 0xDC, 0x1E, 0xDC, 0x1E, 0xFF, 0xF6, 0xFF, 0xFD, 0xD9, 0x01,
|
||||
0xF1, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x6F, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x79, 0x7A, 0xDC, 0x2B, 0xDC, 0x2F, 0xDC, 0x3F,
|
||||
0xDC, 0x3F, 0xDC, 0x3F, 0xFE, 0xB2, 0xDC, 0x3F, 0xDC, 0x3F, 0xDC, 0x3F, 0xFF, 0x0C, 0xDC, 0x3F,
|
||||
0xDC, 0x3F, 0xDC, 0x3F, 0xDC, 0x3F, 0xFF, 0x18, 0xE2, 0x93, 0xDC, 0x3F, 0xDC, 0x3F, 0xFF, 0x51,
|
||||
0xDC, 0x3F, 0xDC, 0x3F, 0xDC, 0x3F, 0xDC, 0x3F, 0xFF, 0xC3, 0xDC, 0x3F, 0xC1, 0x00, 0x61, 0x6F,
|
||||
0xE2, 0x3E, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0xFA, 0xD9, 0xB8, 0xD9, 0xB8, 0xD9, 0xB8,
|
||||
0xD9, 0xB8, 0xA0, 0x13, 0x51, 0xA1, 0x13, 0x62, 0x75, 0xFD, 0x21, 0x87, 0xFB, 0x21, 0xC4, 0xFD,
|
||||
0x21, 0x9B, 0xFD, 0x21, 0xC5, 0xFD, 0x41, 0x6F, 0xE7, 0x58, 0x21, 0x70, 0xFC, 0x21, 0x73, 0xFD,
|
||||
0xC5, 0x01, 0xB2, 0xC5, 0x6F, 0x72, 0x79, 0x7A, 0xDD, 0x9E, 0xFF, 0xF3, 0xDB, 0xD0, 0xFF, 0xFD,
|
||||
0xDB, 0xD0, 0xC3, 0x01, 0xB2, 0x74, 0x6C, 0x72, 0xDD, 0xA5, 0xDB, 0xBE, 0xDD, 0x78, 0xC3, 0x01,
|
||||
0xB2, 0x67, 0x6B, 0x74, 0xDD, 0x99, 0xDD, 0x99, 0xDD, 0x99, 0x42, 0x72, 0x73, 0xFE, 0xE9, 0xFE,
|
||||
0xE9, 0x21, 0x62, 0xF9, 0x41, 0x68, 0xDD, 0x83, 0xC3, 0x01, 0xB2, 0x63, 0xC5, 0x7A, 0xFF, 0xFC,
|
||||
0xDD, 0xA2, 0xDB, 0x98, 0x41, 0x6F, 0xDA, 0xAF, 0xA1, 0x01, 0xB2, 0x74, 0xFC, 0xA0, 0x08, 0x41,
|
||||
0x21, 0x63, 0xFD, 0xA1, 0x0C, 0xB2, 0x69, 0xFD, 0x42, 0x6E, 0x7A, 0xFF, 0xFB, 0xFE, 0xE2, 0x41,
|
||||
0x64, 0xDB, 0xD4, 0x21, 0x65, 0xFC, 0x21, 0x69, 0xFD, 0x55, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x75, 0x77, 0x7A, 0x69, 0x6E, 0xDD,
|
||||
0x0D, 0xE6, 0x77, 0xDD, 0x27, 0xDD, 0x2C, 0xFF, 0x97, 0xDA, 0xF6, 0xDD, 0x49, 0xDA, 0xF6, 0xFF,
|
||||
0xA9, 0xFF, 0xB5, 0xDD, 0x63, 0xFF, 0xC8, 0xDA, 0xF6, 0xFF, 0xCF, 0xDD, 0x81, 0xDD, 0x8D, 0xFF,
|
||||
0xDF, 0xDA, 0xF6, 0xDA, 0xF6, 0xFF, 0xEF, 0xFF, 0xFD, 0xA0, 0x13, 0x82, 0x21, 0x75, 0xFD, 0x21,
|
||||
0x77, 0xFD, 0x22, 0x6C, 0x72, 0xF7, 0xF7, 0xA3, 0x0C, 0xB2, 0x61, 0x65, 0x79, 0xF8, 0xFB, 0xF2,
|
||||
0x21, 0x7A, 0xF7, 0x41, 0x74, 0xEE, 0xCD, 0x21, 0x6E, 0xFC, 0x41, 0x64, 0xEE, 0xC6, 0x21, 0x65,
|
||||
0xFC, 0x41, 0x6C, 0xEE, 0xBF, 0x21, 0x61, 0xFC, 0x23, 0x6F, 0x72, 0x77, 0xEF, 0xF6, 0xFD, 0x21,
|
||||
0x6B, 0xF9, 0x21, 0x73, 0xFD, 0xA2, 0x00, 0x51, 0x65, 0x79, 0xDB, 0xFD, 0xC1, 0x01, 0xB2, 0x6C,
|
||||
0xDA, 0xE4, 0xC3, 0x01, 0xB2, 0x6B, 0xC5, 0x7A, 0xDC, 0xC5, 0xDC, 0xE8, 0xDA, 0xDE, 0xC3, 0x01,
|
||||
0xB2, 0x63, 0x6D, 0x7A, 0xDA, 0xCE, 0xFF, 0x4F, 0xDA, 0xD2, 0xA0, 0x13, 0xA5, 0x21, 0x73, 0xFD,
|
||||
0x42, 0x65, 0x70, 0xFF, 0xFD, 0xDC, 0xA7, 0x21, 0x6C, 0xF3, 0x21, 0x65, 0xFD, 0x22, 0x6D, 0x73,
|
||||
0xF3, 0xFD, 0x41, 0x70, 0xDC, 0x95, 0x21, 0x61, 0xFC, 0xC3, 0x01, 0xB2, 0x6B, 0x65, 0x68, 0xDC,
|
||||
0x4A, 0xFF, 0xF4, 0xFF, 0xFD, 0x51, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C,
|
||||
0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xDC, 0x41, 0xE5, 0xAB, 0xDA, 0x2A, 0xDC, 0x60, 0xDC,
|
||||
0x70, 0xDA, 0x2A, 0xDA, 0x2A, 0xDA, 0x2A, 0xFF, 0xB7, 0xDA, 0x2A, 0xDA, 0x2A, 0xFC, 0xA2, 0xFF,
|
||||
0xBD, 0xFF, 0xC9, 0xFF, 0xF4, 0xDA, 0x2A, 0xDA, 0x2A, 0x41, 0x6B, 0xEE, 0x58, 0x21, 0x6F, 0xFC,
|
||||
0x21, 0x6E, 0xFD, 0x21, 0x6D, 0xFD, 0xC1, 0x13, 0xF2, 0x77, 0xE7, 0xE4, 0x21, 0x68, 0xFA, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x79, 0xFD, 0x21, 0x77, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x72, 0xFD, 0xA1, 0x00,
|
||||
0x51, 0x61, 0xFD, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9, 0xA5, 0xD9,
|
||||
0xA5, 0xD9, 0xA5, 0xA0, 0x14, 0x12, 0x42, 0xBA, 0xBC, 0xFF, 0xFD, 0xD8, 0x6D, 0xC2, 0x01, 0x92,
|
||||
0xC5, 0x7A, 0xFF, 0xF9, 0xDE, 0x8E, 0xC1, 0x01, 0x92, 0x63, 0xE1, 0xD8, 0x52, 0xC4, 0xC5, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0xE1,
|
||||
0xA8, 0xFF, 0xD7, 0xD9, 0x87, 0xE1, 0xB9, 0xFF, 0xF1, 0xD9, 0x87, 0xD9, 0x87, 0xD9, 0x87, 0xD9,
|
||||
0x87, 0xD9, 0x87, 0xFF, 0xFA, 0xD9, 0x87, 0xD9, 0x87, 0xE1, 0xEB, 0xE1, 0xEB, 0xD9, 0x87, 0xD9,
|
||||
0x87, 0xD9, 0x87, 0x21, 0x65, 0xC9, 0xA1, 0x00, 0x51, 0x69, 0xFD, 0xC1, 0x0C, 0xB2, 0x75, 0xFE,
|
||||
0xAE, 0x21, 0x7A, 0xFA, 0xA1, 0x00, 0x51, 0x6F, 0xFD, 0xD9, 0x01, 0xF1, 0xC4, 0xC5, 0x61, 0x62,
|
||||
0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73,
|
||||
0x74, 0x76, 0x77, 0x78, 0x7A, 0xD9, 0xA0, 0xFD, 0xC9, 0xFE, 0x60, 0xD9, 0xB4, 0xD9, 0xB4, 0xFE,
|
||||
0xDC, 0xFF, 0x2C, 0xD9, 0xB4, 0xD9, 0xB4, 0xD9, 0xB4, 0xFF, 0x6A, 0xD9, 0xB4, 0xD9, 0xB4, 0xD9,
|
||||
0xB4, 0xFF, 0x85, 0xFF, 0xED, 0xDD, 0xD7, 0xD9, 0xB4, 0xFF, 0xFB, 0xD9, 0xB4, 0xD9, 0xB4, 0xD9,
|
||||
0xB4, 0xD9, 0xB4, 0xD9, 0xB4, 0xD9, 0xB4, 0x5A, 0xC4, 0xC5, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66,
|
||||
0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, 0x70, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
|
||||
0x78, 0x7A, 0xD7, 0x77, 0xD8, 0x78, 0xD9, 0x3F, 0xD9, 0xA7, 0xDA, 0x7B, 0xDC, 0x80, 0xDD, 0x34,
|
||||
0xDD, 0x44, 0xDD, 0x9C, 0xDD, 0xFC, 0xDE, 0x6F, 0xDE, 0x83, 0xDF, 0x6B, 0xDF, 0xBD, 0xE0, 0x1E,
|
||||
0xEA, 0x49, 0xED, 0xC7, 0xF6, 0x22, 0xF7, 0x53, 0xF8, 0xF2, 0xF9, 0xCD, 0xFB, 0x24, 0xDD, 0x44,
|
||||
0xFD, 0x27, 0xDD, 0x44, 0xFF, 0xB2, 0xA0, 0x14, 0x31, 0xA0, 0x15, 0x81, 0xA1, 0x14, 0x62, 0x2E,
|
||||
0xFD, 0x21, 0x2E, 0xF8, 0x25, 0x84, 0x9B, 0xBA, 0xBC, 0x82, 0xF8, 0xF8, 0xF8, 0xF8, 0xFD, 0xA1,
|
||||
0x15, 0x42, 0x2E, 0xEA, 0x21, 0x87, 0xFB, 0x36, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D,
|
||||
0x6E, 0x70, 0x73, 0x74, 0x7A, 0xC4, 0x2E, 0x68, 0x6A, 0x6C, 0x72, 0x76, 0x77, 0x78, 0xED, 0xE5,
|
||||
0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xE5, 0xFD, 0xE2, 0xEA, 0xEA, 0xEA,
|
||||
0xEA, 0xEA, 0xEA, 0xEA, 0x43, 0x85, 0x99, 0x87, 0xFF, 0xB2, 0xFF, 0xB2, 0xFF, 0xD3, 0x41, 0x7A,
|
||||
0xF9, 0x9F, 0x21, 0x63, 0xFC, 0xA1, 0x14, 0x31, 0x77, 0xFD, 0x21, 0xB3, 0xFB, 0xA0, 0x14, 0x42,
|
||||
0xC2, 0x14, 0x31, 0x69, 0x2E, 0xFF, 0xFD, 0xFF, 0x99, 0xA0, 0x14, 0x61, 0x42, 0x63, 0x2E, 0xFF,
|
||||
0xFD, 0xFF, 0x8D, 0x21, 0x87, 0xF9, 0x45, 0x9B, 0xBA, 0xBC, 0x82, 0x84, 0xFF, 0x86, 0xFF, 0x86,
|
||||
0xFF, 0x86, 0xFF, 0x8B, 0xFF, 0x8B, 0x43, 0x6D, 0x6E, 0x2E, 0xFF, 0xE3, 0xFF, 0xE3, 0xFF, 0x73,
|
||||
0x56, 0x63, 0xC4, 0xC5, 0x62, 0x64, 0x66, 0x67, 0x6B, 0x70, 0x73, 0x74, 0x7A, 0x6C, 0x2E, 0x68,
|
||||
0x6A, 0x6D, 0x6E, 0x72, 0x76, 0x77, 0x78, 0xFF, 0xD0, 0xFF, 0xE3, 0xFF, 0xE6, 0xFF, 0x6C, 0xFF,
|
||||
0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF, 0x6C, 0xFF,
|
||||
0xF6, 0xFF, 0x69, 0xFF, 0x71, 0xFF, 0x71, 0xFF, 0x71, 0xFF, 0x71, 0xFF, 0x71, 0xFF, 0x71, 0xFF,
|
||||
0x71, 0xFF, 0x71, 0xA0, 0x00, 0xF1, 0x21, 0xBA, 0xFD, 0xC2, 0x14, 0x62, 0xC5, 0x2E, 0xFF, 0xFD,
|
||||
0xFF, 0x20, 0x41, 0x87, 0xFF, 0x1A, 0x45, 0x9B, 0xBC, 0xBA, 0x82, 0x84, 0xFF, 0x16, 0xFF, 0x16,
|
||||
0xFF, 0x29, 0xFF, 0x1B, 0xFF, 0x1B, 0x56, 0x64, 0xC4, 0xC5, 0x62, 0x63, 0x66, 0x67, 0x6B, 0x6C,
|
||||
0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0x2E, 0x68, 0x6A, 0x72, 0x76, 0x78, 0xFF, 0xE3, 0xFF,
|
||||
0xEC, 0xFF, 0xF0, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF,
|
||||
0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x06, 0xFF, 0x03, 0xFF,
|
||||
0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0xFF, 0x0B, 0x45, 0x84, 0x9B, 0xBA, 0xBC, 0x82, 0xFE,
|
||||
0xC3, 0xFE, 0xC3, 0xFE, 0xC3, 0xFE, 0xC3, 0xFE, 0xD6, 0x56, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x2E, 0x76, 0x78,
|
||||
0xFF, 0x99, 0xFF, 0xF0, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3,
|
||||
0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3,
|
||||
0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB3, 0xFE, 0xB0, 0xFE, 0xB8, 0xFE, 0xB8, 0x45, 0x82, 0x84, 0x9B,
|
||||
0xBA, 0xBC, 0xFE, 0x70, 0xFE, 0x70, 0xFE, 0x70, 0xFE, 0x70, 0xFE, 0x70, 0x56, 0xC4, 0xC5, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A,
|
||||
0x2E, 0x76, 0x78, 0xFF, 0x46, 0xFF, 0xF0, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE,
|
||||
0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE,
|
||||
0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x60, 0xFE, 0x5D, 0xFE, 0x65, 0xFE, 0x65, 0x45,
|
||||
0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xFE, 0x1D, 0xFE, 0x1D, 0xFE, 0x1D, 0xFE, 0x1D, 0xFE, 0x30, 0x56,
|
||||
0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74,
|
||||
0x77, 0x7A, 0x2E, 0x68, 0x76, 0x78, 0xFE, 0xF3, 0xFF, 0xF0, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D,
|
||||
0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D,
|
||||
0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0D, 0xFE, 0x0A, 0xFE, 0x12, 0xFE, 0x12,
|
||||
0xFE, 0x12, 0x45, 0x9B, 0xBA, 0x82, 0x84, 0xBC, 0xFE, 0x5E, 0xFE, 0xC4, 0xFF, 0x17, 0xFF, 0x6A,
|
||||
0xFF, 0xBD, 0x42, 0x6B, 0x2E, 0xFE, 0x27, 0xFD, 0xB7, 0xC1, 0x14, 0x82, 0x2E, 0xFD, 0xB0, 0x25,
|
||||
0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xF3, 0xFA, 0xFA, 0xFA, 0xFA, 0x21, 0x87, 0xEF, 0xA0, 0x16, 0x43,
|
||||
0x21, 0x79, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x77, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x61, 0xFD, 0x45,
|
||||
0x6E, 0x2E, 0x7A, 0x64, 0x6F, 0xFD, 0xFA, 0xFD, 0x8A, 0xFD, 0x92, 0xFE, 0x64, 0xFF, 0xFD, 0xC1,
|
||||
0x15, 0x62, 0x2E, 0xFD, 0x7A, 0xA0, 0x16, 0x22, 0x21, 0x7A, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x61,
|
||||
0xFD, 0x21, 0x68, 0xFD, 0x21, 0x6B, 0xFD, 0x42, 0x2E, 0x6F, 0xFD, 0x62, 0xFF, 0xFD, 0x41, 0x68,
|
||||
0xFE, 0x35, 0x21, 0x74, 0xFC, 0xA0, 0x15, 0x93, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x41, 0x64,
|
||||
0xD9, 0xF0, 0x21, 0x6E, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x68, 0xFD, 0x23, 0x65, 0x66, 0x6B, 0xE7,
|
||||
0xF0, 0xFD, 0xA0, 0x04, 0xD3, 0x21, 0x75, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x6F, 0xFD, 0xA0, 0x0B,
|
||||
0xB2, 0x21, 0x70, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x7A, 0xFD, 0xA0, 0x15, 0xC4,
|
||||
0xA1, 0x01, 0x22, 0x6D, 0xFD, 0x21, 0x73, 0xFB, 0x21, 0x65, 0xFD, 0x21, 0x6E, 0xFD, 0xA0, 0x16,
|
||||
0x02, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6E, 0xFD,
|
||||
0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x24, 0x6E, 0x73, 0x7A, 0x72, 0xC5, 0xD4, 0xE5, 0xFD, 0x41,
|
||||
0x61, 0xDB, 0x01, 0x21, 0x6D, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xB5, 0x21, 0x6D, 0xFD, 0xA0,
|
||||
0x13, 0x02, 0xA1, 0x00, 0x71, 0x6D, 0xFD, 0x21, 0x73, 0xFB, 0x21, 0x73, 0xFD, 0x21, 0x65, 0xFD,
|
||||
0x21, 0x6E, 0xFD, 0x22, 0x7A, 0x69, 0xE9, 0xFD, 0xA0, 0x16, 0x72, 0x21, 0x6E, 0xFD, 0x21, 0x61,
|
||||
0xFD, 0x21, 0x70, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x22, 0x73, 0x6B, 0xE9, 0xFD, 0x5A,
|
||||
0xC5, 0xC4, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x72, 0x62, 0x2E,
|
||||
0x68, 0x6A, 0x6C, 0x76, 0x77, 0x78, 0x65, 0x69, 0x6F, 0x75, 0xFF, 0x00, 0xFF, 0x0B, 0xFE, 0xFA,
|
||||
0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA, 0xFE, 0xFA,
|
||||
0xFE, 0xFA, 0xFE, 0xFA, 0xFF, 0x20, 0xFF, 0x30, 0xFC, 0xAA, 0xFC, 0xB2, 0xFC, 0xB2, 0xFF, 0x48,
|
||||
0xFC, 0xB2, 0xFC, 0xB2, 0xFC, 0xB2, 0xFF, 0x6C, 0xFF, 0xB7, 0xFF, 0xC7, 0xFF, 0xFB, 0x45, 0x84,
|
||||
0x9B, 0xBA, 0xBC, 0x82, 0xFE, 0xAB, 0xFE, 0xAB, 0xFE, 0xAB, 0xFE, 0xAB, 0xFC, 0x63, 0xA0, 0x14,
|
||||
0x62, 0x21, 0x87, 0xFD, 0xC1, 0x00, 0xF1, 0x2E, 0xFC, 0x45, 0x25, 0x84, 0x9B, 0xBA, 0xBC, 0x82,
|
||||
0xF4, 0xF4, 0xF4, 0xF4, 0xFA, 0xC1, 0x00, 0xF1, 0x7A, 0xFC, 0x3C, 0xA0, 0x16, 0x91, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x73, 0xFD, 0xD4, 0x02, 0x31, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D,
|
||||
0x6E, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0x72, 0x77, 0x6A, 0x6C, 0x75, 0xFF, 0xDD, 0xFF, 0xE6, 0xFF,
|
||||
0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF,
|
||||
0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFF, 0xDA, 0xFC, 0x25, 0xFF, 0xF1, 0xFF, 0xE0, 0xFC, 0xFF, 0xFC,
|
||||
0xFF, 0xFF, 0xFD, 0x44, 0x84, 0x9B, 0xBA, 0xBC, 0xFF, 0x9B, 0xFF, 0x9B, 0xFF, 0x9B, 0xFF, 0x9B,
|
||||
0xD0, 0x02, 0x31, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73,
|
||||
0x74, 0x7A, 0x2E, 0xFF, 0x91, 0xFF, 0xF3, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF,
|
||||
0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFF, 0x8E, 0xFB, 0xDC, 0xFF,
|
||||
0x8E, 0xFB, 0xD9, 0x41, 0x6C, 0xFE, 0x32, 0x21, 0x6C, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x77, 0xFD,
|
||||
0x21, 0x64, 0xFD, 0x21, 0x6C, 0xFD, 0xA0, 0x16, 0xA2, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x58, 0xC4, 0xC5, 0x62, 0x64, 0x66, 0x67, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x73, 0x74, 0x68, 0x7A, 0x63, 0x2E, 0x6A, 0x72, 0x76, 0x77, 0x78, 0x61, 0x75, 0xFD, 0xE5,
|
||||
0xFF, 0x29, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4,
|
||||
0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFD, 0xD4, 0xFF, 0x5F, 0xFF, 0xAB, 0xFE, 0x0A, 0xFB, 0x84,
|
||||
0xFB, 0x8C, 0xFB, 0x8C, 0xFB, 0x8C, 0xFB, 0x8C, 0xFB, 0x8C, 0xFF, 0xEE, 0xFF, 0xFD, 0x41, 0x7A,
|
||||
0xFB, 0xAB, 0x43, 0x62, 0x73, 0x2E, 0xFB, 0xA7, 0xFF, 0xFC, 0xFB, 0x37, 0xA0, 0x14, 0xA2, 0x21,
|
||||
0x87, 0xFD, 0x24, 0x84, 0x9B, 0xBA, 0xBC, 0xFA, 0xFA, 0xFA, 0xFA, 0xCF, 0x02, 0x31, 0xC4, 0xC5,
|
||||
0x62, 0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0xFF, 0xF4, 0xFF,
|
||||
0xF7, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF,
|
||||
0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFF, 0xF1, 0xFB, 0x1E, 0x45, 0x84, 0x9B, 0xBA, 0xBC,
|
||||
0x82, 0xFF, 0xC1, 0xFF, 0xC1, 0xFF, 0xC1, 0xFF, 0xC1, 0xE7, 0xF3, 0x42, 0x62, 0x6D, 0xDF, 0xFD,
|
||||
0xDF, 0xFD, 0x21, 0x7A, 0xF9, 0xD4, 0x02, 0x31, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6B,
|
||||
0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0x6A, 0x6C, 0x72, 0x77, 0x65, 0xFF, 0xAA, 0xFF, 0xE6,
|
||||
0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7,
|
||||
0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFF, 0xA7, 0xFA, 0xD4, 0xE7, 0xD9, 0xE7, 0xD9, 0xE7, 0xD9,
|
||||
0xE7, 0xD9, 0xFF, 0xFD, 0x45, 0x82, 0xBA, 0xBC, 0x84, 0x9B, 0xFF, 0x5E, 0xFF, 0x77, 0xFF, 0xC1,
|
||||
0xFC, 0xE5, 0xFC, 0xE5, 0xA0, 0x14, 0xC2, 0x21, 0x77, 0xFD, 0x21, 0x6F, 0xFD, 0xC2, 0x14, 0x82,
|
||||
0x69, 0x2E, 0xFF, 0xFD, 0xFA, 0x7C, 0x42, 0x2E, 0x77, 0xFA, 0x73, 0xE7, 0x78, 0x43, 0x6E, 0x2E,
|
||||
0x7A, 0xFA, 0xDC, 0xFA, 0x6C, 0xFF, 0xF9, 0xD0, 0x02, 0x31, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66,
|
||||
0x67, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0xFE, 0x1A, 0xFE, 0x7C, 0xFE, 0x17,
|
||||
0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17,
|
||||
0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFE, 0x17, 0xFA, 0x62, 0x41, 0x64, 0xDF, 0x60, 0x21, 0x6E,
|
||||
0xFC, 0x21, 0x61, 0xFD, 0x21, 0x6C, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x73, 0xFD,
|
||||
0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x41, 0x74, 0xFC, 0xE8, 0x21, 0x73, 0xFC, 0x21, 0x61, 0xFD,
|
||||
0x21, 0x70, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x58, 0xC5, 0xC4, 0x62, 0x63, 0x66, 0x67,
|
||||
0x6B, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x72, 0x7A, 0x64, 0x2E, 0x68, 0x6A, 0x6C, 0x76, 0x77, 0x78,
|
||||
0x65, 0x75, 0xFF, 0x6B, 0xFC, 0x61, 0xFC, 0x50, 0xFC, 0x50, 0xFC, 0x50, 0xFC, 0x50, 0xFC, 0x50,
|
||||
0xFC, 0x50, 0xFF, 0x84, 0xFC, 0x50, 0xFC, 0x50, 0xFC, 0x50, 0xFF, 0x94, 0xFF, 0x9E, 0xFC, 0x86,
|
||||
0xFA, 0x00, 0xFA, 0x08, 0xFA, 0x08, 0xFA, 0x08, 0xFA, 0x08, 0xFA, 0x08, 0xFA, 0x08, 0xFF, 0xEA,
|
||||
0xFF, 0xFD, 0x41, 0x87, 0xF9, 0xBF, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC, 0xF9, 0xBB, 0xF9, 0xBB,
|
||||
0xF9, 0xBB, 0xF9, 0xBB, 0xF9, 0xBB, 0x41, 0x6D, 0xD7, 0xBA, 0x21, 0x72, 0xFC, 0x21, 0x61, 0xFD,
|
||||
0x41, 0x6E, 0xFC, 0x6E, 0x21, 0x69, 0xFC, 0x21, 0x62, 0xFD, 0x22, 0x68, 0x7A, 0xF3, 0xFD, 0x21,
|
||||
0x73, 0xFB, 0xA0, 0x16, 0xC2, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x7A, 0xFD, 0x41, 0x72,
|
||||
0xFC, 0x07, 0x21, 0x6F, 0xFC, 0x21, 0x6C, 0xFD, 0x21, 0x6B, 0xFD, 0xA0, 0x16, 0xE3, 0x21, 0x74,
|
||||
0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x66, 0xFD, 0x44, 0x6B, 0x72,
|
||||
0x6C, 0x73, 0xD1, 0x98, 0xFF, 0xDE, 0xFF, 0xEB, 0xFF, 0xFD, 0x58, 0x63, 0x6B, 0x6D, 0x6E, 0x66,
|
||||
0x2E, 0xC4, 0xC5, 0x62, 0x64, 0x67, 0x68, 0x6A, 0x6C, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x7A, 0x69, 0x6F, 0xFB, 0x9F, 0xFB, 0x9F, 0xFB, 0x9F, 0xFB, 0x9F, 0xFB, 0xD5, 0xF9, 0x4F, 0xFF,
|
||||
0x98, 0xFF, 0x9C, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9,
|
||||
0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xF9, 0x57, 0xFF,
|
||||
0xC5, 0xFF, 0xF3, 0x42, 0x62, 0x2E, 0xF9, 0x76, 0xF9, 0x06, 0x45, 0x82, 0x84, 0x9B, 0xBA, 0xBC,
|
||||
0xFF, 0xF9, 0xFB, 0x4F, 0xFB, 0x4F, 0xFB, 0x4F, 0xFB, 0x4F, 0xA0, 0x17, 0x12, 0x21, 0x6F, 0xFD,
|
||||
0x21, 0x6C, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x64, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x74, 0xF4, 0x52,
|
||||
0x43, 0x2E, 0x61, 0x6F, 0xF8, 0xD9, 0xFF, 0xF9, 0xFF, 0xFC, 0x41, 0x74, 0xE5, 0xAD, 0x21, 0x65,
|
||||
0xFC, 0x41, 0x61, 0xD6, 0xC0, 0x21, 0x74, 0xFC, 0x21, 0x70, 0xFD, 0x22, 0x67, 0x6F, 0xF3, 0xFD,
|
||||
0x21, 0x64, 0xFB, 0xC1, 0x02, 0x02, 0x7A, 0xD1, 0x1E, 0x21, 0x73, 0xFA, 0x21, 0x66, 0xFD, 0x21,
|
||||
0x6C, 0xFD, 0x58, 0xC5, 0xC4, 0x62, 0x63, 0x64, 0x66, 0x6B, 0x6D, 0x70, 0x73, 0x74, 0x7A, 0x67,
|
||||
0x2E, 0x68, 0x6A, 0x6C, 0x6E, 0x72, 0x76, 0x77, 0x78, 0x61, 0x6F, 0xFF, 0xA8, 0xFB, 0x08, 0xFA,
|
||||
0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA, 0xF7, 0xFA,
|
||||
0xF7, 0xFA, 0xF7, 0xFB, 0x2D, 0xF8, 0xA7, 0xF8, 0xAF, 0xF8, 0xAF, 0xF8, 0xAF, 0xF8, 0xAF, 0xFF,
|
||||
0xCE, 0xF8, 0xAF, 0xF8, 0xAF, 0xF8, 0xAF, 0xFF, 0xEE, 0xFF, 0xFD, 0x45, 0x82, 0x84, 0x9B, 0xBA,
|
||||
0xBC, 0xFA, 0xAE, 0xFA, 0xAE, 0xFA, 0xAE, 0xFA, 0xAE, 0xFA, 0xAE, 0x41, 0x7A, 0xFB, 0x26, 0x21,
|
||||
0x73, 0xFC, 0xA0, 0x17, 0x32, 0x21, 0x77, 0xFD, 0x21, 0x7A, 0xFD, 0x41, 0x79, 0xD6, 0x55, 0x21,
|
||||
0x65, 0xFC, 0x21, 0x6C, 0xFD, 0x22, 0x63, 0x78, 0xF3, 0xFD, 0x58, 0xC4, 0xC5, 0x62, 0x63, 0x64,
|
||||
0x66, 0x67, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x68, 0x2E, 0x76,
|
||||
0x78, 0x69, 0x75, 0xFA, 0x90, 0xFF, 0xD1, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA,
|
||||
0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA,
|
||||
0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0x7F, 0xFA, 0xB5, 0xF8, 0x2F, 0xF8, 0x37, 0xF8, 0x37, 0xFF,
|
||||
0xE5, 0xFF, 0xFB, 0xA0, 0x05, 0x42, 0x21, 0x82, 0xFD, 0xC3, 0x14, 0x82, 0x2E, 0xC5, 0x72, 0xF7,
|
||||
0xE0, 0xFF, 0xFD, 0xFF, 0xFA, 0xA0, 0x17, 0x53, 0x22, 0x62, 0x6D, 0xFD, 0xFD, 0x21, 0x7A, 0xFB,
|
||||
0x21, 0x7A, 0xFD, 0x57, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6B, 0x6C, 0x6D, 0x6E,
|
||||
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6A, 0x2E, 0x76, 0x78, 0x61, 0xFA, 0x27, 0xFF, 0x68, 0xFA,
|
||||
0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA,
|
||||
0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFA, 0x16, 0xFF, 0xE6, 0xFA, 0x16, 0xFA, 0x16, 0xFA,
|
||||
0x4C, 0xF7, 0xC6, 0xF7, 0xCE, 0xF7, 0xCE, 0xFF, 0xFD, 0xA0, 0x14, 0xE3, 0xA1, 0x14, 0x82, 0x74,
|
||||
0xFD, 0xC3, 0x14, 0x82, 0x7A, 0x2E, 0x74, 0xFF, 0xFB, 0xF7, 0x78, 0xF7, 0x80, 0x41, 0x6E, 0xFE,
|
||||
0x7D, 0x21, 0x6F, 0xFC, 0x21, 0x72, 0xFD, 0x41, 0x65, 0xFE, 0x73, 0x21, 0x68, 0xFC, 0x21, 0x75,
|
||||
0xFD, 0x22, 0x6B, 0x72, 0xF3, 0xFD, 0x21, 0x73, 0xFB, 0x21, 0x6C, 0xFD, 0x21, 0x72, 0xFD, 0x41,
|
||||
0x66, 0xE4, 0x4F, 0x21, 0x66, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21, 0x68, 0xFD, 0x21,
|
||||
0x63, 0xFD, 0x21, 0x72, 0xFD, 0x41, 0x6D, 0xE6, 0x76, 0x21, 0x73, 0xFC, 0x21, 0x65, 0xFD, 0x21,
|
||||
0x72, 0xFD, 0x21, 0x67, 0xFD, 0x21, 0x6E, 0xFD, 0x59, 0xC5, 0xC4, 0x62, 0x63, 0x64, 0x66, 0x67,
|
||||
0x6D, 0x6E, 0x70, 0x73, 0x74, 0x7A, 0x6B, 0x2E, 0x68, 0x6A, 0x6C, 0x72, 0x76, 0x77, 0x78, 0x61,
|
||||
0x69, 0x6F, 0xFE, 0x22, 0xF9, 0x82, 0xF9, 0x71, 0xF9, 0x71, 0xF9, 0x71, 0xF9, 0x71, 0xF9, 0x71,
|
||||
0xF9, 0x71, 0xF9, 0x71, 0xF9, 0x71, 0xFF, 0xA9, 0xF9, 0x71, 0xF9, 0x71, 0xF9, 0xA7, 0xF7, 0x21,
|
||||
0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xF7, 0x29, 0xFF, 0xD4,
|
||||
0xFF, 0xEA, 0xFF, 0xFD, 0xA0, 0x14, 0xB1, 0x21, 0x77, 0xFD, 0x21, 0x64, 0xFD, 0x41, 0x66, 0xF9,
|
||||
0xEC, 0x21, 0x66, 0xFC, 0x21, 0x61, 0xFD, 0x21, 0x77, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x72, 0xFE,
|
||||
0x75, 0x21, 0x65, 0xFC, 0x21, 0x66, 0xFD, 0x21, 0x73, 0xFD, 0x22, 0x66, 0x6B, 0xF0, 0xFD, 0x41,
|
||||
0x6F, 0xF7, 0x84, 0x59, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6D, 0x6E,
|
||||
0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6C, 0x2E, 0x76, 0x78, 0x65, 0x75, 0x79, 0xF9, 0x07, 0xFE,
|
||||
0x48, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8,
|
||||
0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8, 0xF6, 0xF8,
|
||||
0xF6, 0xF9, 0x2C, 0xF6, 0xA6, 0xF6, 0xAE, 0xF6, 0xAE, 0xFF, 0xD7, 0xFF, 0xF7, 0xFF, 0xFC, 0xA0,
|
||||
0x15, 0x13, 0xC2, 0x14, 0x82, 0x6E, 0x2E, 0xFF, 0xFD, 0xF6, 0x57, 0x41, 0x74, 0xFB, 0xC9, 0x41,
|
||||
0x6A, 0xFB, 0xC5, 0x22, 0x73, 0x7A, 0xF8, 0xFC, 0xC2, 0x14, 0x82, 0x65, 0x2E, 0xFF, 0xFB, 0xF6,
|
||||
0x41, 0x41, 0x6E, 0xFE, 0xB8, 0xC3, 0x14, 0x82, 0x6B, 0x2E, 0x74, 0xFF, 0xFC, 0xF6, 0x34, 0xF6,
|
||||
0x3C, 0x41, 0x82, 0xFC, 0xA1, 0x43, 0xC5, 0x6C, 0x6E, 0xFF, 0xFC, 0xFC, 0x9D, 0xFC, 0x9D, 0x21,
|
||||
0x7A, 0xF6, 0x21, 0x72, 0xFD, 0x41, 0x7A, 0xFD, 0xCD, 0x21, 0x73, 0xFC, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x21, 0x7A, 0xFD, 0xA0, 0x17, 0x82, 0x21, 0x82, 0xFD, 0x22, 0xC5, 0x69, 0xFD, 0xFA,
|
||||
0x21, 0x7A, 0xFB, 0x21, 0x72, 0xFD, 0x22, 0x73, 0x65, 0xEC, 0xFD, 0x41, 0x6C, 0xD3, 0x22, 0x21,
|
||||
0x61, 0xFC, 0x21, 0x65, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x74, 0xFD, 0x41, 0x6B, 0xE2, 0xC5, 0x21,
|
||||
0x69, 0xFC, 0x21, 0x61, 0xFD, 0x22, 0x6E, 0x7A, 0xF3, 0xFD, 0xA0, 0x17, 0xA3, 0x21, 0x6C, 0xFD,
|
||||
0x21, 0x68, 0xFD, 0x21, 0x63, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x73, 0xFD, 0x21, 0x61, 0xFD, 0x21,
|
||||
0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x5A, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6C, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6D, 0x2E, 0x76, 0x78, 0x61, 0x69, 0x6F, 0x75,
|
||||
0xF8, 0x15, 0xFD, 0x56, 0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x04,
|
||||
0xF8, 0x04, 0xFF, 0x5D, 0xF8, 0x04, 0xFF, 0x73, 0xF8, 0x04, 0xF8, 0x04, 0xFF, 0x80, 0xF8, 0x04,
|
||||
0xF8, 0x04, 0xF8, 0x04, 0xF8, 0x3A, 0xF5, 0xB4, 0xF5, 0xBC, 0xF5, 0xBC, 0xFF, 0x9D, 0xFF, 0xC1,
|
||||
0xFF, 0xE0, 0xFF, 0xFD, 0xC2, 0x14, 0x82, 0x6E, 0x2E, 0xF5, 0xD5, 0xF5, 0x65, 0x41, 0x73, 0xEF,
|
||||
0xFD, 0x21, 0x77, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x82, 0xFD, 0x41, 0x76, 0xEF,
|
||||
0x40, 0x22, 0xC5, 0x72, 0xF9, 0xFC, 0x57, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A,
|
||||
0x6B, 0x6C, 0x6D, 0x70, 0x72, 0x73, 0x74, 0x77, 0x7A, 0x6E, 0x2E, 0x76, 0x78, 0x61, 0xF7, 0xA4,
|
||||
0xFC, 0xE5, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93,
|
||||
0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xF7, 0x93, 0xFF, 0xDE, 0xF7, 0x93,
|
||||
0xF7, 0x93, 0xF7, 0xC9, 0xF5, 0x43, 0xF5, 0x4B, 0xF5, 0x4B, 0xFF, 0xFB, 0x41, 0x75, 0xFA, 0x78,
|
||||
0xC2, 0x14, 0x82, 0x65, 0x2E, 0xFF, 0xFC, 0xF4, 0xF9, 0xA0, 0x14, 0x82, 0xC2, 0x14, 0x82, 0x7A,
|
||||
0x2E, 0xFF, 0xFD, 0xF4, 0xED, 0x42, 0x6E, 0x2E, 0xF5, 0x54, 0xF4, 0xE4, 0x41, 0x6C, 0xF5, 0xB7,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x68, 0xFD, 0x41, 0x61, 0xF8, 0x12, 0x21, 0x70, 0xFC, 0x21, 0x7A, 0xFD,
|
||||
0x21, 0x73, 0xFD, 0x44, 0x2E, 0x7A, 0x63, 0x65, 0xF4, 0xC6, 0xF4, 0xCE, 0xFF, 0xF0, 0xFF, 0xFD,
|
||||
0x41, 0x72, 0xF7, 0x82, 0x21, 0x65, 0xFC, 0x21, 0x6D, 0xFD, 0x21, 0x61, 0xFD, 0x42, 0x61, 0x68,
|
||||
0xD2, 0xC6, 0xD2, 0xC3, 0x21, 0x63, 0xF9, 0x22, 0x6E, 0x73, 0xF3, 0xFD, 0x41, 0x69, 0xCD, 0x37,
|
||||
0x21, 0x6E, 0xFC, 0x21, 0x64, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x82, 0xFD, 0x41, 0x7A, 0xD2, 0x7C,
|
||||
0x21, 0x72, 0xFC, 0x21, 0x70, 0xFD, 0x21, 0x65, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6E, 0xFD, 0x42,
|
||||
0x85, 0x99, 0xE1, 0x58, 0xE1, 0x58, 0x42, 0xC4, 0x69, 0xFF, 0xF9, 0xE1, 0x51, 0x21, 0x6E, 0xF9,
|
||||
0xA0, 0x17, 0xD4, 0x21, 0x68, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x75, 0xFD, 0x21, 0x6F, 0xFD, 0x21,
|
||||
0x6D, 0xFD, 0x41, 0x64, 0xEE, 0xF8, 0x21, 0x6E, 0xFC, 0x21, 0x61, 0xFD, 0x22, 0x73, 0x6C, 0xF3,
|
||||
0xFD, 0x21, 0x74, 0xFB, 0x41, 0x65, 0xD2, 0x3D, 0x21, 0x72, 0xFC, 0x42, 0x65, 0x75, 0xD4, 0xF0,
|
||||
0xFF, 0xFD, 0x21, 0x69, 0xF9, 0x41, 0x69, 0xCD, 0xBE, 0x21, 0x6E, 0xFC, 0x21, 0x64, 0xFD, 0x21,
|
||||
0x65, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD, 0x46, 0xC5, 0x64, 0x6D, 0x72, 0x6C, 0x77, 0xFF,
|
||||
0x91, 0xFF, 0xA4, 0xFF, 0xB5, 0xFF, 0xD9, 0xFF, 0xEA, 0xFF, 0xFD, 0x58, 0xC4, 0xC5, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x6B, 0x6D, 0x6E, 0x73, 0x74, 0x7A, 0x6C, 0x70, 0x2E, 0x68, 0x6A, 0x72, 0x76,
|
||||
0x77, 0x78, 0x61, 0x6F, 0xF6, 0x6F, 0xF7, 0xB3, 0xF6, 0x5E, 0xF6, 0x5E, 0xF6, 0x5E, 0xF6, 0x5E,
|
||||
0xF6, 0x5E, 0xF6, 0x5E, 0xF6, 0x5E, 0xFF, 0x15, 0xFF, 0x21, 0xF6, 0x5E, 0xF6, 0x5E, 0xFF, 0x2A,
|
||||
0xF6, 0x94, 0xF4, 0x0E, 0xF4, 0x16, 0xF4, 0x16, 0xFF, 0x48, 0xF4, 0x16, 0xF4, 0x16, 0xF4, 0x16,
|
||||
0xFF, 0x6C, 0xFF, 0xED, 0xC2, 0x14, 0x82, 0x2E, 0x7A, 0xF3, 0xC5, 0xF3, 0xCD, 0x45, 0x82, 0x84,
|
||||
0x9B, 0xBA, 0xBC, 0xF3, 0xBF, 0xF7, 0x71, 0xF7, 0x71, 0xF7, 0x71, 0xF7, 0x71, 0xD3, 0x02, 0x31,
|
||||
0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73,
|
||||
0x74, 0x77, 0x2E, 0xF7, 0x64, 0xFF, 0xF0, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7,
|
||||
0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7,
|
||||
0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF7, 0x61, 0xF3, 0xAC, 0x41, 0x6E, 0xF6, 0x1C, 0x21, 0x65, 0xFC,
|
||||
0x21, 0x67, 0xFD, 0x21, 0x74, 0xFD, 0x21, 0x6E, 0xFD, 0x41, 0x7A, 0xD1, 0x77, 0x21, 0x63, 0xFC,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x72, 0xFD, 0xA0, 0x18, 0x14, 0x21, 0x6B, 0xFD, 0x21, 0x63, 0xFD, 0x21,
|
||||
0x6F, 0xFD, 0x21, 0x74, 0xFD, 0x23, 0x65, 0x6B, 0x73, 0xE1, 0xEE, 0xFD, 0x57, 0xC4, 0xC5, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0x72,
|
||||
0x2E, 0x76, 0x78, 0x6F, 0xF5, 0x9E, 0xFA, 0xDF, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D,
|
||||
0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D, 0xF5, 0x8D,
|
||||
0xFF, 0x78, 0xF5, 0x8D, 0xF5, 0x8D, 0xFF, 0x91, 0xF5, 0xC3, 0xF3, 0x3D, 0xF3, 0x45, 0xF3, 0x45,
|
||||
0xFF, 0xF9, 0x45, 0x82, 0xBA, 0xBC, 0x84, 0x9B, 0xF9, 0xF1, 0xF5, 0x47, 0xF5, 0x47, 0xF2, 0xFF,
|
||||
0xF2, 0xFF, 0x42, 0x2E, 0x7A, 0xF2, 0xE7, 0xF2, 0xEF, 0x46, 0x6B, 0x6E, 0x73, 0x2E, 0x72, 0x77,
|
||||
0xF3, 0x50, 0xF3, 0x50, 0xF7, 0xA5, 0xF2, 0xE0, 0xFF, 0xF9, 0xF2, 0xE8, 0x41, 0x9B, 0xF6, 0x82,
|
||||
0x42, 0x2E, 0x62, 0xF2, 0xC9, 0xF2, 0xD1, 0xA1, 0x14, 0x62, 0x7A, 0xF9, 0xC1, 0x14, 0x62, 0x6E,
|
||||
0xF3, 0x2D, 0x41, 0x6D, 0xF9, 0xC8, 0x21, 0x6C, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x68, 0xFD, 0x21,
|
||||
0x6B, 0xFD, 0xC3, 0x14, 0x62, 0x2E, 0x72, 0x6F, 0xF2, 0xA7, 0xF2, 0xAF, 0xFF, 0xFD, 0xA0, 0x18,
|
||||
0x52, 0x21, 0x79, 0xFD, 0x21, 0x82, 0xFD, 0x21, 0xC5, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x73, 0xFD,
|
||||
0x41, 0x67, 0xF5, 0x15, 0x21, 0x6E, 0xFC, 0x21, 0x6F, 0xFD, 0x21, 0x6C, 0xFD, 0x41, 0x87, 0xCA,
|
||||
0xC6, 0x21, 0xC4, 0xFC, 0x21, 0x9B, 0xFD, 0x22, 0x7A, 0xC5, 0xF3, 0xFD, 0x41, 0x73, 0xF9, 0x7E,
|
||||
0x21, 0x61, 0xFC, 0x21, 0x77, 0xFD, 0x21, 0x6B, 0xFD, 0x21, 0x6E, 0xFD, 0xD1, 0x02, 0x31, 0xC4,
|
||||
0xC5, 0x63, 0x66, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x73, 0x74, 0x77, 0x7A, 0x2E, 0x61, 0x65, 0x79,
|
||||
0xF6, 0x15, 0xFF, 0x90, 0xFF, 0x9B, 0xF6, 0x12, 0xF2, 0x60, 0xFF, 0xA0, 0xF6, 0x12, 0xF2, 0x60,
|
||||
0xF6, 0x12, 0xF6, 0x12, 0xFF, 0xB6, 0xF6, 0x12, 0xF6, 0x12, 0xF2, 0x5D, 0xFF, 0xD1, 0xFF, 0xEB,
|
||||
0xFF, 0xFD, 0x42, 0x2E, 0x68, 0xF2, 0x27, 0xF2, 0x2F, 0x41, 0x7A, 0xF2, 0x28, 0x41, 0x72, 0xFC,
|
||||
0x18, 0x21, 0x65, 0xFC, 0x21, 0x6E, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD,
|
||||
0x43, 0x2E, 0x72, 0x6F, 0xF2, 0x09, 0xFF, 0xE9, 0xFF, 0xFD, 0x42, 0x2E, 0x72, 0xF1, 0xFF, 0xF2,
|
||||
0xD9, 0x41, 0x65, 0xD0, 0x0F, 0x21, 0x6C, 0xFC, 0x21, 0x74, 0xFD, 0x41, 0x7A, 0xCF, 0xE6, 0x21,
|
||||
0x69, 0xFC, 0x21, 0x77, 0xFD, 0x41, 0x74, 0xCF, 0xFB, 0x21, 0x70, 0xFC, 0x21, 0x6F, 0xFD, 0x41,
|
||||
0x6D, 0xCE, 0x7F, 0x21, 0x65, 0xFC, 0x21, 0x74, 0xFD, 0x22, 0x6E, 0x73, 0xF3, 0xFD, 0x59, 0xC5,
|
||||
0x62, 0x64, 0x66, 0x67, 0x73, 0x6E, 0x74, 0x7A, 0x2E, 0xC4, 0x63, 0x68, 0x6A, 0x6B, 0x6C, 0x6D,
|
||||
0x70, 0x72, 0x76, 0x77, 0x78, 0x65, 0x6F, 0x79, 0xFE, 0xD4, 0xF4, 0x1B, 0xF4, 0x1B, 0xF4, 0x1B,
|
||||
0xF4, 0x1B, 0xF4, 0x1B, 0xF4, 0x14, 0xFE, 0xEB, 0xFF, 0x6E, 0xF1, 0xCB, 0xF8, 0x14, 0xFF, 0xA4,
|
||||
0xF1, 0xD3, 0xF1, 0xD3, 0xFF, 0xC2, 0xF1, 0xD3, 0xFF, 0xCC, 0xF1, 0xD3, 0xF1, 0xD3, 0xF1, 0xD3,
|
||||
0xF1, 0xD3, 0xF1, 0xD3, 0xFF, 0xDA, 0xFF, 0xE4, 0xFF, 0xFB, 0x43, 0x6B, 0x7A, 0x2E, 0xF1, 0xEF,
|
||||
0xFC, 0x9B, 0xF1, 0x7F, 0xA0, 0x18, 0x73, 0x21, 0x74, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x79, 0xFD,
|
||||
0x41, 0x74, 0xE0, 0xAB, 0x21, 0x75, 0xFC, 0x21, 0x7A, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x64, 0xFD,
|
||||
0x21, 0x6F, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x62, 0xFD, 0x21, 0x72, 0xFD, 0x41, 0x69, 0xCF, 0x45,
|
||||
0x21, 0x6E, 0xFC, 0x21, 0x64, 0xFD, 0x21, 0x6F, 0xFD, 0x21, 0x67, 0xFD, 0x59, 0xC4, 0xC5, 0x62,
|
||||
0x63, 0x64, 0x66, 0x67, 0x6B, 0x6D, 0x6E, 0x70, 0x73, 0x7A, 0x6C, 0x72, 0x74, 0x2E, 0x68, 0x6A,
|
||||
0x76, 0x77, 0x78, 0x6F, 0x75, 0x79, 0xF3, 0x9E, 0xF4, 0xE2, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D,
|
||||
0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D, 0xF3, 0x8D,
|
||||
0xFC, 0x59, 0xFF, 0xBE, 0xF3, 0xC3, 0xF1, 0x3D, 0xF1, 0x45, 0xF1, 0x45, 0xF1, 0x45, 0xF1, 0x45,
|
||||
0xF1, 0x45, 0xFF, 0xD1, 0xFF, 0xED, 0xFF, 0xFD, 0x41, 0x64, 0xF6, 0x6C, 0x41, 0x73, 0xF6, 0x68,
|
||||
0xC3, 0x14, 0x82, 0x61, 0x6F, 0x2E, 0xFF, 0xF8, 0xFF, 0xFC, 0xF0, 0xE9, 0x45, 0x82, 0x84, 0x9B,
|
||||
0xBA, 0xBC, 0xFF, 0xF4, 0xF3, 0x2D, 0xF3, 0x2D, 0xF3, 0x2D, 0xF3, 0x2D, 0x21, 0x61, 0xE0, 0xC2,
|
||||
0x14, 0x82, 0x7A, 0x2E, 0xFF, 0xFD, 0xF0, 0xCA, 0xA0, 0x18, 0xA3, 0x21, 0x64, 0xFD, 0x21, 0x6E,
|
||||
0xFD, 0x21, 0x65, 0xFD, 0x21, 0x6B, 0xFD, 0x42, 0x66, 0x6D, 0xEB, 0x53, 0xEB, 0x53, 0x21, 0x74,
|
||||
0xF9, 0x22, 0x65, 0x73, 0xF3, 0xFD, 0x57, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x7A, 0x77, 0x2E, 0x68, 0x76, 0x78, 0x65, 0xF3, 0x04,
|
||||
0xFF, 0xC6, 0xF2, 0xF3, 0xFF, 0xD9, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3,
|
||||
0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3, 0xF2, 0xF3,
|
||||
0xF3, 0x29, 0xF0, 0xA3, 0xF0, 0xAB, 0xF0, 0xAB, 0xF0, 0xAB, 0xFF, 0xFB, 0x45, 0x9B, 0x82, 0x84,
|
||||
0xBA, 0xBC, 0xF2, 0xAD, 0xF0, 0x65, 0xF0, 0x65, 0xF0, 0x65, 0xF0, 0x65, 0xC4, 0x14, 0x82, 0x6B,
|
||||
0x6E, 0x2E, 0x72, 0xF0, 0xBD, 0xF0, 0xBD, 0xF0, 0x4D, 0xFD, 0x66, 0x41, 0x70, 0xEA, 0x32, 0x21,
|
||||
0x70, 0xFC, 0x57, 0xC4, 0xC5, 0x63, 0x64, 0x66, 0x6B, 0x70, 0x73, 0x74, 0x7A, 0x2E, 0x62, 0x67,
|
||||
0x68, 0x6A, 0x6C, 0x6D, 0x6E, 0x72, 0x76, 0x77, 0x78, 0x65, 0xF2, 0x98, 0xFF, 0xDA, 0xF2, 0x87,
|
||||
0xFF, 0xEA, 0xF2, 0x87, 0xF2, 0x87, 0xF2, 0x87, 0xF2, 0x87, 0xF2, 0x87, 0xF2, 0xBD, 0xF0, 0x37,
|
||||
0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F,
|
||||
0xF0, 0x3F, 0xF0, 0x3F, 0xF0, 0x3F, 0xFF, 0xFD, 0xA0, 0x15, 0xB3, 0x21, 0x73, 0xFD, 0x21, 0x6B,
|
||||
0xFD, 0x21, 0x6C, 0xFD, 0x57, 0x2E, 0xC4, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B,
|
||||
0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78, 0x7A, 0x6F, 0xEF, 0xE5, 0xF6, 0x2E,
|
||||
0xF6, 0x32, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED,
|
||||
0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED,
|
||||
0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xEF, 0xED, 0xFF, 0xFD, 0x56, 0x2E, 0xC4, 0xC5, 0x62, 0x63,
|
||||
0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72, 0x73, 0x74, 0x76, 0x77, 0x78,
|
||||
0x7A, 0xEF, 0x9F, 0xF5, 0xE8, 0xF5, 0xEC, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF,
|
||||
0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF,
|
||||
0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xEF, 0xA7, 0xA0, 0x00, 0xD1,
|
||||
0xC6, 0x02, 0x61, 0x75, 0x79, 0x61, 0x65, 0x69, 0x6F, 0xFF, 0xFD, 0xFF, 0xFD, 0xF0, 0x33, 0xF0,
|
||||
0x33, 0xF0, 0x33, 0xF0, 0x33, 0xA0, 0x04, 0xF2, 0x21, 0x63, 0xFD, 0x21, 0x61, 0xFD, 0x21, 0x7A,
|
||||
0xFD, 0xA0, 0x04, 0xE2, 0x21, 0x7A, 0xFD, 0xA1, 0x00, 0xD1, 0x73, 0xFD, 0xC7, 0x02, 0x61, 0x72,
|
||||
0x75, 0x79, 0x61, 0x65, 0x69, 0x6F, 0xFF, 0xF2, 0xFF, 0xFB, 0xFF, 0xD1, 0xF0, 0x07, 0xF0, 0x07,
|
||||
0xF0, 0x07, 0xF0, 0x07, 0x22, 0x85, 0x99, 0xB9, 0xB9, 0x21, 0xB3, 0xB4, 0x41, 0x6B, 0xE9, 0xB1,
|
||||
0x21, 0x63, 0xFC, 0x21, 0x75, 0xFD, 0x21, 0x72, 0xFD, 0x21, 0x62, 0xFD, 0x21, 0x73, 0xFD, 0x41,
|
||||
0x63, 0xE9, 0x9E, 0x21, 0x75, 0xFC, 0x21, 0x72, 0xFD, 0x21, 0x62, 0xFD, 0x22, 0x6E, 0x73, 0xF0,
|
||||
0xFD, 0xC9, 0x02, 0x61, 0xC4, 0xC3, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0x6E, 0xFF, 0xD3, 0xFF,
|
||||
0xD8, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0x8C, 0xFF, 0xFB, 0x41,
|
||||
0x72, 0xDB, 0xBC, 0x21, 0x74, 0xFC, 0x21, 0x73, 0xFD, 0x21, 0x69, 0xFD, 0x21, 0x6D, 0xFD, 0x21,
|
||||
0x68, 0xFD, 0x41, 0x65, 0xFC, 0xD3, 0x21, 0x73, 0xFC, 0x21, 0x66, 0xFD, 0xC8, 0x02, 0x61, 0x79,
|
||||
0x61, 0x63, 0x65, 0x66, 0x69, 0x6F, 0x75, 0xFF, 0x51, 0xEF, 0x87, 0xFF, 0xF3, 0xEF, 0x87, 0xFF,
|
||||
0xFD, 0xEF, 0x87, 0xEF, 0x87, 0xEF, 0x87, 0xC6, 0x02, 0x61, 0x79, 0x61, 0x65, 0x69, 0x6F, 0x75,
|
||||
0xFF, 0x36, 0xEF, 0x6C, 0xEF, 0x6C, 0xEF, 0x6C, 0xEF, 0x6C, 0xEF, 0x6C, 0xC5, 0x02, 0x61, 0x61,
|
||||
0x65, 0x69, 0x6F, 0x75, 0xEF, 0x57, 0xEF, 0x57, 0xEF, 0x57, 0xEF, 0x57, 0xEF, 0x57, 0x5D, 0x2E,
|
||||
0xC4, 0xC3, 0xC5, 0x62, 0x63, 0x64, 0x66, 0x67, 0x68, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x70, 0x72,
|
||||
0x73, 0x74, 0x77, 0x7A, 0x76, 0x78, 0x61, 0x65, 0x69, 0x6F, 0x75, 0x79, 0xEE, 0x19, 0xEE, 0xB6,
|
||||
0xEE, 0xCC, 0xF0, 0xA4, 0xF1, 0xC1, 0xF2, 0xE7, 0xF4, 0x6B, 0xF5, 0x1C, 0xF5, 0xC4, 0xF6, 0x3C,
|
||||
0xF6, 0xA5, 0xF7, 0x4A, 0xF7, 0xC5, 0xF8, 0xB7, 0xF9, 0x28, 0xFA, 0x5D, 0xFB, 0x2E, 0xFC, 0xA0,
|
||||
0xFD, 0x2E, 0xFD, 0xC8, 0xFE, 0x34, 0xFE, 0x86, 0xFE, 0xCC, 0xFF, 0x12, 0xFF, 0x3E, 0xFF, 0x83,
|
||||
0xFF, 0xBE, 0xFF, 0xD9, 0xFF, 0xEE,
|
||||
};
|
||||
|
||||
constexpr SerializedHyphenationPatterns pl_patterns = {
|
||||
0x3C4Eu,
|
||||
pl_trie_data,
|
||||
sizeof(pl_trie_data),
|
||||
};
|
||||
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
@@ -1,182 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <epub/Expat.h> // FreeInkBook's vendored expat
|
||||
|
||||
#include <climits>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub/FootnoteEntry.h"
|
||||
#include "Epub/ParsedText.h"
|
||||
#include "Epub/blocks/ImageBlock.h"
|
||||
#include "Epub/blocks/TextBlock.h"
|
||||
#include "Epub/css/CssParser.h"
|
||||
#include "Epub/css/CssStyle.h"
|
||||
|
||||
class Page;
|
||||
class GfxRenderer;
|
||||
class Epub;
|
||||
|
||||
#define MAX_WORD_SIZE 200
|
||||
|
||||
class ChapterHtmlSlimParser {
|
||||
std::shared_ptr<Epub> epub;
|
||||
const std::string& filepath;
|
||||
GfxRenderer& renderer;
|
||||
std::function<void(std::unique_ptr<Page>, uint16_t, uint16_t)> completePageFn;
|
||||
std::function<void()> popupFn; // Popup callback
|
||||
int depth = 0;
|
||||
int skipUntilDepth = INT_MAX;
|
||||
int boldUntilDepth = INT_MAX;
|
||||
int italicUntilDepth = INT_MAX;
|
||||
// buffer for building up words from characters, will auto break if longer than this
|
||||
// leave one char at end for null pointer
|
||||
char partWordBuffer[MAX_WORD_SIZE + 1] = {};
|
||||
int partWordBufferIndex = 0;
|
||||
bool nextWordContinues = false; // true when next flushed word attaches to previous (inline element boundary)
|
||||
std::unique_ptr<ParsedText> currentTextBlock = nullptr;
|
||||
std::unique_ptr<Page> currentPage = nullptr;
|
||||
int16_t currentPageNextY = 0;
|
||||
int fontId;
|
||||
float lineCompression;
|
||||
bool extraParagraphSpacing;
|
||||
uint8_t paragraphAlignment;
|
||||
uint16_t viewportWidth;
|
||||
uint16_t viewportHeight;
|
||||
bool hyphenationEnabled;
|
||||
bool focusReadingEnabled;
|
||||
const CssParser* cssParser;
|
||||
bool embeddedStyle;
|
||||
uint8_t imageRendering;
|
||||
std::string contentBase;
|
||||
std::string imageBasePath;
|
||||
int imageCounter = 0;
|
||||
|
||||
// Style tracking (replaces depth-based approach)
|
||||
struct StyleStackEntry {
|
||||
int depth = 0;
|
||||
bool hasBold = false, bold = false;
|
||||
bool hasItalic = false, italic = false;
|
||||
bool hasTextDecoration = false;
|
||||
CssTextDecoration textDecoration = CssTextDecoration::None;
|
||||
bool hasDirection = false;
|
||||
CssTextDirection direction = CssTextDirection::Ltr;
|
||||
bool hasSup = false, sup = false;
|
||||
bool hasSub = false, sub = false;
|
||||
};
|
||||
std::vector<StyleStackEntry> inlineStyleStack;
|
||||
std::vector<BlockStyle> blockStyleStack; // accumulated block styles from open ancestor elements
|
||||
CssStyle currentCssStyle;
|
||||
bool effectiveBold = false;
|
||||
bool effectiveItalic = false;
|
||||
CssTextDecoration effectiveTextDecoration = CssTextDecoration::None;
|
||||
bool effectiveDirectionDefined = false;
|
||||
CssTextDirection effectiveDirection = CssTextDirection::Ltr;
|
||||
bool effectiveSup = false;
|
||||
bool effectiveSub = false;
|
||||
int tableDepth = 0;
|
||||
int tableRowIndex = 0;
|
||||
int tableColIndex = 0;
|
||||
|
||||
// Anchor-to-page mapping: tracks which page each HTML id attribute lands on
|
||||
int completedPageCount = 0;
|
||||
std::vector<std::pair<std::string, uint16_t>> anchorData;
|
||||
std::string pendingAnchorId; // deferred until after previous text block is flushed
|
||||
std::vector<std::string> tocAnchors; // the list of anchors that are TOC chapter boundaries
|
||||
uint16_t xpathParagraphIndex = 0;
|
||||
uint16_t xpathListItemIndex = 0;
|
||||
|
||||
// Footnote link tracking
|
||||
bool insideFootnoteLink = false;
|
||||
int footnoteLinkDepth = -1;
|
||||
FootnoteEntry currentFootnote = {};
|
||||
int currentFootnoteLinkTextLen = 0;
|
||||
std::vector<std::pair<int, FootnoteEntry>> pendingFootnotes; // <wordIndex, entry>
|
||||
int wordsExtractedInBlock = 0;
|
||||
|
||||
// Resumable parse state. The one-shot parseAndBuildPages() drives these
|
||||
// internally; the incremental section builder drives them across render ticks
|
||||
// so a large single chapter can yield between pages instead of blocking the UI
|
||||
// until the whole thing is laid out. parseFile_ and the expat parser stay alive
|
||||
// for the lifetime of the parse so it can be paused and resumed at buffer
|
||||
// boundaries.
|
||||
XML_Parser xmlParser_ = nullptr;
|
||||
HalFile parseFile_;
|
||||
uint32_t parseStartTime_ = 0;
|
||||
|
||||
void updateEffectiveInlineStyle();
|
||||
void startNewTextBlock(const BlockStyle& blockStyle);
|
||||
void flushPendingAnchor();
|
||||
void flushPartWordBuffer();
|
||||
void makePages();
|
||||
static EpdFontFamily::Style fontStyleForTextDecoration(CssTextDecoration decoration);
|
||||
static void applyDirectionToEntry(StyleStackEntry& entry, const CssStyle& css);
|
||||
static void applyTextDecorationToEntry(StyleStackEntry& entry, const CssStyle& css);
|
||||
void pushDecorationStyleEntry(CssTextDecoration defaultDecoration, const CssStyle& cssStyle);
|
||||
void emitHorizontalRule(const BlockStyle& blockStyle);
|
||||
// XML callbacks
|
||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void XMLCALL characterData(void* userData, const XML_Char* s, int len);
|
||||
static void XMLCALL defaultHandlerExpand(void* userData, const XML_Char* s, int len);
|
||||
static void XMLCALL endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
explicit ChapterHtmlSlimParser(std::shared_ptr<Epub> epub, const std::string& filepath, GfxRenderer& renderer,
|
||||
const int fontId, const float lineCompression, const bool extraParagraphSpacing,
|
||||
const uint8_t paragraphAlignment, const uint16_t viewportWidth,
|
||||
const uint16_t viewportHeight, const bool hyphenationEnabled,
|
||||
const bool focusReadingEnabled,
|
||||
const std::function<void(std::unique_ptr<Page>, uint16_t, uint16_t)>& completePageFn,
|
||||
const bool embeddedStyle, const std::string& contentBase,
|
||||
const std::string& imageBasePath, const uint8_t imageRendering = 0,
|
||||
std::vector<std::string> tocAnchors = {},
|
||||
const std::function<void()>& popupFn = nullptr, const CssParser* cssParser = nullptr)
|
||||
|
||||
: epub(epub),
|
||||
filepath(filepath),
|
||||
renderer(renderer),
|
||||
fontId(fontId),
|
||||
lineCompression(lineCompression),
|
||||
extraParagraphSpacing(extraParagraphSpacing),
|
||||
paragraphAlignment(paragraphAlignment),
|
||||
viewportWidth(viewportWidth),
|
||||
viewportHeight(viewportHeight),
|
||||
hyphenationEnabled(hyphenationEnabled),
|
||||
focusReadingEnabled(focusReadingEnabled),
|
||||
completePageFn(completePageFn),
|
||||
popupFn(popupFn),
|
||||
cssParser(cssParser),
|
||||
embeddedStyle(embeddedStyle),
|
||||
imageRendering(imageRendering),
|
||||
contentBase(contentBase),
|
||||
imageBasePath(imageBasePath),
|
||||
tocAnchors(std::move(tocAnchors)) {}
|
||||
|
||||
~ChapterHtmlSlimParser();
|
||||
|
||||
// One-shot parse: builds every page before returning (begin + step* + finish).
|
||||
bool parseAndBuildPages();
|
||||
|
||||
// Resumable parse, for the incremental section builder. Drive as:
|
||||
// if (!beginParse()) fail;
|
||||
// loop: switch (parseStep()) { More: keep going / yield; Done: finishParse(); Error: abortParse(); }
|
||||
// Pages are emitted via completePageFn as they complete during parseStep(), so
|
||||
// the caller can stop once enough pages are built and resume on a later tick.
|
||||
enum class ParseStatus { More, Done, Error };
|
||||
bool beginParse();
|
||||
ParseStatus parseStep();
|
||||
bool finishParse(); // flush the trailing page and tear down; returns true
|
||||
void abortParse(); // tear down without flushing (error / abandon)
|
||||
|
||||
void addLineToPage(std::shared_ptr<TextBlock> line);
|
||||
const std::vector<std::pair<std::string, uint16_t>>& getAnchors() const { return anchorData; }
|
||||
|
||||
// Byte progress of the in-flight parse, used to estimate a still-building section's total page
|
||||
// count (a giant single-spine book never fully lays out, so its real count is unknown). Valid
|
||||
// between beginParse() and finishParse()/abortParse().
|
||||
size_t parseBytesConsumed() { return parseFile_ ? parseFile_.position() : 0; }
|
||||
size_t parseTotalBytes() { return parseFile_ ? parseFile_.size() : 0; }
|
||||
};
|
||||
@@ -1,93 +0,0 @@
|
||||
#include "ContainerParser.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <XmlParserUtils.h>
|
||||
|
||||
bool ContainerParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
LOG_ERR("CTR", "Couldn't allocate memory for parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
return true;
|
||||
}
|
||||
|
||||
ContainerParser::~ContainerParser() { destroyXmlParser(parser); }
|
||||
|
||||
size_t ContainerParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
size_t ContainerParser::write(const uint8_t* buffer, const size_t size) {
|
||||
if (!parser) return 0;
|
||||
|
||||
const uint8_t* currentBufferPos = buffer;
|
||||
auto remainingInBuffer = size;
|
||||
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
LOG_DBG("CTR", "Couldn't allocate buffer");
|
||||
destroyXmlParser(parser);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
LOG_ERR("CTR", "Parse error: %s", XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
destroyXmlParser(parser);
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentBufferPos += toRead;
|
||||
remainingInBuffer -= toRead;
|
||||
remainingSize -= toRead;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
void XMLCALL ContainerParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<ContainerParser*>(userData);
|
||||
|
||||
// Simple state tracking to ensure we are looking at the valid schema structure
|
||||
if (self->state == START && strcmp(name, "container") == 0) {
|
||||
self->state = IN_CONTAINER;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_CONTAINER && strcmp(name, "rootfiles") == 0) {
|
||||
self->state = IN_ROOTFILES;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_ROOTFILES && strcmp(name, "rootfile") == 0) {
|
||||
const char* mediaType = nullptr;
|
||||
const char* path = nullptr;
|
||||
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "media-type") == 0) {
|
||||
mediaType = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "full-path") == 0) {
|
||||
path = atts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this is the standard OEBPS package
|
||||
if (mediaType && path && strcmp(mediaType, "application/oebps-package+xml") == 0) {
|
||||
self->fullPath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL ContainerParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<ContainerParser*>(userData);
|
||||
|
||||
if (self->state == IN_ROOTFILES && strcmp(name, "rootfiles") == 0) {
|
||||
self->state = IN_CONTAINER;
|
||||
} else if (self->state == IN_CONTAINER && strcmp(name, "container") == 0) {
|
||||
self->state = START;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <epub/Expat.h> // FreeInkBook's vendored expat
|
||||
|
||||
class ContainerParser final : public Print {
|
||||
enum ParserState {
|
||||
START,
|
||||
IN_CONTAINER,
|
||||
IN_ROOTFILES,
|
||||
};
|
||||
|
||||
size_t remainingSize;
|
||||
XML_Parser parser = nullptr;
|
||||
ParserState state = START;
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
std::string fullPath;
|
||||
|
||||
explicit ContainerParser(const size_t xmlSize) : remainingSize(xmlSize) {}
|
||||
~ContainerParser() override;
|
||||
|
||||
bool setup();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
};
|
||||
@@ -1,402 +0,0 @@
|
||||
#include "ContentOpfParser.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <Logging.h>
|
||||
#include <Serialization.h>
|
||||
#include <XmlParserUtils.h>
|
||||
|
||||
#include <cctype>
|
||||
|
||||
#include "Epub/BookMetadataCache.h"
|
||||
|
||||
namespace {
|
||||
constexpr char MEDIA_TYPE_NCX[] = "application/x-dtbncx+xml";
|
||||
constexpr char MEDIA_TYPE_CSS[] = "text/css";
|
||||
constexpr char MEDIA_TYPE_IMAGE_PREFIX[] = "image/";
|
||||
constexpr char itemCacheFile[] = "/.items.bin";
|
||||
|
||||
bool startsWithImageMediaType(const std::string& mediaType) {
|
||||
constexpr size_t prefixLen = sizeof(MEDIA_TYPE_IMAGE_PREFIX) - 1;
|
||||
if (mediaType.size() < prefixLen) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < prefixLen; ++i) {
|
||||
const char c = static_cast<char>(std::tolower(static_cast<unsigned char>(mediaType[i])));
|
||||
if (c != MEDIA_TYPE_IMAGE_PREFIX[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ContentOpfParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
LOG_DBG("COF", "Couldn't allocate memory for parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(parser, characterData);
|
||||
return true;
|
||||
}
|
||||
|
||||
ContentOpfParser::~ContentOpfParser() {
|
||||
destroyXmlParser(parser);
|
||||
if (tempItemStore) {
|
||||
tempItemStore.close();
|
||||
}
|
||||
const auto itemCachePath = cachePath + itemCacheFile;
|
||||
if (Storage.exists(itemCachePath.c_str())) {
|
||||
Storage.remove(itemCachePath.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
size_t ContentOpfParser::write(const uint8_t* buffer, const size_t size) {
|
||||
if (!parser) return 0;
|
||||
|
||||
const uint8_t* currentBufferPos = buffer;
|
||||
auto remainingInBuffer = size;
|
||||
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
|
||||
if (!buf) {
|
||||
LOG_ERR("COF", "Couldn't allocate memory for buffer");
|
||||
destroyXmlParser(parser);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
LOG_DBG("COF", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
destroyXmlParser(parser);
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentBufferPos += toRead;
|
||||
remainingInBuffer -= toRead;
|
||||
remainingSize -= toRead;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<ContentOpfParser*>(userData);
|
||||
(void)atts;
|
||||
|
||||
if (self->state == START && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
|
||||
self->state = IN_PACKAGE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
|
||||
self->state = IN_METADATA;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_METADATA && strcmp(name, "dc:title") == 0) {
|
||||
// Only capture the first dc:title element; subsequent ones are subtitles
|
||||
if (self->title.empty()) {
|
||||
self->state = IN_BOOK_TITLE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_METADATA && strcmp(name, "dc:creator") == 0) {
|
||||
self->state = IN_BOOK_AUTHOR;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_METADATA && strcmp(name, "dc:language") == 0) {
|
||||
self->state = IN_BOOK_LANGUAGE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||
self->state = IN_MANIFEST;
|
||||
if (!Storage.openFileForWrite("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
LOG_ERR("COF", "Couldn't open temp items file for writing. This is probably going to be a fatal error.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||
self->state = IN_SPINE;
|
||||
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||
}
|
||||
|
||||
// Sort item index for binary search if we have enough items
|
||||
if (self->itemIndex.size() >= LARGE_SPINE_THRESHOLD) {
|
||||
std::sort(self->itemIndex.begin(), self->itemIndex.end(), [](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||
});
|
||||
self->useItemIndex = true;
|
||||
LOG_DBG("COF", "Using fast index for %zu manifest items", self->itemIndex.size());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||
self->state = IN_GUIDE;
|
||||
// TODO Remove print
|
||||
LOG_DBG("COF", "Entering guide state.");
|
||||
if (!Storage.openFileForRead("COF", self->cachePath + itemCacheFile, self->tempItemStore)) {
|
||||
LOG_ERR("COF", "Couldn't open temp items file for reading. This is probably going to be a fatal error.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_METADATA && (strcmp(name, "meta") == 0 || strcmp(name, "opf:meta") == 0)) {
|
||||
bool isCover = false;
|
||||
std::string coverItemId;
|
||||
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "name") == 0 && strcmp(atts[i + 1], "cover") == 0) {
|
||||
isCover = true;
|
||||
} else if (strcmp(atts[i], "content") == 0) {
|
||||
coverItemId = atts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (isCover) {
|
||||
self->coverItemId = coverItemId;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_MANIFEST && (strcmp(name, "item") == 0 || strcmp(name, "opf:item") == 0)) {
|
||||
std::string itemId;
|
||||
std::string href;
|
||||
std::string mediaType;
|
||||
std::string properties;
|
||||
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "id") == 0) {
|
||||
itemId = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "href") == 0) {
|
||||
href = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(self->baseContentPath + atts[i + 1]));
|
||||
} else if (strcmp(atts[i], "media-type") == 0) {
|
||||
mediaType = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "properties") == 0) {
|
||||
properties = atts[i + 1];
|
||||
}
|
||||
}
|
||||
|
||||
// Record index entry for fast lookup later
|
||||
if (self->tempItemStore) {
|
||||
ItemIndexEntry entry;
|
||||
entry.idHash = fnvHash(itemId);
|
||||
entry.idLen = static_cast<uint16_t>(itemId.size());
|
||||
entry.fileOffset = static_cast<uint32_t>(self->tempItemStore.position());
|
||||
self->itemIndex.push_back(entry);
|
||||
}
|
||||
|
||||
// Write items down to SD card
|
||||
serialization::writeString(self->tempItemStore, itemId);
|
||||
serialization::writeString(self->tempItemStore, href);
|
||||
|
||||
if (itemId == self->coverItemId) {
|
||||
// Some EPUBs set meta name="cover" to an XHTML wrapper item.
|
||||
// Only treat it as a cover image when the manifest media-type is image/*.
|
||||
if (startsWithImageMediaType(mediaType)) {
|
||||
self->coverItemHref = href;
|
||||
} else {
|
||||
LOG_DBG("COF", "Ignoring meta cover item '%s' with non-image media type: %s", itemId.c_str(),
|
||||
mediaType.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaType == MEDIA_TYPE_NCX) {
|
||||
if (self->tocNcxPath.empty()) {
|
||||
self->tocNcxPath = href;
|
||||
} else {
|
||||
LOG_DBG("COF", "Warning: Multiple NCX files found in manifest. Ignoring duplicate: %s", href.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Collect CSS files
|
||||
if (mediaType == MEDIA_TYPE_CSS) {
|
||||
self->cssFiles.push_back(href);
|
||||
}
|
||||
|
||||
// EPUB 3: Check for nav document (properties contains "nav")
|
||||
if (!properties.empty() && self->tocNavPath.empty()) {
|
||||
// Properties is space-separated, check if "nav" is present as a word
|
||||
if (properties == "nav" || properties.find("nav ") == 0 || properties.find(" nav") != std::string::npos) {
|
||||
self->tocNavPath = href;
|
||||
LOG_DBG("COF", "Found EPUB 3 nav document: %s", href.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// EPUB 3: Check for cover image (properties contains "cover-image")
|
||||
if (!properties.empty() && self->coverItemHref.empty()) {
|
||||
if (properties == "cover-image" || properties.find("cover-image ") == 0 ||
|
||||
properties.find(" cover-image") != std::string::npos) {
|
||||
self->coverItemHref = href;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// NOTE: This relies on spine appearing after item manifest (which is pretty safe as it's part of the EPUB spec)
|
||||
// Only run the spine parsing if there's a cache to add it to
|
||||
if (self->cache) {
|
||||
if (self->state == IN_SPINE && (strcmp(name, "itemref") == 0 || strcmp(name, "opf:itemref") == 0)) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "idref") == 0) {
|
||||
const std::string idref = atts[i + 1];
|
||||
std::string href;
|
||||
bool found = false;
|
||||
|
||||
if (self->useItemIndex) {
|
||||
// Fast path: binary search
|
||||
uint32_t targetHash = fnvHash(idref);
|
||||
uint16_t targetLen = static_cast<uint16_t>(idref.size());
|
||||
|
||||
auto it = std::lower_bound(self->itemIndex.begin(), self->itemIndex.end(),
|
||||
ItemIndexEntry{targetHash, targetLen, 0},
|
||||
[](const ItemIndexEntry& a, const ItemIndexEntry& b) {
|
||||
return a.idHash < b.idHash || (a.idHash == b.idHash && a.idLen < b.idLen);
|
||||
});
|
||||
|
||||
// Check for match (may need to check a few due to hash collisions)
|
||||
while (it != self->itemIndex.end() && it->idHash == targetHash) {
|
||||
self->tempItemStore.seek(it->fileOffset);
|
||||
std::string itemId;
|
||||
serialization::readString(self->tempItemStore, itemId);
|
||||
if (itemId == idref) {
|
||||
serialization::readString(self->tempItemStore, href);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
// Slow path: linear scan (for small manifests, keeps original behavior)
|
||||
// TODO: This lookup is slow as need to scan through all items each time.
|
||||
// It can take up to 200ms per item when getting to 1500 items.
|
||||
self->tempItemStore.seek(0);
|
||||
std::string itemId;
|
||||
while (self->tempItemStore.available()) {
|
||||
serialization::readString(self->tempItemStore, itemId);
|
||||
serialization::readString(self->tempItemStore, href);
|
||||
if (itemId == idref) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (found && self->cache) {
|
||||
self->cache->createSpineEntry(href);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// parse the guide
|
||||
if (self->state == IN_GUIDE && (strcmp(name, "reference") == 0 || strcmp(name, "opf:reference") == 0)) {
|
||||
std::string type;
|
||||
std::string guideHref;
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "type") == 0) {
|
||||
type = atts[i + 1];
|
||||
} else if (strcmp(atts[i], "href") == 0) {
|
||||
guideHref = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(self->baseContentPath + atts[i + 1]));
|
||||
}
|
||||
}
|
||||
if (!guideHref.empty()) {
|
||||
if (type == "text" || (type == "start" && !self->textReferenceHref.empty())) {
|
||||
LOG_DBG("COF", "Found %s reference in guide: %s", type.c_str(), guideHref.c_str());
|
||||
self->textReferenceHref = guideHref;
|
||||
} else if ((type == "cover" || type == "cover-page") && self->guideCoverPageHref.empty()) {
|
||||
LOG_DBG("COF", "Found cover reference in guide: %s", guideHref.c_str());
|
||||
self->guideCoverPageHref = guideHref;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL ContentOpfParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||
auto* self = static_cast<ContentOpfParser*>(userData);
|
||||
|
||||
if (self->state == IN_BOOK_TITLE) {
|
||||
self->title.append(s, len);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_BOOK_AUTHOR) {
|
||||
if (!self->author.empty()) {
|
||||
self->author.append(", "); // Add separator for multiple authors
|
||||
}
|
||||
self->author.append(s, len);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_BOOK_LANGUAGE) {
|
||||
self->language.append(s, len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL ContentOpfParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<ContentOpfParser*>(userData);
|
||||
(void)name;
|
||||
|
||||
if (self->state == IN_SPINE && (strcmp(name, "spine") == 0 || strcmp(name, "opf:spine") == 0)) {
|
||||
self->state = IN_PACKAGE;
|
||||
self->tempItemStore.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_GUIDE && (strcmp(name, "guide") == 0 || strcmp(name, "opf:guide") == 0)) {
|
||||
self->state = IN_PACKAGE;
|
||||
self->tempItemStore.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_MANIFEST && (strcmp(name, "manifest") == 0 || strcmp(name, "opf:manifest") == 0)) {
|
||||
self->state = IN_PACKAGE;
|
||||
self->tempItemStore.close();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_BOOK_TITLE && strcmp(name, "dc:title") == 0) {
|
||||
self->state = IN_METADATA;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_BOOK_AUTHOR && strcmp(name, "dc:creator") == 0) {
|
||||
self->state = IN_METADATA;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_BOOK_LANGUAGE && strcmp(name, "dc:language") == 0) {
|
||||
self->state = IN_METADATA;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_METADATA && (strcmp(name, "metadata") == 0 || strcmp(name, "opf:metadata") == 0)) {
|
||||
self->state = IN_PACKAGE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_PACKAGE && (strcmp(name, "package") == 0 || strcmp(name, "opf:package") == 0)) {
|
||||
self->state = START;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
|
||||
#include "Epub.h"
|
||||
#include <epub/Expat.h> // FreeInkBook's vendored expat
|
||||
|
||||
class BookMetadataCache;
|
||||
|
||||
class ContentOpfParser final : public Print {
|
||||
enum ParserState {
|
||||
START,
|
||||
IN_PACKAGE,
|
||||
IN_METADATA,
|
||||
IN_BOOK_TITLE,
|
||||
IN_BOOK_AUTHOR,
|
||||
IN_BOOK_LANGUAGE,
|
||||
IN_MANIFEST,
|
||||
IN_SPINE,
|
||||
IN_GUIDE,
|
||||
};
|
||||
|
||||
const std::string& cachePath;
|
||||
const std::string& baseContentPath;
|
||||
size_t remainingSize;
|
||||
XML_Parser parser = nullptr;
|
||||
ParserState state = START;
|
||||
BookMetadataCache* cache;
|
||||
HalFile tempItemStore;
|
||||
std::string coverItemId;
|
||||
|
||||
// Index for fast idref→href lookup (used only for large EPUBs)
|
||||
struct ItemIndexEntry {
|
||||
uint32_t idHash; // FNV-1a hash of itemId
|
||||
uint16_t idLen; // length for collision reduction
|
||||
uint32_t fileOffset; // offset in .items.bin
|
||||
};
|
||||
std::deque<ItemIndexEntry> itemIndex;
|
||||
bool useItemIndex = false;
|
||||
|
||||
static constexpr uint16_t LARGE_SPINE_THRESHOLD = 400;
|
||||
|
||||
// FNV-1a hash function
|
||||
static uint32_t fnvHash(const std::string& s) {
|
||||
uint32_t hash = 2166136261u;
|
||||
for (char c : s) {
|
||||
hash ^= static_cast<uint8_t>(c);
|
||||
hash *= 16777619u;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void characterData(void* userData, const XML_Char* s, int len);
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
std::string title;
|
||||
std::string author;
|
||||
std::string language;
|
||||
std::string tocNcxPath;
|
||||
std::string tocNavPath; // EPUB 3 nav document path
|
||||
std::string coverItemHref;
|
||||
std::string guideCoverPageHref; // Guide reference with type="cover" or "cover-page" (points to XHTML wrapper)
|
||||
std::string textReferenceHref;
|
||||
std::vector<std::string> cssFiles; // CSS stylesheet paths
|
||||
|
||||
explicit ContentOpfParser(const std::string& cachePath, const std::string& baseContentPath, const size_t xmlSize,
|
||||
BookMetadataCache* cache)
|
||||
: cachePath(cachePath), baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||
~ContentOpfParser() override;
|
||||
|
||||
bool setup();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
};
|
||||
@@ -1,171 +0,0 @@
|
||||
#include "TocNavParser.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <Logging.h>
|
||||
#include <XmlParserUtils.h>
|
||||
|
||||
#include "Epub/BookMetadataCache.h"
|
||||
|
||||
bool TocNavParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
LOG_DBG("NAV", "Couldn't allocate memory for parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(parser, characterData);
|
||||
return true;
|
||||
}
|
||||
|
||||
TocNavParser::~TocNavParser() { destroyXmlParser(parser); }
|
||||
|
||||
size_t TocNavParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
size_t TocNavParser::write(const uint8_t* buffer, const size_t size) {
|
||||
if (!parser) return 0;
|
||||
|
||||
const uint8_t* currentBufferPos = buffer;
|
||||
auto remainingInBuffer = size;
|
||||
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
LOG_DBG("NAV", "Couldn't allocate memory for buffer");
|
||||
destroyXmlParser(parser);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
LOG_DBG("NAV", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
destroyXmlParser(parser);
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentBufferPos += toRead;
|
||||
remainingInBuffer -= toRead;
|
||||
remainingSize -= toRead;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
void XMLCALL TocNavParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
auto* self = static_cast<TocNavParser*>(userData);
|
||||
|
||||
// Track HTML structure loosely - we mainly care about finding <nav epub:type="toc">
|
||||
if (strcmp(name, "html") == 0) {
|
||||
self->state = IN_HTML;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_HTML && strcmp(name, "body") == 0) {
|
||||
self->state = IN_BODY;
|
||||
return;
|
||||
}
|
||||
|
||||
// Look for <nav epub:type="toc"> anywhere in body (or nested elements)
|
||||
if (self->state >= IN_BODY && strcmp(name, "nav") == 0) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if ((strcmp(atts[i], "epub:type") == 0 || strcmp(atts[i], "type") == 0) && strcmp(atts[i + 1], "toc") == 0) {
|
||||
self->state = IN_NAV_TOC;
|
||||
LOG_DBG("NAV", "Found nav toc element");
|
||||
return;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Only process ol/li/a if we're inside the toc nav
|
||||
if (self->state < IN_NAV_TOC) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "ol") == 0) {
|
||||
self->olDepth++;
|
||||
self->state = IN_OL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_OL && strcmp(name, "li") == 0) {
|
||||
self->state = IN_LI;
|
||||
self->currentLabel.clear();
|
||||
self->currentHref.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_LI && strcmp(name, "a") == 0) {
|
||||
self->state = IN_ANCHOR;
|
||||
// Get href attribute
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "href") == 0) {
|
||||
self->currentHref = atts[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL TocNavParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||
auto* self = static_cast<TocNavParser*>(userData);
|
||||
|
||||
// Only collect text when inside an anchor within the TOC nav
|
||||
if (self->state == IN_ANCHOR) {
|
||||
self->currentLabel.append(s, len);
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL TocNavParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<TocNavParser*>(userData);
|
||||
|
||||
if (strcmp(name, "a") == 0 && self->state == IN_ANCHOR) {
|
||||
// Create TOC entry when closing anchor tag (we have all data now)
|
||||
if (!self->currentLabel.empty() && !self->currentHref.empty()) {
|
||||
const std::string rawTarget = self->baseContentPath + self->currentHref;
|
||||
const size_t pos = rawTarget.find('#');
|
||||
const std::string rawPath = pos == std::string::npos ? rawTarget : rawTarget.substr(0, pos);
|
||||
std::string href = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(rawPath));
|
||||
std::string anchor;
|
||||
|
||||
if (pos != std::string::npos) {
|
||||
anchor = FsHelpers::decodeUriEscapes(rawTarget.substr(pos + 1));
|
||||
}
|
||||
|
||||
if (self->cache) {
|
||||
// olDepth gives us the nesting level (1-based from the outer ol)
|
||||
self->cache->createTocEntry(self->currentLabel, href, anchor, self->olDepth);
|
||||
}
|
||||
|
||||
self->currentLabel.clear();
|
||||
self->currentHref.clear();
|
||||
}
|
||||
self->state = IN_LI;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "li") == 0 && (self->state == IN_LI || self->state == IN_OL)) {
|
||||
self->state = IN_OL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "ol") == 0 && self->state >= IN_NAV_TOC) {
|
||||
self->olDepth--;
|
||||
if (self->olDepth == 0) {
|
||||
self->state = IN_NAV_TOC;
|
||||
} else {
|
||||
self->state = IN_LI; // Back to parent li
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (strcmp(name, "nav") == 0 && self->state >= IN_NAV_TOC) {
|
||||
self->state = IN_BODY;
|
||||
LOG_DBG("NAV", "Finished parsing nav toc");
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
#include <epub/Expat.h> // FreeInkBook's vendored expat
|
||||
|
||||
#include <string>
|
||||
|
||||
class BookMetadataCache;
|
||||
|
||||
// Parser for EPUB 3 nav.xhtml navigation documents
|
||||
// Parses HTML5 nav elements with epub:type="toc" to extract table of contents
|
||||
class TocNavParser final : public Print {
|
||||
enum ParserState {
|
||||
START,
|
||||
IN_HTML,
|
||||
IN_BODY,
|
||||
IN_NAV_TOC, // Inside <nav epub:type="toc">
|
||||
IN_OL, // Inside <ol>
|
||||
IN_LI, // Inside <li>
|
||||
IN_ANCHOR, // Inside <a>
|
||||
};
|
||||
|
||||
const std::string& baseContentPath;
|
||||
size_t remainingSize;
|
||||
XML_Parser parser = nullptr;
|
||||
ParserState state = START;
|
||||
BookMetadataCache* cache;
|
||||
|
||||
// Track nesting depth for <ol> elements to determine TOC depth
|
||||
uint8_t olDepth = 0;
|
||||
// Current entry data being collected
|
||||
std::string currentLabel;
|
||||
std::string currentHref;
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void characterData(void* userData, const XML_Char* s, int len);
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
explicit TocNavParser(const std::string& baseContentPath, const size_t xmlSize, BookMetadataCache* cache)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||
~TocNavParser() override;
|
||||
|
||||
bool setup();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
};
|
||||
@@ -1,167 +0,0 @@
|
||||
#include "TocNcxParser.h"
|
||||
|
||||
#include <FsHelpers.h>
|
||||
#include <Logging.h>
|
||||
#include <XmlParserUtils.h>
|
||||
|
||||
#include "Epub/BookMetadataCache.h"
|
||||
|
||||
bool TocNcxParser::setup() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
LOG_DBG("TOC", "Couldn't allocate memory for parser");
|
||||
return false;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, startElement, endElement);
|
||||
XML_SetCharacterDataHandler(parser, characterData);
|
||||
return true;
|
||||
}
|
||||
|
||||
TocNcxParser::~TocNcxParser() { destroyXmlParser(parser); }
|
||||
|
||||
size_t TocNcxParser::write(const uint8_t data) { return write(&data, 1); }
|
||||
|
||||
size_t TocNcxParser::write(const uint8_t* buffer, const size_t size) {
|
||||
if (!parser) return 0;
|
||||
|
||||
const uint8_t* currentBufferPos = buffer;
|
||||
auto remainingInBuffer = size;
|
||||
|
||||
while (remainingInBuffer > 0) {
|
||||
void* const buf = XML_GetBuffer(parser, 1024);
|
||||
if (!buf) {
|
||||
LOG_DBG("TOC", "Couldn't allocate memory for buffer");
|
||||
destroyXmlParser(parser);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto toRead = remainingInBuffer < 1024 ? remainingInBuffer : 1024;
|
||||
memcpy(buf, currentBufferPos, toRead);
|
||||
|
||||
if (XML_ParseBuffer(parser, static_cast<int>(toRead), remainingSize == toRead) == XML_STATUS_ERROR) {
|
||||
LOG_DBG("TOC", "Parse error at line %lu: %s", XML_GetCurrentLineNumber(parser),
|
||||
XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
destroyXmlParser(parser);
|
||||
return 0;
|
||||
}
|
||||
|
||||
currentBufferPos += toRead;
|
||||
remainingInBuffer -= toRead;
|
||||
remainingSize -= toRead;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
void XMLCALL TocNcxParser::startElement(void* userData, const XML_Char* name, const XML_Char** atts) {
|
||||
// NOTE: We rely on navPoint label and content coming before any nested navPoints, this will be fine:
|
||||
// <navPoint>
|
||||
// <navLabel><text>Chapter 1</text></navLabel>
|
||||
// <content src="ch1.html"/>
|
||||
// <navPoint> ...nested... </navPoint>
|
||||
// </navPoint>
|
||||
//
|
||||
// This will NOT:
|
||||
// <navPoint>
|
||||
// <navPoint> ...nested... </navPoint>
|
||||
// <navLabel><text>Chapter 1</text></navLabel>
|
||||
// <content src="ch1.html"/>
|
||||
// </navPoint>
|
||||
|
||||
auto* self = static_cast<TocNcxParser*>(userData);
|
||||
|
||||
if (self->state == START && strcmp(name, "ncx") == 0) {
|
||||
self->state = IN_NCX;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NCX && strcmp(name, "navMap") == 0) {
|
||||
self->state = IN_NAV_MAP;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handles both top-level and nested navPoints
|
||||
if ((self->state == IN_NAV_MAP || self->state == IN_NAV_POINT) && strcmp(name, "navPoint") == 0) {
|
||||
self->state = IN_NAV_POINT;
|
||||
self->currentDepth++;
|
||||
|
||||
self->currentLabel.clear();
|
||||
self->currentSrc.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_POINT && strcmp(name, "navLabel") == 0) {
|
||||
self->state = IN_NAV_LABEL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_LABEL && strcmp(name, "text") == 0) {
|
||||
self->state = IN_NAV_LABEL_TEXT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
|
||||
for (int i = 0; atts[i]; i += 2) {
|
||||
if (strcmp(atts[i], "src") == 0) {
|
||||
self->currentSrc = atts[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL TocNcxParser::characterData(void* userData, const XML_Char* s, const int len) {
|
||||
auto* self = static_cast<TocNcxParser*>(userData);
|
||||
if (self->state == IN_NAV_LABEL_TEXT) {
|
||||
self->currentLabel.append(s, len);
|
||||
}
|
||||
}
|
||||
|
||||
void XMLCALL TocNcxParser::endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<TocNcxParser*>(userData);
|
||||
|
||||
if (self->state == IN_NAV_LABEL_TEXT && strcmp(name, "text") == 0) {
|
||||
self->state = IN_NAV_LABEL;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_LABEL && strcmp(name, "navLabel") == 0) {
|
||||
self->state = IN_NAV_POINT;
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_POINT && strcmp(name, "navPoint") == 0) {
|
||||
self->currentDepth--;
|
||||
if (self->currentDepth == 0) {
|
||||
self->state = IN_NAV_MAP;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (self->state == IN_NAV_POINT && strcmp(name, "content") == 0) {
|
||||
// At this point (end of content tag), we likely have both Label (from previous tags) and Src.
|
||||
// This is the safest place to push the data, assuming <navLabel> always comes before <content>.
|
||||
// NCX spec says navLabel comes before content.
|
||||
if (!self->currentLabel.empty() && !self->currentSrc.empty()) {
|
||||
const std::string rawTarget = self->baseContentPath + self->currentSrc;
|
||||
const size_t pos = rawTarget.find('#');
|
||||
const std::string rawPath = pos == std::string::npos ? rawTarget : rawTarget.substr(0, pos);
|
||||
std::string href = FsHelpers::normalisePath(FsHelpers::decodeUriEscapes(rawPath));
|
||||
std::string anchor;
|
||||
|
||||
if (pos != std::string::npos) {
|
||||
anchor = FsHelpers::decodeUriEscapes(rawTarget.substr(pos + 1));
|
||||
}
|
||||
|
||||
if (self->cache) {
|
||||
self->cache->createTocEntry(self->currentLabel, href, anchor, self->currentDepth);
|
||||
}
|
||||
|
||||
// Clear them so we don't re-add them if there are weird XML structures
|
||||
self->currentLabel.clear();
|
||||
self->currentSrc.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
#pragma once
|
||||
#include <Print.h>
|
||||
#include <epub/Expat.h> // FreeInkBook's vendored expat
|
||||
|
||||
#include <string>
|
||||
|
||||
class BookMetadataCache;
|
||||
|
||||
class TocNcxParser final : public Print {
|
||||
enum ParserState { START, IN_NCX, IN_NAV_MAP, IN_NAV_POINT, IN_NAV_LABEL, IN_NAV_LABEL_TEXT, IN_CONTENT };
|
||||
|
||||
const std::string& baseContentPath;
|
||||
size_t remainingSize;
|
||||
XML_Parser parser = nullptr;
|
||||
ParserState state = START;
|
||||
BookMetadataCache* cache;
|
||||
|
||||
std::string currentLabel;
|
||||
std::string currentSrc;
|
||||
uint8_t currentDepth = 0;
|
||||
|
||||
static void startElement(void* userData, const XML_Char* name, const XML_Char** atts);
|
||||
static void characterData(void* userData, const XML_Char* s, int len);
|
||||
static void endElement(void* userData, const XML_Char* name);
|
||||
|
||||
public:
|
||||
explicit TocNcxParser(const std::string& baseContentPath, const size_t xmlSize, BookMetadataCache* cache)
|
||||
: baseContentPath(baseContentPath), remainingSize(xmlSize), cache(cache) {}
|
||||
~TocNcxParser() override;
|
||||
|
||||
bool setup();
|
||||
|
||||
size_t write(uint8_t) override;
|
||||
size_t write(const uint8_t* buffer, size_t size) override;
|
||||
};
|
||||
@@ -3,7 +3,6 @@
|
||||
#include <HalStorage.h>
|
||||
|
||||
class Print;
|
||||
class ZipFile;
|
||||
|
||||
class JpegToBmpConverter {
|
||||
static bool jpegFileToBmpStreamInternal(HalFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight,
|
||||
|
||||
@@ -1,600 +0,0 @@
|
||||
#include "ChapterXPathResolver.h"
|
||||
|
||||
#include <Logging.h>
|
||||
#include <Print.h>
|
||||
#include <Utf8.h>
|
||||
#include <XmlParserUtils.h>
|
||||
#include <epub/Expat.h> // FreeInkBook's vendored expat
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
std::string stripPrefix(const XML_Char* name) {
|
||||
if (!name) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const char* local = std::strrchr(name, ':');
|
||||
return local ? std::string(local + 1) : std::string(name);
|
||||
}
|
||||
|
||||
struct NameCounter {
|
||||
std::string name;
|
||||
int count;
|
||||
};
|
||||
|
||||
struct ParentState {
|
||||
std::vector<NameCounter> children;
|
||||
|
||||
int nextIndex(const std::string& name) {
|
||||
for (auto& child : children) {
|
||||
if (child.name == name) {
|
||||
child.count++;
|
||||
return child.count;
|
||||
}
|
||||
}
|
||||
|
||||
children.push_back({name, 1});
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
struct PathSegment {
|
||||
std::string name;
|
||||
int index;
|
||||
};
|
||||
|
||||
std::string buildParagraphXPath(const int spineIndex, const std::vector<PathSegment>& path, const int textNodeIndex,
|
||||
const size_t charOffset) {
|
||||
std::string xpath = "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body";
|
||||
for (const auto& segment : path) {
|
||||
xpath += "/" + segment.name + "[" + std::to_string(segment.index) + "]";
|
||||
}
|
||||
if (textNodeIndex > 0 && charOffset > 0) {
|
||||
xpath += "/text()[" + std::to_string(textNodeIndex) + "]." + std::to_string(charOffset);
|
||||
}
|
||||
return xpath;
|
||||
}
|
||||
|
||||
size_t countUtf8Codepoints(const XML_Char* data, const int len) {
|
||||
if (!data || len <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
size_t count = 0;
|
||||
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(data);
|
||||
const unsigned char* end = ptr + len;
|
||||
while (ptr < end) {
|
||||
utf8NextCodepoint(&ptr);
|
||||
count++;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
class ParagraphTextCounter final : public Print {
|
||||
public:
|
||||
ParagraphTextCounter() {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
LOG_ERR("KOX", "Failed to create XML parser");
|
||||
return;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, &ParagraphTextCounter::startElement, &ParagraphTextCounter::endElement);
|
||||
XML_SetCharacterDataHandler(parser, &ParagraphTextCounter::characterData);
|
||||
}
|
||||
|
||||
~ParagraphTextCounter() override { destroyXmlParser(parser); }
|
||||
|
||||
bool ok() const { return parser != nullptr && parseOk; }
|
||||
|
||||
bool finish() {
|
||||
if (!parser || !parseOk || stopped) {
|
||||
return parseOk;
|
||||
}
|
||||
|
||||
if (XML_Parse(parser, "", 0, XML_TRUE) == XML_STATUS_ERROR) {
|
||||
LOG_ERR("KOX", "Final XML parse error: %s", XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
parseOk = false;
|
||||
}
|
||||
return parseOk;
|
||||
}
|
||||
|
||||
size_t write(uint8_t c) override { return write(&c, 1); }
|
||||
|
||||
size_t write(const uint8_t* buffer, size_t size) override {
|
||||
if (!parser || !parseOk || stopped) {
|
||||
return size;
|
||||
}
|
||||
|
||||
if (XML_Parse(parser, reinterpret_cast<const char*>(buffer), static_cast<int>(size), XML_FALSE) != XML_STATUS_OK) {
|
||||
const enum XML_Error error = XML_GetErrorCode(parser);
|
||||
if (error != XML_ERROR_ABORTED) {
|
||||
LOG_ERR("KOX", "XML parse error: %s", XML_ErrorString(error));
|
||||
parseOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
size_t totalVisibleChars() const { return visibleChars; }
|
||||
|
||||
private:
|
||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char**) {
|
||||
auto* self = static_cast<ParagraphTextCounter*>(userData);
|
||||
self->onStartElement(name);
|
||||
}
|
||||
|
||||
static void XMLCALL endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<ParagraphTextCounter*>(userData);
|
||||
self->onEndElement(name);
|
||||
}
|
||||
|
||||
static void XMLCALL characterData(void* userData, const XML_Char* data, const int len) {
|
||||
auto* self = static_cast<ParagraphTextCounter*>(userData);
|
||||
self->onCharacterData(data, len);
|
||||
}
|
||||
|
||||
void onStartElement(const XML_Char* rawName) {
|
||||
const std::string name = stripPrefix(rawName);
|
||||
|
||||
if (!insideBody) {
|
||||
if (name == "body") {
|
||||
insideBody = true;
|
||||
bodyDepth = depth;
|
||||
}
|
||||
depth++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (name == "p") {
|
||||
paragraphDepth++;
|
||||
}
|
||||
depth++;
|
||||
}
|
||||
|
||||
void onEndElement(const XML_Char* rawName) {
|
||||
const std::string name = stripPrefix(rawName);
|
||||
|
||||
depth--;
|
||||
if (!insideBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (depth == bodyDepth && name == "body") {
|
||||
insideBody = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (name == "p" && paragraphDepth > 0) {
|
||||
paragraphDepth--;
|
||||
}
|
||||
}
|
||||
|
||||
void onCharacterData(const XML_Char* data, const int len) {
|
||||
if (!insideBody || paragraphDepth <= 0 || len <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
visibleChars += countUtf8Codepoints(data, len);
|
||||
}
|
||||
|
||||
private:
|
||||
XML_Parser parser = nullptr;
|
||||
bool parseOk = true;
|
||||
bool insideBody = false;
|
||||
bool stopped = false;
|
||||
int depth = 0;
|
||||
int bodyDepth = -1;
|
||||
int paragraphDepth = 0;
|
||||
size_t visibleChars = 0;
|
||||
};
|
||||
|
||||
class XPathParagraphResolver final : public Print {
|
||||
public:
|
||||
explicit XPathParagraphResolver(const int targetParagraph) : targetParagraph(targetParagraph) {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
LOG_ERR("KOX", "Failed to create XML parser");
|
||||
return;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, &XPathParagraphResolver::startElement, &XPathParagraphResolver::endElement);
|
||||
}
|
||||
|
||||
~XPathParagraphResolver() override { destroyXmlParser(parser); }
|
||||
|
||||
bool ok() const { return parser != nullptr && parseOk; }
|
||||
|
||||
bool finish() {
|
||||
if (!parser || !parseOk || stopped) {
|
||||
return parseOk;
|
||||
}
|
||||
|
||||
if (XML_Parse(parser, "", 0, XML_TRUE) == XML_STATUS_ERROR) {
|
||||
LOG_ERR("KOX", "Final XML parse error: %s", XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
parseOk = false;
|
||||
}
|
||||
return parseOk;
|
||||
}
|
||||
|
||||
bool hasMatch() const { return !xpath.empty(); }
|
||||
const std::string& getXPath() const { return xpath; }
|
||||
|
||||
size_t write(uint8_t c) override { return write(&c, 1); }
|
||||
|
||||
size_t write(const uint8_t* buffer, size_t size) override {
|
||||
if (!parser || !parseOk || stopped) {
|
||||
return size;
|
||||
}
|
||||
|
||||
if (XML_Parse(parser, reinterpret_cast<const char*>(buffer), static_cast<int>(size), XML_FALSE) != XML_STATUS_OK) {
|
||||
const enum XML_Error error = XML_GetErrorCode(parser);
|
||||
if (error != XML_ERROR_ABORTED) {
|
||||
LOG_ERR("KOX", "XML parse error: %s", XML_ErrorString(error));
|
||||
parseOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int spineIndex = 0;
|
||||
|
||||
private:
|
||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char**) {
|
||||
auto* self = static_cast<XPathParagraphResolver*>(userData);
|
||||
self->onStartElement(name);
|
||||
}
|
||||
|
||||
static void XMLCALL endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<XPathParagraphResolver*>(userData);
|
||||
self->onEndElement(name);
|
||||
}
|
||||
|
||||
void onStartElement(const XML_Char* rawName) {
|
||||
const std::string name = stripPrefix(rawName);
|
||||
|
||||
if (!insideBody) {
|
||||
if (name == "body") {
|
||||
insideBody = true;
|
||||
bodyDepth = depth;
|
||||
parentStates.emplace_back();
|
||||
}
|
||||
depth++;
|
||||
return;
|
||||
}
|
||||
|
||||
const int siblingIndex = parentStates.back().nextIndex(name);
|
||||
path.push_back({name, siblingIndex});
|
||||
parentStates.emplace_back();
|
||||
|
||||
// Count both <p> and <li> as paragraph-like positions, matching how the section
|
||||
// layout tracks them (xpathParagraphIndex and xpathListItemIndex). This ensures
|
||||
// KOReader progress in list items maps to the correct XPath.
|
||||
if (name == "p") {
|
||||
paragraphCount++;
|
||||
} else if (name == "li") {
|
||||
paragraphCount++;
|
||||
}
|
||||
if (paragraphCount == targetParagraph) {
|
||||
xpath = buildParagraphXPath(spineIndex, path, 0, 0);
|
||||
stopped = true;
|
||||
XML_StopParser(parser, XML_FALSE);
|
||||
}
|
||||
|
||||
depth++;
|
||||
}
|
||||
|
||||
void onEndElement(const XML_Char* rawName) {
|
||||
const std::string name = stripPrefix(rawName);
|
||||
|
||||
depth--;
|
||||
if (!insideBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (depth == bodyDepth && name == "body") {
|
||||
insideBody = false;
|
||||
parentStates.clear();
|
||||
path.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!path.empty()) {
|
||||
path.pop_back();
|
||||
}
|
||||
if (!parentStates.empty()) {
|
||||
parentStates.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
XML_Parser parser = nullptr;
|
||||
const int targetParagraph;
|
||||
bool parseOk = true;
|
||||
bool insideBody = false;
|
||||
bool stopped = false;
|
||||
int depth = 0;
|
||||
int bodyDepth = -1;
|
||||
int paragraphCount = 0;
|
||||
std::vector<ParentState> parentStates;
|
||||
std::vector<PathSegment> path;
|
||||
std::string xpath;
|
||||
};
|
||||
|
||||
class XPathProgressResolver final : public Print {
|
||||
public:
|
||||
explicit XPathProgressResolver(const size_t targetVisibleChar) : targetVisibleChar(targetVisibleChar) {
|
||||
parser = XML_ParserCreate(nullptr);
|
||||
if (!parser) {
|
||||
LOG_ERR("KOX", "Failed to create XML parser");
|
||||
return;
|
||||
}
|
||||
|
||||
XML_SetUserData(parser, this);
|
||||
XML_SetElementHandler(parser, &XPathProgressResolver::startElement, &XPathProgressResolver::endElement);
|
||||
XML_SetCharacterDataHandler(parser, &XPathProgressResolver::characterData);
|
||||
}
|
||||
|
||||
~XPathProgressResolver() override { destroyXmlParser(parser); }
|
||||
|
||||
bool ok() const { return parser != nullptr && parseOk; }
|
||||
|
||||
bool finish() {
|
||||
if (!parser || !parseOk || stopped) {
|
||||
return parseOk;
|
||||
}
|
||||
|
||||
if (XML_Parse(parser, "", 0, XML_TRUE) == XML_STATUS_ERROR) {
|
||||
LOG_ERR("KOX", "Final XML parse error: %s", XML_ErrorString(XML_GetErrorCode(parser)));
|
||||
parseOk = false;
|
||||
}
|
||||
return parseOk;
|
||||
}
|
||||
|
||||
bool hasMatch() const { return !xpath.empty(); }
|
||||
const std::string& getXPath() const { return xpath; }
|
||||
|
||||
size_t write(uint8_t c) override { return write(&c, 1); }
|
||||
|
||||
size_t write(const uint8_t* buffer, size_t size) override {
|
||||
if (!parser || !parseOk || stopped) {
|
||||
return size;
|
||||
}
|
||||
|
||||
if (XML_Parse(parser, reinterpret_cast<const char*>(buffer), static_cast<int>(size), XML_FALSE) != XML_STATUS_OK) {
|
||||
const enum XML_Error error = XML_GetErrorCode(parser);
|
||||
if (error != XML_ERROR_ABORTED) {
|
||||
LOG_ERR("KOX", "XML parse error: %s", XML_ErrorString(error));
|
||||
parseOk = false;
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
int spineIndex = 0;
|
||||
|
||||
private:
|
||||
static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char**) {
|
||||
auto* self = static_cast<XPathProgressResolver*>(userData);
|
||||
self->onStartElement(name);
|
||||
}
|
||||
|
||||
static void XMLCALL endElement(void* userData, const XML_Char* name) {
|
||||
auto* self = static_cast<XPathProgressResolver*>(userData);
|
||||
self->onEndElement(name);
|
||||
}
|
||||
|
||||
static void XMLCALL characterData(void* userData, const XML_Char* data, const int len) {
|
||||
auto* self = static_cast<XPathProgressResolver*>(userData);
|
||||
self->onCharacterData(data, len);
|
||||
}
|
||||
|
||||
void onStartElement(const XML_Char* rawName) {
|
||||
const std::string name = stripPrefix(rawName);
|
||||
|
||||
if (!insideBody) {
|
||||
if (name == "body") {
|
||||
insideBody = true;
|
||||
bodyDepth = depth;
|
||||
parentStates.emplace_back();
|
||||
}
|
||||
depth++;
|
||||
return;
|
||||
}
|
||||
|
||||
const int siblingIndex = parentStates.back().nextIndex(name);
|
||||
path.push_back({name, siblingIndex});
|
||||
parentStates.emplace_back();
|
||||
textNodeIndexStack.push_back(0);
|
||||
pendingTextNode = true;
|
||||
|
||||
if (name == "p") {
|
||||
paragraphDepth++;
|
||||
}
|
||||
if (name == "li") {
|
||||
liDepth++;
|
||||
}
|
||||
|
||||
depth++;
|
||||
}
|
||||
|
||||
void onEndElement(const XML_Char* rawName) {
|
||||
const std::string name = stripPrefix(rawName);
|
||||
|
||||
depth--;
|
||||
if (!insideBody) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (depth == bodyDepth && name == "body") {
|
||||
insideBody = false;
|
||||
parentStates.clear();
|
||||
path.clear();
|
||||
textNodeIndexStack.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
if (name == "p" && paragraphDepth > 0) {
|
||||
paragraphDepth--;
|
||||
}
|
||||
if (name == "li" && liDepth > 0) {
|
||||
liDepth--;
|
||||
}
|
||||
|
||||
if (!textNodeIndexStack.empty()) {
|
||||
textNodeIndexStack.pop_back();
|
||||
}
|
||||
if (paragraphDepth > 0 || liDepth > 0) {
|
||||
pendingTextNode = true;
|
||||
}
|
||||
if (!path.empty()) {
|
||||
path.pop_back();
|
||||
}
|
||||
if (!parentStates.empty()) {
|
||||
parentStates.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
void onCharacterData(const XML_Char* data, const int len) {
|
||||
if (!insideBody || (paragraphDepth <= 0 && liDepth <= 0) || len <= 0 || stopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t codepointCount = countUtf8Codepoints(data, len);
|
||||
if (codepointCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start a new text node on first non-empty content after any element boundary.
|
||||
// Only counting non-empty nodes matches KOReader's text()[N] indexing behavior,
|
||||
// which skips empty text nodes created by bare <a id="anchor"/> anchors.
|
||||
if (pendingTextNode) {
|
||||
if (!textNodeIndexStack.empty()) {
|
||||
textNodeIndexStack.back()++;
|
||||
}
|
||||
textNodeStartChars = visibleChars;
|
||||
pendingTextNode = false;
|
||||
}
|
||||
|
||||
const size_t nextVisibleChars = visibleChars + codepointCount;
|
||||
if (targetVisibleChar <= nextVisibleChars) {
|
||||
const size_t delta = targetVisibleChar - visibleChars;
|
||||
const int texNode = textNodeIndexStack.empty() ? 0 : textNodeIndexStack.back();
|
||||
const size_t charOff = visibleChars - textNodeStartChars + delta;
|
||||
xpath = buildParagraphXPath(spineIndex, path, texNode, charOff);
|
||||
stopped = true;
|
||||
XML_StopParser(parser, XML_FALSE);
|
||||
return;
|
||||
}
|
||||
|
||||
visibleChars = nextVisibleChars;
|
||||
}
|
||||
|
||||
XML_Parser parser = nullptr;
|
||||
const size_t targetVisibleChar;
|
||||
bool parseOk = true;
|
||||
bool insideBody = false;
|
||||
bool stopped = false;
|
||||
bool pendingTextNode = true;
|
||||
int depth = 0;
|
||||
int bodyDepth = -1;
|
||||
int paragraphDepth = 0;
|
||||
int liDepth = 0;
|
||||
size_t visibleChars = 0;
|
||||
size_t textNodeStartChars = 0;
|
||||
std::vector<int> textNodeIndexStack;
|
||||
std::vector<ParentState> parentStates;
|
||||
std::vector<PathSegment> path;
|
||||
std::string xpath;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
std::string ChapterXPathResolver::findXPathForParagraph(const std::shared_ptr<Epub>& epub, const int spineIndex,
|
||||
const uint16_t paragraphIndex) {
|
||||
if (!epub || paragraphIndex == 0 || spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto href = epub->getSpineItem(spineIndex).href;
|
||||
if (href.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
XPathParagraphResolver resolver(paragraphIndex);
|
||||
if (!resolver.ok()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
resolver.spineIndex = spineIndex;
|
||||
if (!epub->readItemContentsToStream(href, resolver, 1024) || !resolver.finish()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (resolver.hasMatch()) {
|
||||
LOG_DBG("KOX", "Resolved paragraph %u in spine %d -> %s", paragraphIndex, spineIndex, resolver.getXPath().c_str());
|
||||
return resolver.getXPath();
|
||||
}
|
||||
|
||||
LOG_DBG("KOX", "Paragraph %u not found in spine %d", paragraphIndex, spineIndex);
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string ChapterXPathResolver::findXPathForProgress(const std::shared_ptr<Epub>& epub, const int spineIndex,
|
||||
const float intraSpineProgress) {
|
||||
if (!epub || spineIndex < 0 || spineIndex >= epub->getSpineItemsCount()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const auto href = epub->getSpineItem(spineIndex).href;
|
||||
if (href.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!(intraSpineProgress > 0.0f)) {
|
||||
return "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body";
|
||||
}
|
||||
|
||||
ParagraphTextCounter counter;
|
||||
if (!counter.ok() || !epub->readItemContentsToStream(href, counter, 1024) || !counter.finish()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const size_t totalVisibleChars = counter.totalVisibleChars();
|
||||
if (totalVisibleChars == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const float clamped = std::max(0.0f, std::min(1.0f, intraSpineProgress));
|
||||
const size_t targetVisibleChar =
|
||||
std::max<size_t>(1, std::min(totalVisibleChars, static_cast<size_t>(std::ceil(clamped * totalVisibleChars))));
|
||||
|
||||
XPathProgressResolver resolver(targetVisibleChar);
|
||||
if (!resolver.ok()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
resolver.spineIndex = spineIndex;
|
||||
if (!epub->readItemContentsToStream(href, resolver, 1024) || !resolver.finish()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (resolver.hasMatch()) {
|
||||
LOG_DBG("KOX", "Resolved progress %.3f in spine %d -> %s", intraSpineProgress, spineIndex,
|
||||
resolver.getXPath().c_str());
|
||||
return resolver.getXPath();
|
||||
}
|
||||
|
||||
LOG_DBG("KOX", "Could not resolve progress %.3f in spine %d", intraSpineProgress, spineIndex);
|
||||
return "";
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <Epub.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
class ChapterXPathResolver {
|
||||
public:
|
||||
/**
|
||||
* Resolve the Nth paragraph in a spine item to its real XHTML ancestry path.
|
||||
*
|
||||
* Returns a KOReader-compatible path like:
|
||||
* /body/DocFragment[8]/body/div[2]/section[1]/p[4]
|
||||
*
|
||||
* An empty string means parsing failed or the paragraph index was not found.
|
||||
*/
|
||||
static std::string findXPathForParagraph(const std::shared_ptr<Epub>& epub, int spineIndex, uint16_t paragraphIndex);
|
||||
|
||||
/**
|
||||
* Resolve intra-spine progress to a real XHTML ancestry path plus text offset.
|
||||
*
|
||||
* Returns a KOReader-compatible path like:
|
||||
* /body/DocFragment[8]/body/div[2]/section[1]/p[4]/text().96
|
||||
*
|
||||
* An empty string means parsing failed or the location could not be resolved.
|
||||
*/
|
||||
static std::string findXPathForProgress(const std::shared_ptr<Epub>& epub, int spineIndex, float intraSpineProgress);
|
||||
};
|
||||
@@ -1,903 +0,0 @@
|
||||
#include "ProgressMapper.h"
|
||||
|
||||
#include <GfxRenderer.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include "ChapterXPathResolver.h"
|
||||
#include "Epub/Section.h"
|
||||
#include "Epub/htmlEntities.h"
|
||||
#include "Utf8.h"
|
||||
|
||||
namespace {
|
||||
int parseIndex(const std::string& xpath, const char* prefix, bool last = false) {
|
||||
const size_t prefixLen = strlen(prefix);
|
||||
const size_t pos = last ? xpath.rfind(prefix) : xpath.find(prefix);
|
||||
if (pos == std::string::npos) return -1;
|
||||
const size_t numStart = pos + prefixLen;
|
||||
const size_t numEnd = xpath.find(']', numStart);
|
||||
if (numEnd == std::string::npos || numEnd == numStart) return -1;
|
||||
int val = 0;
|
||||
for (size_t i = numStart; i < numEnd; i++) {
|
||||
if (xpath[i] < '0' || xpath[i] > '9') return -1;
|
||||
val = val * 10 + (xpath[i] - '0');
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
int parseCharOffset(const std::string& xpath) {
|
||||
const size_t textPos = xpath.rfind("text()");
|
||||
const size_t dotPos = (textPos != std::string::npos) ? xpath.find('.', textPos) : xpath.rfind('.');
|
||||
if (dotPos == std::string::npos || dotPos + 1 >= xpath.size()) return 0;
|
||||
int val = 0;
|
||||
for (size_t i = dotPos + 1; i < xpath.size(); i++) {
|
||||
if (xpath[i] < '0' || xpath[i] > '9') return 0;
|
||||
val = val * 10 + (xpath[i] - '0');
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
// Parse the N from text()[N] in the XPath (1-based; defaults to 1 if absent or 1).
|
||||
int parseTextNodeIndex(const std::string& xpath) {
|
||||
const size_t textPos = xpath.rfind("text()[");
|
||||
if (textPos == std::string::npos) return 1;
|
||||
const size_t numStart = textPos + 7; // strlen("text()[")
|
||||
const size_t numEnd = xpath.find(']', numStart);
|
||||
if (numEnd == std::string::npos || numEnd == numStart) return 1;
|
||||
int val = 0;
|
||||
for (size_t i = numStart; i < numEnd; i++) {
|
||||
if (xpath[i] < '0' || xpath[i] > '9') return 1;
|
||||
val = val * 10 + (xpath[i] - '0');
|
||||
}
|
||||
return val > 0 ? val : 1;
|
||||
}
|
||||
|
||||
bool isChapterStartXPath(const std::string& xpath) {
|
||||
if (xpath.find("/p[") != std::string::npos || xpath.find("/li[") != std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr char kDocFragment[] = "/body/DocFragment[";
|
||||
const size_t docFragPos = xpath.find(kDocFragment);
|
||||
if (docFragPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
const size_t docFragEnd = xpath.find(']', docFragPos + strlen(kDocFragment));
|
||||
if (docFragEnd == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
if (docFragEnd + 1 == xpath.size()) {
|
||||
return true;
|
||||
}
|
||||
if (xpath[docFragEnd + 1] == '.') {
|
||||
if (docFragEnd + 2 >= xpath.size()) {
|
||||
return false;
|
||||
}
|
||||
for (size_t i = docFragEnd + 2; i < xpath.size(); i++) {
|
||||
if (xpath[i] != '0') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static constexpr char kDocBody[] = "]/body";
|
||||
const size_t docBodyPos = xpath.find(kDocBody);
|
||||
if (docBodyPos == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
size_t bodyContentStart = docBodyPos + strlen(kDocBody);
|
||||
if (bodyContentStart == xpath.size()) {
|
||||
return true;
|
||||
}
|
||||
if (xpath[bodyContentStart] != '/') {
|
||||
return false;
|
||||
}
|
||||
bodyContentStart++;
|
||||
if (bodyContentStart == xpath.size()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const size_t dotPos = xpath.rfind('.');
|
||||
if (dotPos == std::string::npos || dotPos <= bodyContentStart || dotPos + 1 >= xpath.size()) {
|
||||
return false;
|
||||
}
|
||||
size_t terminalEnd = dotPos;
|
||||
static constexpr char kTextNode[] = "/text()";
|
||||
const size_t textNodePos = xpath.rfind(kTextNode, dotPos);
|
||||
if (textNodePos != std::string::npos && textNodePos >= bodyContentStart) {
|
||||
terminalEnd = textNodePos;
|
||||
}
|
||||
if (xpath.find('/', bodyContentStart) < terminalEnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = dotPos + 1; i < xpath.size(); i++) {
|
||||
if (xpath[i] != '0') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Parsed representation of one step in the XPath ancestry.
|
||||
struct XPathStep {
|
||||
char tag[12]; // element name, null-terminated
|
||||
int siblingIndex; // 1-based sibling index, or 0 if unspecified (treat as 1)
|
||||
};
|
||||
|
||||
static constexpr int MAX_XPATH_DEPTH = 16;
|
||||
|
||||
// Parse the XPath segment between /body/DocFragment[N]/body/ and the terminal position
|
||||
// into an ordered sequence of steps. Returns step count, 0 on failure.
|
||||
// Example input: "/body/DocFragment[1]/body/div[1]/ul/li[4]/text()[1].51"
|
||||
// Fills steps with: {div,1}, {ul,1}, {li,4}
|
||||
int parseXPathSteps(const std::string& xpath, XPathStep steps[MAX_XPATH_DEPTH]) {
|
||||
static const char kBodyFrag[] = "/body/DocFragment[";
|
||||
const size_t fragPos = xpath.find(kBodyFrag);
|
||||
if (fragPos == std::string::npos) return 0;
|
||||
const size_t afterBracket = xpath.find(']', fragPos + strlen(kBodyFrag));
|
||||
if (afterBracket == std::string::npos) return 0;
|
||||
static const char kBody[] = "/body/";
|
||||
if (xpath.compare(afterBracket + 1, strlen(kBody), kBody) != 0) return 0;
|
||||
size_t pos = afterBracket + 1 + strlen(kBody);
|
||||
|
||||
size_t stepsEnd = xpath.rfind("/text()");
|
||||
if (stepsEnd == std::string::npos) {
|
||||
stepsEnd = xpath.rfind('.');
|
||||
if (stepsEnd == std::string::npos || stepsEnd <= pos || stepsEnd + 1 >= xpath.size()) return 0;
|
||||
for (size_t i = stepsEnd + 1; i < xpath.size(); i++) {
|
||||
if (xpath[i] < '0' || xpath[i] > '9') return 0;
|
||||
}
|
||||
}
|
||||
if (stepsEnd <= pos) return 0;
|
||||
|
||||
int count = 0;
|
||||
while (pos < stepsEnd && count < MAX_XPATH_DEPTH) {
|
||||
const size_t slash = xpath.find('/', pos);
|
||||
const size_t segEnd = (slash < stepsEnd) ? slash : stepsEnd;
|
||||
|
||||
XPathStep& step = steps[count];
|
||||
const size_t bracket = xpath.find('[', pos);
|
||||
const size_t nameEnd = (bracket != std::string::npos && bracket < segEnd) ? bracket : segEnd;
|
||||
const size_t nameLen = nameEnd - pos;
|
||||
if (nameLen == 0 || nameLen >= sizeof(step.tag)) return 0;
|
||||
memcpy(step.tag, xpath.c_str() + pos, nameLen);
|
||||
step.tag[nameLen] = '\0';
|
||||
|
||||
if (bracket != std::string::npos && bracket < segEnd) {
|
||||
const size_t closeBracket = xpath.find(']', bracket + 1);
|
||||
if (closeBracket == std::string::npos || closeBracket > segEnd) return 0;
|
||||
int idx = 0;
|
||||
for (size_t i = bracket + 1; i < closeBracket; i++) {
|
||||
if (xpath[i] < '0' || xpath[i] > '9') return 0;
|
||||
idx = idx * 10 + (xpath[i] - '0');
|
||||
}
|
||||
step.siblingIndex = idx;
|
||||
} else {
|
||||
step.siblingIndex = 1;
|
||||
}
|
||||
|
||||
count++;
|
||||
pos = (slash < stepsEnd) ? slash + 1 : stepsEnd;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
class ParagraphStreamer final : public Print {
|
||||
size_t bytesWritten = 0;
|
||||
bool globalInTag = false;
|
||||
bool globalInEntity = false;
|
||||
static constexpr size_t MAX_ENTITY_SIZE = 16;
|
||||
char entityBuffer[MAX_ENTITY_SIZE] = {};
|
||||
size_t entityLen = 0;
|
||||
|
||||
// Forward mode: count <p> paragraphs at a byte offset (legacy, used by generateXPath)
|
||||
size_t fwdTarget;
|
||||
int fwdResult = 0;
|
||||
bool fwdCaptured = false;
|
||||
|
||||
// Reverse mode shared state
|
||||
int revChar;
|
||||
bool revPFound = false;
|
||||
bool revDone = false;
|
||||
int revVisChars = 0;
|
||||
size_t totalVisChars = 0;
|
||||
size_t targetVisChars = 0;
|
||||
|
||||
// --- Legacy reverse mode (paragraph index only, no ancestry) ---
|
||||
int revParagraph = 0;
|
||||
int pCount = 0;
|
||||
int paragraphAtMatch = 0;
|
||||
int liCount = 0;
|
||||
int liCountAtMatch = 0;
|
||||
int targetTextNode = 1;
|
||||
int currentTextNode = 0;
|
||||
int paragraphHtmlDepth = -1;
|
||||
|
||||
// --- Ancestry-aware reverse mode ---
|
||||
const XPathStep* steps = nullptr;
|
||||
int stepCount = 0;
|
||||
int siblingCounters[MAX_XPATH_DEPTH] = {};
|
||||
bool insideStep[MAX_XPATH_DEPTH] = {};
|
||||
int htmlDepth = 0;
|
||||
int stepEnteredAtDepth[MAX_XPATH_DEPTH] = {};
|
||||
|
||||
// Tag name accumulation
|
||||
enum TagParseState { TAG_IDLE, TAG_IN_NAME, TAG_ATTRS } tagState = TAG_IDLE;
|
||||
bool tagIsClose = false;
|
||||
char tagName[12] = {};
|
||||
int tagNameLen = 0;
|
||||
|
||||
int matchedDepth = 0;
|
||||
|
||||
// Anchor ID capture
|
||||
static constexpr int MAX_ANCHOR_ID = 64;
|
||||
char capturedAnchorId[MAX_ANCHOR_ID] = {};
|
||||
int capturedAnchorIdLen = 0;
|
||||
bool capturingAnchorTag = false;
|
||||
enum AnchorAttrState {
|
||||
ATTR_FIND_NAME,
|
||||
ATTR_READ_NAME,
|
||||
ATTR_AFTER_NAME,
|
||||
ATTR_BEFORE_VALUE,
|
||||
ATTR_CAPTURE_D,
|
||||
ATTR_CAPTURE_S
|
||||
} attrState = ATTR_FIND_NAME;
|
||||
uint8_t attrNameLen = 0;
|
||||
bool currentAttrIsId = false;
|
||||
bool inAttrQuote =
|
||||
false; // true while inside a quoted attribute value (prevents '/' from being treated as self-close)
|
||||
char attrQuoteChar = 0;
|
||||
uint8_t nonVisibleDepth = 0;
|
||||
|
||||
bool isNonVisibleTag() const {
|
||||
return strcasecmp(tagName, "head") == 0 || strcasecmp(tagName, "style") == 0 ||
|
||||
strcasecmp(tagName, "script") == 0 || strcasecmp(tagName, "title") == 0;
|
||||
}
|
||||
|
||||
static bool isAttrWhitespace(uint8_t c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
|
||||
|
||||
static bool isAttrNameChar(uint8_t c) {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_' || c == '-' ||
|
||||
c == ':' || c == '.';
|
||||
}
|
||||
|
||||
void resetAnchorAttrScan() {
|
||||
attrState = ATTR_FIND_NAME;
|
||||
attrNameLen = 0;
|
||||
currentAttrIsId = false;
|
||||
}
|
||||
|
||||
void finishCapturedAnchorId() {
|
||||
capturedAnchorId[capturedAnchorIdLen] = '\0';
|
||||
capturingAnchorTag = false;
|
||||
resetAnchorAttrScan();
|
||||
}
|
||||
|
||||
void beginAnchorIdScan() {
|
||||
capturingAnchorTag = true;
|
||||
resetAnchorAttrScan();
|
||||
}
|
||||
|
||||
void endAnchorIdScan() {
|
||||
if (capturingAnchorTag) {
|
||||
capturedAnchorIdLen = 0;
|
||||
}
|
||||
capturingAnchorTag = false;
|
||||
resetAnchorAttrScan();
|
||||
}
|
||||
|
||||
void appendCapturedAnchorId(uint8_t c) {
|
||||
if (capturedAnchorIdLen + 1 < MAX_ANCHOR_ID) {
|
||||
capturedAnchorId[capturedAnchorIdLen++] = c;
|
||||
}
|
||||
}
|
||||
|
||||
void scanAnchorAttribute(uint8_t c) {
|
||||
switch (attrState) {
|
||||
case ATTR_FIND_NAME:
|
||||
if (isAttrNameChar(c)) {
|
||||
attrState = ATTR_READ_NAME;
|
||||
attrNameLen = 1;
|
||||
currentAttrIsId = c == 'i';
|
||||
}
|
||||
break;
|
||||
case ATTR_READ_NAME:
|
||||
if (isAttrNameChar(c)) {
|
||||
if (attrNameLen == 1) {
|
||||
currentAttrIsId = currentAttrIsId && c == 'd';
|
||||
} else {
|
||||
currentAttrIsId = false;
|
||||
}
|
||||
attrNameLen++;
|
||||
} else {
|
||||
currentAttrIsId = currentAttrIsId && attrNameLen == 2;
|
||||
if (isAttrWhitespace(c)) {
|
||||
attrState = ATTR_AFTER_NAME;
|
||||
} else if (c == '=') {
|
||||
attrState = ATTR_BEFORE_VALUE;
|
||||
} else {
|
||||
resetAnchorAttrScan();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ATTR_AFTER_NAME:
|
||||
if (isAttrWhitespace(c)) {
|
||||
break;
|
||||
}
|
||||
if (c == '=') {
|
||||
attrState = ATTR_BEFORE_VALUE;
|
||||
} else if (isAttrNameChar(c)) {
|
||||
attrState = ATTR_READ_NAME;
|
||||
attrNameLen = 1;
|
||||
currentAttrIsId = c == 'i';
|
||||
} else {
|
||||
resetAnchorAttrScan();
|
||||
}
|
||||
break;
|
||||
case ATTR_BEFORE_VALUE:
|
||||
if (isAttrWhitespace(c)) {
|
||||
break;
|
||||
}
|
||||
if (currentAttrIsId && c == '"') {
|
||||
capturedAnchorIdLen = 0;
|
||||
attrState = ATTR_CAPTURE_D;
|
||||
} else if (currentAttrIsId && c == '\'') {
|
||||
capturedAnchorIdLen = 0;
|
||||
attrState = ATTR_CAPTURE_S;
|
||||
} else if (c == '"') {
|
||||
attrState = ATTR_CAPTURE_D;
|
||||
} else if (c == '\'') {
|
||||
attrState = ATTR_CAPTURE_S;
|
||||
} else {
|
||||
resetAnchorAttrScan();
|
||||
}
|
||||
break;
|
||||
case ATTR_CAPTURE_D:
|
||||
if (c == '"') {
|
||||
if (currentAttrIsId) {
|
||||
finishCapturedAnchorId();
|
||||
} else {
|
||||
resetAnchorAttrScan();
|
||||
}
|
||||
} else if (currentAttrIsId) {
|
||||
appendCapturedAnchorId(c);
|
||||
}
|
||||
break;
|
||||
case ATTR_CAPTURE_S:
|
||||
if (c == '\'') {
|
||||
if (currentAttrIsId) {
|
||||
finishCapturedAnchorId();
|
||||
} else {
|
||||
resetAnchorAttrScan();
|
||||
}
|
||||
} else if (currentAttrIsId) {
|
||||
appendCapturedAnchorId(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void onVisibleCodepoint() {
|
||||
totalVisChars++;
|
||||
if (revPFound && !revDone) {
|
||||
// Ancestry mode: count only while inside the fully-matched element and in the target text node.
|
||||
// Legacy mode: count only while still inside the matched paragraph and in the target text node.
|
||||
const bool inTargetNode = (stepCount > 0) ? (matchedDepth == stepCount && currentTextNode == targetTextNode)
|
||||
: (paragraphHtmlDepth >= 0 && currentTextNode == targetTextNode);
|
||||
if (inTargetNode) {
|
||||
revVisChars++;
|
||||
if (revVisChars >= revChar) {
|
||||
targetVisChars = totalVisChars;
|
||||
revDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onVisibleText(const char* text) {
|
||||
if (!text) return;
|
||||
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(text);
|
||||
while (*ptr != 0) {
|
||||
utf8NextCodepoint(&ptr);
|
||||
onVisibleCodepoint();
|
||||
}
|
||||
}
|
||||
|
||||
void flushEntityAsLiteral() {
|
||||
for (size_t i = 0; i < entityLen; i++) onVisibleCodepoint();
|
||||
}
|
||||
|
||||
void finishEntity() {
|
||||
entityBuffer[entityLen] = '\0';
|
||||
const char* resolved = lookupHtmlEntity(entityBuffer, entityLen);
|
||||
if (resolved)
|
||||
onVisibleText(resolved);
|
||||
else
|
||||
flushEntityAsLiteral();
|
||||
globalInEntity = false;
|
||||
entityLen = 0;
|
||||
}
|
||||
|
||||
void onLegacyP() {
|
||||
pCount++;
|
||||
if (!revPFound && revParagraph > 0 && pCount >= revParagraph) {
|
||||
revPFound = true;
|
||||
revVisChars = 0;
|
||||
paragraphHtmlDepth = htmlDepth;
|
||||
currentTextNode = 1;
|
||||
if (revChar <= 0 && targetTextNode <= 1) {
|
||||
targetVisChars = totalVisChars;
|
||||
revDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onOpenTag() {
|
||||
htmlDepth++;
|
||||
|
||||
if (nonVisibleDepth > 0 || isNonVisibleTag()) {
|
||||
nonVisibleDepth++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (stepCount == 0) {
|
||||
if (strcasecmp(tagName, "p") == 0) onLegacyP();
|
||||
return;
|
||||
}
|
||||
|
||||
// Capture a child <a id> inside the fully-matched element even after target char is found.
|
||||
if (revPFound && matchedDepth == stepCount && capturedAnchorIdLen == 0 && strcasecmp(tagName, "a") == 0) {
|
||||
beginAnchorIdScan();
|
||||
}
|
||||
|
||||
if (revDone) return;
|
||||
|
||||
if (strcasecmp(tagName, "p") == 0) pCount++;
|
||||
if (strcasecmp(tagName, "li") == 0) liCount++;
|
||||
|
||||
if (matchedDepth < stepCount) {
|
||||
const XPathStep& target = steps[matchedDepth];
|
||||
if (strcasecmp(tagName, target.tag) == 0) {
|
||||
// Count only direct children of the previously matched ancestor step.
|
||||
// For step 0 any depth is valid; subsequent steps must be exactly one level deeper.
|
||||
const bool atCorrectDepth = (matchedDepth == 0) || (htmlDepth == stepEnteredAtDepth[matchedDepth - 1] + 1);
|
||||
if (!atCorrectDepth) return;
|
||||
siblingCounters[matchedDepth]++;
|
||||
if (siblingCounters[matchedDepth] == target.siblingIndex) {
|
||||
insideStep[matchedDepth] = true;
|
||||
stepEnteredAtDepth[matchedDepth] = htmlDepth;
|
||||
matchedDepth++;
|
||||
if (matchedDepth == stepCount) {
|
||||
beginAnchorIdScan();
|
||||
paragraphAtMatch = pCount;
|
||||
liCountAtMatch = liCount;
|
||||
revPFound = true;
|
||||
capturedAnchorIdLen = 0;
|
||||
revVisChars = 0;
|
||||
currentTextNode = 1; // Reset text node counter for this element
|
||||
if (revChar <= 0 && targetTextNode <= 1) {
|
||||
targetVisChars = totalVisChars;
|
||||
revDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void onCloseTag() {
|
||||
if (nonVisibleDepth > 0) {
|
||||
nonVisibleDepth--;
|
||||
if (htmlDepth > 0) htmlDepth--;
|
||||
return;
|
||||
}
|
||||
|
||||
// Legacy mode: each direct child element closing advances the text node index.
|
||||
if (stepCount == 0 && revPFound && !revDone && paragraphHtmlDepth >= 0 && htmlDepth == paragraphHtmlDepth + 1) {
|
||||
currentTextNode++;
|
||||
if (currentTextNode == targetTextNode && revChar <= 0) {
|
||||
targetVisChars = totalVisChars;
|
||||
revDone = true;
|
||||
}
|
||||
}
|
||||
// Legacy mode: stop tracking when the matched paragraph itself closes.
|
||||
if (stepCount == 0 && revPFound && !revDone && paragraphHtmlDepth >= 0 && htmlDepth == paragraphHtmlDepth) {
|
||||
revPFound = false;
|
||||
paragraphHtmlDepth = -1;
|
||||
}
|
||||
|
||||
// Ancestry mode: advance text node when a direct child of the fully-matched element closes.
|
||||
if (stepCount > 0 && matchedDepth == stepCount && revPFound && !revDone) {
|
||||
const int elementDepth = stepEnteredAtDepth[stepCount - 1];
|
||||
if (htmlDepth == elementDepth + 1) {
|
||||
currentTextNode++;
|
||||
if (currentTextNode == targetTextNode && revChar <= 0) {
|
||||
targetVisChars = totalVisChars;
|
||||
revDone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (stepCount > 0 && matchedDepth > 0) {
|
||||
const int step = matchedDepth - 1;
|
||||
if (insideStep[step] && htmlDepth == stepEnteredAtDepth[step]) {
|
||||
insideStep[step] = false;
|
||||
matchedDepth--;
|
||||
// If the fully-matched element just closed without finding the target, abort.
|
||||
if (matchedDepth < stepCount && revPFound && !revDone) {
|
||||
revPFound = false;
|
||||
}
|
||||
for (int i = matchedDepth + 1; i < stepCount; i++) {
|
||||
siblingCounters[i] = 0;
|
||||
insideStep[i] = false;
|
||||
stepEnteredAtDepth[i] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (htmlDepth > 0) htmlDepth--;
|
||||
}
|
||||
|
||||
void processByteInTag(uint8_t c) {
|
||||
switch (tagState) {
|
||||
case TAG_IDLE:
|
||||
if (c == '/') {
|
||||
tagIsClose = true;
|
||||
tagState = TAG_IN_NAME;
|
||||
} else if (c != '!' && c != '?') {
|
||||
tagIsClose = false;
|
||||
tagName[0] = static_cast<char>(c);
|
||||
tagNameLen = 1;
|
||||
tagState = TAG_IN_NAME;
|
||||
}
|
||||
break;
|
||||
case TAG_IN_NAME:
|
||||
if (c == '>' || c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '/') {
|
||||
tagName[tagNameLen] = '\0';
|
||||
if (tagNameLen > 0) {
|
||||
if (tagIsClose)
|
||||
onCloseTag();
|
||||
else
|
||||
onOpenTag();
|
||||
// Self-closing open tag (<br/>). Don't double-fire for close tags (</br/>).
|
||||
if (c == '/' && !tagIsClose) onCloseTag();
|
||||
}
|
||||
tagNameLen = 0;
|
||||
tagState = (c == '>') ? TAG_IDLE : TAG_ATTRS;
|
||||
} else if (tagNameLen + 1 < static_cast<int>(sizeof(tagName))) {
|
||||
tagName[tagNameLen++] = static_cast<char>(c);
|
||||
}
|
||||
break;
|
||||
case TAG_ATTRS:
|
||||
// Track quoted attribute values so '/' inside them is not mistaken for self-closing.
|
||||
if (!inAttrQuote) {
|
||||
if (c == '"' || c == '\'') {
|
||||
inAttrQuote = true;
|
||||
attrQuoteChar = c;
|
||||
}
|
||||
} else if (c == attrQuoteChar) {
|
||||
inAttrQuote = false;
|
||||
attrQuoteChar = 0;
|
||||
}
|
||||
if (capturingAnchorTag) {
|
||||
scanAnchorAttribute(c);
|
||||
}
|
||||
// Only treat '/' as self-closing when outside a quoted attribute value.
|
||||
if (c == '/' && !inAttrQuote) {
|
||||
endAnchorIdScan();
|
||||
onCloseTag();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
explicit ParagraphStreamer(size_t targetByte) : fwdTarget(targetByte), revChar(0) {
|
||||
memset(stepEnteredAtDepth, -1, sizeof(stepEnteredAtDepth));
|
||||
}
|
||||
|
||||
ParagraphStreamer(int paragraph, int charOff, int textNodeIdx = 1)
|
||||
: fwdTarget(SIZE_MAX), revChar(charOff), revParagraph(paragraph), targetTextNode(textNodeIdx) {
|
||||
memset(stepEnteredAtDepth, -1, sizeof(stepEnteredAtDepth));
|
||||
}
|
||||
|
||||
ParagraphStreamer(const XPathStep* xpathSteps, int xpathStepCount, int charOff, int textNodeIdx = 1)
|
||||
: fwdTarget(SIZE_MAX),
|
||||
revChar(charOff),
|
||||
steps(xpathSteps),
|
||||
stepCount(xpathStepCount),
|
||||
targetTextNode(textNodeIdx) {
|
||||
memset(stepEnteredAtDepth, -1, sizeof(stepEnteredAtDepth));
|
||||
}
|
||||
|
||||
size_t write(uint8_t c) override {
|
||||
if (!fwdCaptured && bytesWritten >= fwdTarget) {
|
||||
fwdResult = pCount;
|
||||
fwdCaptured = true;
|
||||
}
|
||||
bytesWritten++;
|
||||
|
||||
if (globalInEntity) {
|
||||
if (entityLen + 1 < MAX_ENTITY_SIZE) {
|
||||
entityBuffer[entityLen++] = static_cast<char>(c);
|
||||
} else {
|
||||
flushEntityAsLiteral();
|
||||
globalInEntity = false;
|
||||
entityLen = 0;
|
||||
}
|
||||
if (globalInEntity) {
|
||||
if (c == ';') {
|
||||
finishEntity();
|
||||
} else if (c == '<' || c == ' ' || c == '\t' || c == '\n' || c == '\r') {
|
||||
flushEntityAsLiteral();
|
||||
globalInEntity = false;
|
||||
entityLen = 0;
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (c == '<') {
|
||||
globalInTag = true;
|
||||
tagState = TAG_IDLE;
|
||||
tagNameLen = 0;
|
||||
tagIsClose = false;
|
||||
capturingAnchorTag = false;
|
||||
resetAnchorAttrScan();
|
||||
inAttrQuote = false;
|
||||
attrQuoteChar = 0;
|
||||
} else if (c == '>') {
|
||||
if (tagState == TAG_ATTRS) {
|
||||
endAnchorIdScan();
|
||||
}
|
||||
globalInTag = false;
|
||||
inAttrQuote = false;
|
||||
if (tagState == TAG_IN_NAME && tagNameLen > 0) {
|
||||
tagName[tagNameLen] = '\0';
|
||||
if (tagIsClose)
|
||||
onCloseTag();
|
||||
else
|
||||
onOpenTag();
|
||||
tagNameLen = 0;
|
||||
}
|
||||
tagState = TAG_IDLE;
|
||||
} else if (globalInTag) {
|
||||
processByteInTag(c);
|
||||
} else if (nonVisibleDepth > 0) {
|
||||
// Ignore head/style/script/title text. KOReader XPaths are body-relative, and CSS text
|
||||
// should not contribute to intra-spine progress.
|
||||
} else {
|
||||
if (c == '&') {
|
||||
globalInEntity = true;
|
||||
entityBuffer[0] = '&';
|
||||
entityLen = 1;
|
||||
} else {
|
||||
const bool startsCodepoint = (c & 0xC0) != 0x80;
|
||||
if (startsCodepoint) onVisibleCodepoint();
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
size_t write(const uint8_t* buffer, size_t size) override {
|
||||
for (size_t i = 0; i < size; i++) write(buffer[i]);
|
||||
return size;
|
||||
}
|
||||
|
||||
int paragraphCount() const { return fwdCaptured ? fwdResult : pCount; }
|
||||
int getParagraphAtMatch() const { return paragraphAtMatch; }
|
||||
int getListItemAtMatch() const { return liCountAtMatch; }
|
||||
const char* getCapturedAnchorId() const { return capturedAnchorIdLen > 0 ? capturedAnchorId : nullptr; }
|
||||
size_t totalBytes() const { return bytesWritten; }
|
||||
bool found() const { return revDone || revPFound; }
|
||||
size_t getTotalVisChars() const { return totalVisChars; }
|
||||
size_t getTargetVisChars() const { return targetVisChars; }
|
||||
float progress() const {
|
||||
return totalVisChars > 0 ? static_cast<float>(targetVisChars) / static_cast<float>(totalVisChars) : 0.0f;
|
||||
}
|
||||
};
|
||||
|
||||
bool streamSpine(const std::shared_ptr<Epub>& epub, int spineIndex, ParagraphStreamer& s) {
|
||||
const auto href = epub->getSpineItem(spineIndex).href;
|
||||
return !href.empty() && epub->readItemContentsToStream(href, s, 1024);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
SavedProgressPosition ProgressMapper::toSavedProgress(const std::shared_ptr<Epub>& epub,
|
||||
const CrossPointPosition& pos) {
|
||||
SavedProgressPosition result;
|
||||
float intra =
|
||||
(pos.totalPages > 1) ? static_cast<float>(pos.pageNumber) / static_cast<float>(pos.totalPages - 1) : 0.0f;
|
||||
result.percentage = epub->calculateProgress(pos.spineIndex, intra);
|
||||
if (pos.hasParagraphIndex && pos.paragraphIndex > 0) {
|
||||
result.xpath = ChapterXPathResolver::findXPathForParagraph(epub, pos.spineIndex, pos.paragraphIndex);
|
||||
}
|
||||
// Fall back to progress-based XPath, then synthetic progress mapping.
|
||||
if (result.xpath.empty()) {
|
||||
result.xpath = ChapterXPathResolver::findXPathForProgress(epub, pos.spineIndex, intra);
|
||||
}
|
||||
if (result.xpath.empty()) {
|
||||
result.xpath = generateXPath(epub, pos.spineIndex, intra);
|
||||
}
|
||||
LOG_DBG("PM", "-> Progress: spine=%d page=%d/%d %.2f%% %s", pos.spineIndex, pos.pageNumber, pos.totalPages,
|
||||
result.percentage * 100, result.xpath.c_str());
|
||||
return result;
|
||||
}
|
||||
|
||||
CrossPointPosition ProgressMapper::toCrossPoint(const std::shared_ptr<Epub>& epub, const SavedProgressPosition& koPos,
|
||||
GfxRenderer& renderer, int currentSpineIndex,
|
||||
int totalPagesInCurrentSpine, int fallbackTotalPages) {
|
||||
CrossPointPosition result{};
|
||||
const size_t bookSize = epub->getBookSize();
|
||||
if (bookSize == 0) return result;
|
||||
|
||||
const int spineCount = epub->getSpineItemsCount();
|
||||
const float clampedPercentage = std::max(0.0f, std::min(1.0f, koPos.percentage));
|
||||
const size_t targetBytes = static_cast<size_t>(static_cast<float>(bookSize) * clampedPercentage);
|
||||
|
||||
const int docFrag = parseIndex(koPos.xpath, "/body/DocFragment[");
|
||||
const int xpathP = parseIndex(koPos.xpath, "/p[", true);
|
||||
const int xpathChar = parseCharOffset(koPos.xpath);
|
||||
const int xpathTextNode = parseTextNodeIndex(koPos.xpath);
|
||||
const int xpathSpine = (docFrag >= 1) ? (docFrag - 1) : -1;
|
||||
|
||||
XPathStep xpathSteps[MAX_XPATH_DEPTH];
|
||||
const int xpathStepCount = parseXPathSteps(koPos.xpath, xpathSteps);
|
||||
// Use ancestry mode whenever the XPath has a structured path (always more accurate than global counting).
|
||||
const bool useAncestry = xpathStepCount > 0;
|
||||
|
||||
if (xpathSpine >= 0 && xpathSpine < spineCount) {
|
||||
result.spineIndex = xpathSpine;
|
||||
} else {
|
||||
for (int i = 0; i < spineCount; i++) {
|
||||
if (epub->getCumulativeSpineItemSize(i) >= targetBytes) {
|
||||
result.spineIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const size_t prevCum = (result.spineIndex > 0) ? epub->getCumulativeSpineItemSize(result.spineIndex - 1) : 0;
|
||||
const size_t spineSize = epub->getCumulativeSpineItemSize(result.spineIndex) - prevCum;
|
||||
|
||||
if (result.spineIndex == currentSpineIndex && totalPagesInCurrentSpine > 0) {
|
||||
result.totalPages = totalPagesInCurrentSpine;
|
||||
} else if (currentSpineIndex >= 0 && currentSpineIndex < spineCount && totalPagesInCurrentSpine > 0) {
|
||||
const size_t pc = (currentSpineIndex > 0) ? epub->getCumulativeSpineItemSize(currentSpineIndex - 1) : 0;
|
||||
const size_t cs = epub->getCumulativeSpineItemSize(currentSpineIndex) - pc;
|
||||
if (cs > 0)
|
||||
result.totalPages = std::max(
|
||||
1, static_cast<int>(totalPagesInCurrentSpine * static_cast<float>(spineSize) / static_cast<float>(cs)));
|
||||
}
|
||||
|
||||
if (result.totalPages <= 0) {
|
||||
Section tempSection(epub, result.spineIndex, renderer);
|
||||
if (auto cachedCount = tempSection.getCachedPageCount()) {
|
||||
result.totalPages = *cachedCount;
|
||||
} else if (fallbackTotalPages > 0) {
|
||||
result.totalPages = fallbackTotalPages;
|
||||
} else {
|
||||
result.totalPages = 1; // Prevent division by zero and give a fallback
|
||||
}
|
||||
}
|
||||
|
||||
float intra = 0.0f;
|
||||
bool resolvedIntra = false;
|
||||
if (useAncestry) {
|
||||
ParagraphStreamer s(xpathSteps, xpathStepCount, xpathChar, xpathTextNode);
|
||||
if (streamSpine(epub, result.spineIndex, s) && s.found()) {
|
||||
intra = s.progress();
|
||||
resolvedIntra = true;
|
||||
const int pAtMatch = s.getParagraphAtMatch();
|
||||
if (pAtMatch > 0) {
|
||||
result.paragraphIndex = static_cast<uint16_t>(pAtMatch);
|
||||
result.hasParagraphIndex = true;
|
||||
}
|
||||
if (xpathStepCount > 0 && strcasecmp(xpathSteps[xpathStepCount - 1].tag, "li") == 0) {
|
||||
const int liAtMatch = s.getListItemAtMatch();
|
||||
if (liAtMatch > 0) {
|
||||
result.liIndex = static_cast<uint16_t>(liAtMatch);
|
||||
result.hasLiIndex = true;
|
||||
}
|
||||
}
|
||||
const char* anchorId = s.getCapturedAnchorId();
|
||||
if (anchorId) {
|
||||
strncpy(result.xpathAnchorId, anchorId, sizeof(result.xpathAnchorId) - 1);
|
||||
}
|
||||
LOG_DBG("PM", "XPath ancestry(%s[%d])/text()[%d]+%d -> %.1f%% (target=%zu total=%zu p~%d li~%d anchor=%s)",
|
||||
xpathSteps[xpathStepCount - 1].tag, xpathSteps[xpathStepCount - 1].siblingIndex, xpathTextNode, xpathChar,
|
||||
intra * 100, s.getTargetVisChars(), s.getTotalVisChars(), pAtMatch,
|
||||
result.hasLiIndex ? static_cast<int>(result.liIndex) : 0, anchorId ? anchorId : "none");
|
||||
}
|
||||
} else if (xpathP > 0) {
|
||||
ParagraphStreamer s(xpathP, xpathChar, xpathTextNode);
|
||||
if (streamSpine(epub, result.spineIndex, s) && s.found()) {
|
||||
intra = s.progress();
|
||||
resolvedIntra = true;
|
||||
LOG_DBG("PM", "XPath p[%d]/text()[%d]+%d -> %.1f%% (target=%zu total=%zu)", xpathP, xpathTextNode, xpathChar,
|
||||
intra * 100, s.getTargetVisChars(), s.getTotalVisChars());
|
||||
}
|
||||
}
|
||||
if (!resolvedIntra && xpathSpine >= 0 && xpathSpine < spineCount && isChapterStartXPath(koPos.xpath)) {
|
||||
intra = 0.0f;
|
||||
resolvedIntra = true;
|
||||
LOG_DBG("PM", "Chapter-start XPath %s -> spine=%d page start", koPos.xpath.c_str(), result.spineIndex);
|
||||
}
|
||||
if (!resolvedIntra) {
|
||||
const size_t bytesIn = (targetBytes > prevCum) ? (targetBytes - prevCum) : 0;
|
||||
intra = std::max(0.0f, std::min(1.0f, static_cast<float>(bytesIn) / static_cast<float>(spineSize)));
|
||||
}
|
||||
|
||||
result.pageNumber = std::max(
|
||||
0, std::min(static_cast<int>(intra * static_cast<float>(result.totalPages - 1) + 0.5f), result.totalPages - 1));
|
||||
LOG_DBG("PM", "<- Progress: %.2f%% %s -> spine=%d page=%d/%d", koPos.percentage * 100, koPos.xpath.c_str(),
|
||||
result.spineIndex, result.pageNumber, result.totalPages);
|
||||
|
||||
// Refine page using section cache LUTs: li index, anchor, or paragraph index.
|
||||
if (result.hasLiIndex || result.xpathAnchorId[0] != '\0' || result.hasParagraphIndex) {
|
||||
Section tempSection(epub, result.spineIndex, renderer);
|
||||
bool refined = false;
|
||||
if (result.hasLiIndex) {
|
||||
const auto liPage = tempSection.getPageForListItemIndex(result.liIndex);
|
||||
if (liPage.has_value()) {
|
||||
LOG_DBG("PM", "Li index %u -> page %d (was %d)", result.liIndex, *liPage, result.pageNumber);
|
||||
result.pageNumber = *liPage;
|
||||
refined = true;
|
||||
} else {
|
||||
LOG_DBG("PM", "Li index %u not found in section LUT", result.liIndex);
|
||||
}
|
||||
}
|
||||
if (!refined && result.xpathAnchorId[0] != '\0') {
|
||||
const auto anchorPage = tempSection.getPageForAnchor(std::string(result.xpathAnchorId));
|
||||
if (anchorPage.has_value()) {
|
||||
LOG_DBG("PM", "Anchor '%s' -> page %d (was %d)", result.xpathAnchorId, *anchorPage, result.pageNumber);
|
||||
result.pageNumber = *anchorPage;
|
||||
refined = true;
|
||||
} else {
|
||||
LOG_DBG("PM", "Anchor '%s' not found in section cache", result.xpathAnchorId);
|
||||
}
|
||||
}
|
||||
if (!refined && result.hasParagraphIndex) {
|
||||
const auto paragraphPage = tempSection.getPageForParagraphIndex(result.paragraphIndex);
|
||||
const auto nextParagraphPage = tempSection.getPageForParagraphIndex(result.paragraphIndex + 1);
|
||||
if (paragraphPage.has_value()) {
|
||||
int refinedPage = std::max(result.pageNumber, static_cast<int>(*paragraphPage));
|
||||
if (nextParagraphPage.has_value()) {
|
||||
const int lutSpan = static_cast<int>(*nextParagraphPage) - static_cast<int>(*paragraphPage);
|
||||
// Only cap when the LUT span is >1. A span of 1 means the LUT granularity is too
|
||||
// coarse to trust over the intra-spine position (e.g. a stale cache where the paragraph
|
||||
// occupies different pages than at build time).
|
||||
if (lutSpan > 1 && refinedPage >= static_cast<int>(*nextParagraphPage)) {
|
||||
refinedPage = static_cast<int>(*nextParagraphPage) - 1;
|
||||
}
|
||||
}
|
||||
char nextParaBuf[8];
|
||||
if (nextParagraphPage.has_value())
|
||||
snprintf(nextParaBuf, sizeof(nextParaBuf), "%d", *nextParagraphPage);
|
||||
else
|
||||
snprintf(nextParaBuf, sizeof(nextParaBuf), "none");
|
||||
LOG_DBG("PM", "Paragraph %u -> LUT page %d, nextPara page %s, intra page %d, using %d", result.paragraphIndex,
|
||||
*paragraphPage, nextParaBuf, result.pageNumber, refinedPage);
|
||||
result.pageNumber = refinedPage;
|
||||
} else {
|
||||
LOG_DBG("PM", "Paragraph %u not found in section LUT", result.paragraphIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string ProgressMapper::generateXPath(const std::shared_ptr<Epub>& epub, int spineIndex, float intra) {
|
||||
const std::string base = "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body";
|
||||
if (intra <= 0.0f) return base;
|
||||
|
||||
size_t spineSize = 0;
|
||||
const auto href = epub->getSpineItem(spineIndex).href;
|
||||
if (href.empty() || !epub->getItemSize(href, &spineSize) || spineSize == 0) return base;
|
||||
|
||||
ParagraphStreamer s(static_cast<size_t>(spineSize * std::min(intra, 1.0f)));
|
||||
if (!streamSpine(epub, spineIndex, s)) return base;
|
||||
|
||||
const int p = s.paragraphCount();
|
||||
return (p > 0) ? base + "/p[" + std::to_string(p) + "]" : base;
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
#pragma once
|
||||
#include <Epub.h>
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
/**
|
||||
* CrossPoint position representation.
|
||||
*/
|
||||
struct CrossPointPosition {
|
||||
int spineIndex; // Current spine item (chapter) index
|
||||
int pageNumber; // Current page within the spine item
|
||||
int totalPages; // Total pages in the current spine item
|
||||
uint16_t paragraphIndex = 0; // 1-based synthetic paragraph index from XPath p[N]
|
||||
bool hasParagraphIndex = false; // True when paragraphIndex was resolved from XPath
|
||||
uint16_t liIndex = 0; // Running <li> count at the matched XPath element
|
||||
bool hasLiIndex = false; // True when target element is <li> and liIndex was resolved
|
||||
char xpathAnchorId[64] = {}; // First <a id> captured inside the matched XPath element
|
||||
};
|
||||
|
||||
#include "KOReaderPosition.h" // SavedProgressPosition
|
||||
|
||||
/**
|
||||
* Maps between CrossPoint and SavedProgress position formats, such as those used by KOReader.
|
||||
*
|
||||
* CrossPoint tracks position as (spineIndex, pageNumber).
|
||||
* SavedProgress uses XPath-like strings + percentage.
|
||||
*
|
||||
* Since CrossPoint discards HTML structure during parsing, we generate
|
||||
* synthetic XPath strings based on spine index, using percentage as the
|
||||
* primary sync mechanism.
|
||||
*/
|
||||
class ProgressMapper {
|
||||
public:
|
||||
/**
|
||||
* Convert CrossPoint position to SavedProgress format.
|
||||
*
|
||||
* @param epub The EPUB book
|
||||
* @param pos CrossPoint position
|
||||
* @return SavedProgress position
|
||||
*/
|
||||
static SavedProgressPosition toSavedProgress(const std::shared_ptr<Epub>& epub, const CrossPointPosition& pos);
|
||||
|
||||
/**
|
||||
* Convert SavedProgress position to CrossPoint format.
|
||||
*
|
||||
* Note: The returned pageNumber may be approximate since different
|
||||
* rendering settings produce different page counts.
|
||||
*
|
||||
* @param epub The EPUB book
|
||||
* @param savedPos SavedProgress position
|
||||
* @param renderer GfxRenderer for page count estimation
|
||||
* @param currentSpineIndex Index of the currently open spine item (for density estimation)
|
||||
* @param totalPagesInCurrentSpine Total pages in the current spine item (for density estimation)
|
||||
* @return CrossPoint position
|
||||
*/
|
||||
static CrossPointPosition toCrossPoint(const std::shared_ptr<Epub>& epub, const SavedProgressPosition& savedPos,
|
||||
GfxRenderer& renderer, int currentSpineIndex = -1,
|
||||
int totalPagesInCurrentSpine = 0, int fallbackTotalPages = 0);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Generate a fallback XPath by streaming the spine item's XHTML and resolving
|
||||
* a paragraph/text position from intra-spine progress.
|
||||
* Produces a full ancestry path such as
|
||||
* /body/DocFragment[3]/body/p[42]/text().17.
|
||||
*/
|
||||
static std::string generateXPath(const std::shared_ptr<Epub>& epub, int spineIndex, float intraSpineProgress);
|
||||
};
|
||||
@@ -1,558 +0,0 @@
|
||||
#include "ZipFile.h"
|
||||
|
||||
#include <HalStorage.h>
|
||||
#include <InflateReader.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
struct ZipInflateCtx {
|
||||
InflateReader reader; // Must be first — callback casts uzlib_uncomp* to ZipInflateCtx*
|
||||
HalFile* file = nullptr;
|
||||
size_t fileRemaining = 0;
|
||||
uint8_t* readBuf = nullptr;
|
||||
size_t readBufSize = 0;
|
||||
};
|
||||
|
||||
namespace {
|
||||
constexpr uint16_t ZIP_METHOD_STORED = 0;
|
||||
constexpr uint16_t ZIP_METHOD_DEFLATED = 8;
|
||||
|
||||
// RAII zip: opens the zip if not already open, closes on destruction only if
|
||||
// it performed the open. Removes the wasOpen/close boilerplate from every method.
|
||||
class ScopedOpenClose final {
|
||||
public:
|
||||
[[nodiscard]] explicit ScopedOpenClose(ZipFile& zf) : zf(zf), needsClose(!zf.isOpen()) {
|
||||
if (needsClose) ok = zf.open();
|
||||
}
|
||||
~ScopedOpenClose() {
|
||||
if (needsClose && ok) zf.close();
|
||||
}
|
||||
ScopedOpenClose(const ScopedOpenClose&) = delete;
|
||||
ScopedOpenClose& operator=(const ScopedOpenClose&) = delete;
|
||||
ScopedOpenClose(ScopedOpenClose&&) = delete;
|
||||
ScopedOpenClose& operator=(ScopedOpenClose&&) = delete;
|
||||
explicit operator bool() const { return ok || !needsClose; }
|
||||
|
||||
private:
|
||||
ZipFile& zf;
|
||||
bool needsClose = false;
|
||||
bool ok = true; // true when zip was already open (no open() call needed)
|
||||
};
|
||||
|
||||
int zipReadCallback(uzlib_uncomp* uncomp) {
|
||||
auto* ctx = reinterpret_cast<ZipInflateCtx*>(uncomp);
|
||||
if (ctx->fileRemaining == 0) return -1;
|
||||
|
||||
const size_t toRead = ctx->fileRemaining < ctx->readBufSize ? ctx->fileRemaining : ctx->readBufSize;
|
||||
const size_t bytesRead = ctx->file->read(ctx->readBuf, toRead);
|
||||
ctx->fileRemaining -= bytesRead;
|
||||
|
||||
if (bytesRead == 0) return -1;
|
||||
|
||||
uncomp->source = ctx->readBuf + 1;
|
||||
uncomp->source_limit = ctx->readBuf + bytesRead;
|
||||
return ctx->readBuf[0];
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool ZipFile::loadAllFileStatSlims() {
|
||||
const ScopedOpenClose zip{*this};
|
||||
if (!zip) return false;
|
||||
|
||||
if (!loadZipDetails()) return false;
|
||||
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
|
||||
uint32_t sig;
|
||||
char itemName[256];
|
||||
fileStatSlimCache.clear();
|
||||
fileStatSlimCache.reserve(zipDetails.totalEntries);
|
||||
|
||||
while (file.available()) {
|
||||
file.read(&sig, 4);
|
||||
if (sig != 0x02014b50) break; // End of list
|
||||
|
||||
FileStatSlim fileStat = {};
|
||||
|
||||
file.seekCur(6);
|
||||
file.read(&fileStat.method, 2);
|
||||
file.seekCur(8);
|
||||
file.read(&fileStat.compressedSize, 4);
|
||||
file.read(&fileStat.uncompressedSize, 4);
|
||||
uint16_t nameLen, m, k;
|
||||
file.read(&nameLen, 2);
|
||||
file.read(&m, 2);
|
||||
file.read(&k, 2);
|
||||
file.seekCur(8);
|
||||
file.read(&fileStat.localHeaderOffset, 4);
|
||||
|
||||
if (nameLen < sizeof(itemName)) {
|
||||
file.read(itemName, nameLen);
|
||||
itemName[nameLen] = '\0';
|
||||
fileStatSlimCache.emplace(itemName, fileStat);
|
||||
} else {
|
||||
// Skip over oversized entry names to avoid writing past fixed buffer.
|
||||
file.seekCur(nameLen);
|
||||
}
|
||||
|
||||
// Skip the rest of this entry (extra field + comment)
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
// Set cursor to start of central directory for sequential access
|
||||
lastCentralDirPos = zipDetails.centralDirOffset;
|
||||
lastCentralDirPosValid = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) {
|
||||
if (!fileStatSlimCache.empty()) {
|
||||
const auto it = fileStatSlimCache.find(filename);
|
||||
if (it != fileStatSlimCache.end()) {
|
||||
*fileStat = it->second;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const ScopedOpenClose zip{*this};
|
||||
if (!zip) return false;
|
||||
|
||||
if (!loadZipDetails()) return false;
|
||||
|
||||
// Phase 1: Try scanning from cursor position first
|
||||
uint32_t startPos = lastCentralDirPosValid ? lastCentralDirPos : zipDetails.centralDirOffset;
|
||||
bool wrapped = false;
|
||||
bool found = false;
|
||||
|
||||
file.seek(startPos);
|
||||
|
||||
uint32_t sig;
|
||||
char itemName[256];
|
||||
|
||||
while (true) {
|
||||
uint32_t entryStart = file.position();
|
||||
|
||||
if (file.read(&sig, 4) != 4 || sig != 0x02014b50) {
|
||||
// End of central directory
|
||||
if (!wrapped && lastCentralDirPosValid && startPos != zipDetails.centralDirOffset) {
|
||||
// Wrap around to beginning
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
wrapped = true;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// If we've wrapped and reached our start position, stop
|
||||
if (wrapped && entryStart >= startPos) {
|
||||
break;
|
||||
}
|
||||
|
||||
file.seekCur(6);
|
||||
file.read(&fileStat->method, 2);
|
||||
file.seekCur(8);
|
||||
file.read(&fileStat->compressedSize, 4);
|
||||
file.read(&fileStat->uncompressedSize, 4);
|
||||
uint16_t nameLen, m, k;
|
||||
file.read(&nameLen, 2);
|
||||
file.read(&m, 2);
|
||||
file.read(&k, 2);
|
||||
file.seekCur(8);
|
||||
file.read(&fileStat->localHeaderOffset, 4);
|
||||
|
||||
if (nameLen < 256) {
|
||||
file.read(itemName, nameLen);
|
||||
itemName[nameLen] = '\0';
|
||||
|
||||
if (strcmp(itemName, filename) == 0) {
|
||||
// Found it! Update cursor to next entry
|
||||
file.seekCur(m + k);
|
||||
lastCentralDirPos = file.position();
|
||||
lastCentralDirPosValid = true;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Name too long, skip it
|
||||
file.seekCur(nameLen);
|
||||
}
|
||||
|
||||
// Skip extra field + comment
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
long ZipFile::getDataOffset(const FileStatSlim& fileStat) {
|
||||
const ScopedOpenClose zip{*this};
|
||||
if (!zip) return -1;
|
||||
|
||||
constexpr auto localHeaderSize = 30;
|
||||
|
||||
uint8_t pLocalHeader[localHeaderSize];
|
||||
const uint64_t fileOffset = fileStat.localHeaderOffset;
|
||||
|
||||
file.seek(fileOffset);
|
||||
const size_t read = file.read(pLocalHeader, localHeaderSize);
|
||||
|
||||
if (read != localHeaderSize) {
|
||||
LOG_ERR("ZIP", "Something went wrong reading the local header");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
||||
0x04034b50 /* ZIP local file header signature */) {
|
||||
LOG_ERR("ZIP", "Not a valid zip file header");
|
||||
return -1;
|
||||
}
|
||||
|
||||
const uint16_t filenameLength = pLocalHeader[26] + (pLocalHeader[27] << 8);
|
||||
const uint16_t extraOffset = pLocalHeader[28] + (pLocalHeader[29] << 8);
|
||||
return fileOffset + localHeaderSize + filenameLength + extraOffset;
|
||||
}
|
||||
|
||||
bool ZipFile::loadZipDetails() {
|
||||
if (zipDetails.isSet) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const ScopedOpenClose zip{*this};
|
||||
if (!zip) return false;
|
||||
|
||||
const size_t fileSize = file.size();
|
||||
if (fileSize < 22) {
|
||||
LOG_ERR("ZIP", "File too small to be a valid zip");
|
||||
return false; // Minimum EOCD size is 22 bytes
|
||||
}
|
||||
|
||||
// We scan the last 1KB (or the whole file if smaller) for the EOCD signature
|
||||
// 0x06054b50 is stored as 0x50, 0x4b, 0x05, 0x06 in little-endian
|
||||
const int scanRange = fileSize > 1024 ? 1024 : fileSize;
|
||||
const auto buffer = static_cast<uint8_t*>(malloc(scanRange));
|
||||
if (!buffer) {
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for EOCD scan buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
file.seek(fileSize - scanRange);
|
||||
file.read(buffer, scanRange);
|
||||
|
||||
// Scan backwards for the signature
|
||||
int foundOffset = -1;
|
||||
for (int i = scanRange - 22; i >= 0; i--) {
|
||||
constexpr uint32_t signature = 0x06054b50;
|
||||
if (*reinterpret_cast<uint32_t*>(&buffer[i]) == signature) {
|
||||
foundOffset = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundOffset == -1) {
|
||||
LOG_ERR("ZIP", "EOCD signature not found in zip file");
|
||||
free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now extract the values we need from the EOCD record
|
||||
// Relative positions within EOCD:
|
||||
// Offset 10: Total number of entries (2 bytes)
|
||||
// Offset 16: Offset of start of central directory with respect to the starting disk number (4 bytes)
|
||||
zipDetails.totalEntries = *reinterpret_cast<uint16_t*>(&buffer[foundOffset + 10]);
|
||||
zipDetails.centralDirOffset = *reinterpret_cast<uint32_t*>(&buffer[foundOffset + 16]);
|
||||
zipDetails.isSet = true;
|
||||
|
||||
free(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ZipFile::open() {
|
||||
if (!Storage.openFileForRead("ZIP", filePath, file)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ZipFile::close() {
|
||||
if (file) {
|
||||
// Explicit close() required: member variable persists beyond function scope
|
||||
file.close();
|
||||
}
|
||||
lastCentralDirPos = 0;
|
||||
lastCentralDirPosValid = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) {
|
||||
FileStatSlim fileStat = {};
|
||||
if (!loadFileStatSlim(filename, &fileStat)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*size = static_cast<size_t>(fileStat.uncompressedSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
int ZipFile::fillUncompressedSizes(std::deque<SizeTarget>& targets, std::deque<uint32_t>& sizes) {
|
||||
if (targets.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ScopedOpenClose zip{*this};
|
||||
if (!zip) return 0;
|
||||
|
||||
if (!loadZipDetails()) return 0;
|
||||
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
|
||||
int matched = 0;
|
||||
const int targetCount = static_cast<int>(targets.size());
|
||||
uint32_t sig;
|
||||
char itemName[256];
|
||||
|
||||
while (file.available()) {
|
||||
file.read(&sig, 4);
|
||||
if (sig != 0x02014b50) break;
|
||||
|
||||
file.seekCur(6);
|
||||
uint16_t method;
|
||||
file.read(&method, 2);
|
||||
file.seekCur(8);
|
||||
uint32_t compressedSize, uncompressedSize;
|
||||
file.read(&compressedSize, 4);
|
||||
file.read(&uncompressedSize, 4);
|
||||
uint16_t nameLen, m, k;
|
||||
file.read(&nameLen, 2);
|
||||
file.read(&m, 2);
|
||||
file.read(&k, 2);
|
||||
file.seekCur(8);
|
||||
uint32_t localHeaderOffset;
|
||||
file.read(&localHeaderOffset, 4);
|
||||
|
||||
if (nameLen < 256) {
|
||||
file.read(itemName, nameLen);
|
||||
itemName[nameLen] = '\0';
|
||||
|
||||
uint64_t hash = fnvHash64(itemName, nameLen);
|
||||
SizeTarget key = {hash, nameLen, 0};
|
||||
|
||||
auto it = std::lower_bound(targets.begin(), targets.end(), key, [](const SizeTarget& a, const SizeTarget& b) {
|
||||
return a.hash < b.hash || (a.hash == b.hash && a.len < b.len);
|
||||
});
|
||||
|
||||
while (it != targets.end() && it->hash == hash && it->len == nameLen) {
|
||||
if (it->index < sizes.size()) {
|
||||
sizes[it->index] = uncompressedSize;
|
||||
matched++;
|
||||
}
|
||||
++it;
|
||||
}
|
||||
|
||||
if (matched >= targetCount) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
file.seekCur(nameLen);
|
||||
}
|
||||
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
return matched;
|
||||
}
|
||||
|
||||
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) {
|
||||
const ScopedOpenClose zip{*this};
|
||||
if (!zip) return nullptr;
|
||||
|
||||
FileStatSlim fileStat = {};
|
||||
if (!loadFileStatSlim(filename, &fileStat)) return nullptr;
|
||||
|
||||
const long fileOffset = getDataOffset(fileStat);
|
||||
if (fileOffset < 0) return nullptr;
|
||||
|
||||
file.seek(fileOffset);
|
||||
|
||||
const auto deflatedDataSize = fileStat.compressedSize;
|
||||
const auto inflatedDataSize = fileStat.uncompressedSize;
|
||||
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
|
||||
const auto data = static_cast<uint8_t*>(malloc(dataSize));
|
||||
if (data == nullptr) {
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for output buffer (%zu bytes)", dataSize);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (fileStat.method == ZIP_METHOD_STORED) {
|
||||
// no deflation, just read content
|
||||
const size_t dataRead = file.read(data, inflatedDataSize);
|
||||
|
||||
if (dataRead != inflatedDataSize) {
|
||||
LOG_ERR("ZIP", "Failed to read data");
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Continue out of block with data set
|
||||
} else if (fileStat.method == ZIP_METHOD_DEFLATED) {
|
||||
auto* fileReadBuffer = static_cast<uint8_t*>(malloc(1024));
|
||||
if (!fileReadBuffer) {
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
ZipInflateCtx ctx;
|
||||
ctx.file = &file;
|
||||
ctx.fileRemaining = deflatedDataSize;
|
||||
ctx.readBuf = fileReadBuffer;
|
||||
ctx.readBufSize = 1024;
|
||||
|
||||
if (!ctx.reader.init(true)) {
|
||||
LOG_ERR("ZIP", "Failed to init inflate reader");
|
||||
free(fileReadBuffer);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
ctx.reader.setReadCallback(zipReadCallback);
|
||||
|
||||
if (!ctx.reader.read(data, inflatedDataSize)) {
|
||||
LOG_ERR("ZIP", "Failed to inflate file");
|
||||
free(fileReadBuffer);
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
free(fileReadBuffer);
|
||||
|
||||
// Continue out of block with data set
|
||||
} else {
|
||||
LOG_ERR("ZIP", "Unsupported compression method");
|
||||
free(data);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (trailingNullByte) data[inflatedDataSize] = '\0';
|
||||
if (size) *size = inflatedDataSize;
|
||||
return data;
|
||||
}
|
||||
|
||||
bool ZipFile::readFileToStream(const char* filename, Print& out, const size_t chunkSize) {
|
||||
const ScopedOpenClose zip{*this};
|
||||
if (!zip) return false;
|
||||
|
||||
FileStatSlim fileStat = {};
|
||||
if (!loadFileStatSlim(filename, &fileStat)) return false;
|
||||
|
||||
const long fileOffset = getDataOffset(fileStat);
|
||||
if (fileOffset < 0) return false;
|
||||
|
||||
file.seek(fileOffset);
|
||||
const auto deflatedDataSize = fileStat.compressedSize;
|
||||
const auto inflatedDataSize = fileStat.uncompressedSize;
|
||||
|
||||
if (fileStat.method == ZIP_METHOD_STORED) {
|
||||
// no deflation, just read content
|
||||
const auto buffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
if (!buffer) {
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t remaining = inflatedDataSize;
|
||||
while (remaining > 0) {
|
||||
const size_t dataRead = file.read(buffer, remaining < chunkSize ? remaining : chunkSize);
|
||||
if (dataRead == 0) {
|
||||
LOG_ERR("ZIP", "Could not read more bytes");
|
||||
free(buffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (out.write(buffer, dataRead) != dataRead) {
|
||||
LOG_ERR("ZIP", "Failed to write all output bytes to stream");
|
||||
free(buffer);
|
||||
return false;
|
||||
}
|
||||
remaining -= dataRead;
|
||||
}
|
||||
|
||||
free(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (fileStat.method == ZIP_METHOD_DEFLATED) {
|
||||
auto* fileReadBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
if (!fileReadBuffer) {
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for zip file read buffer");
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* outputBuffer = static_cast<uint8_t*>(malloc(chunkSize));
|
||||
if (!outputBuffer) {
|
||||
LOG_ERR("ZIP", "Failed to allocate memory for output buffer");
|
||||
free(fileReadBuffer);
|
||||
return false;
|
||||
}
|
||||
|
||||
ZipInflateCtx ctx;
|
||||
ctx.file = &file;
|
||||
ctx.fileRemaining = deflatedDataSize;
|
||||
ctx.readBuf = fileReadBuffer;
|
||||
ctx.readBufSize = chunkSize;
|
||||
|
||||
if (!ctx.reader.init(true)) {
|
||||
LOG_ERR("ZIP", "Failed to init inflate reader");
|
||||
free(outputBuffer);
|
||||
free(fileReadBuffer);
|
||||
return false;
|
||||
}
|
||||
ctx.reader.setReadCallback(zipReadCallback);
|
||||
|
||||
bool success = false;
|
||||
size_t totalProduced = 0;
|
||||
|
||||
while (true) {
|
||||
size_t produced;
|
||||
const InflateStatus status = ctx.reader.readAtMost(outputBuffer, chunkSize, &produced);
|
||||
|
||||
totalProduced += produced;
|
||||
if (totalProduced > static_cast<size_t>(inflatedDataSize)) {
|
||||
LOG_ERR("ZIP", "Decompressed size exceeds expected (%zu > %zu)", totalProduced,
|
||||
static_cast<size_t>(inflatedDataSize));
|
||||
break;
|
||||
}
|
||||
|
||||
if (produced > 0) {
|
||||
if (out.write(outputBuffer, produced) != produced) {
|
||||
LOG_ERR("ZIP", "Failed to write all output bytes to stream");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (status == InflateStatus::Done) {
|
||||
if (totalProduced != static_cast<size_t>(inflatedDataSize)) {
|
||||
LOG_ERR("ZIP", "Decompressed size mismatch (expected %zu, got %zu)", static_cast<size_t>(inflatedDataSize),
|
||||
totalProduced);
|
||||
break;
|
||||
}
|
||||
LOG_DBG("ZIP", "Decompressed %d bytes into %d bytes", deflatedDataSize, inflatedDataSize);
|
||||
success = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (status == InflateStatus::Error) {
|
||||
LOG_ERR("ZIP", "Decompression failed");
|
||||
break;
|
||||
}
|
||||
// InflateStatus::Ok: output buffer full, continue
|
||||
}
|
||||
|
||||
free(outputBuffer);
|
||||
free(fileReadBuffer);
|
||||
return success; // ctx.reader destructor frees the ring buffer
|
||||
}
|
||||
|
||||
LOG_ERR("ZIP", "Unsupported compression method");
|
||||
return false;
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
#pragma once
|
||||
#include <HalStorage.h>
|
||||
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
class ZipFile {
|
||||
public:
|
||||
struct FileStatSlim {
|
||||
uint16_t method; // Compression method
|
||||
uint32_t compressedSize; // Compressed size
|
||||
uint32_t uncompressedSize; // Uncompressed size
|
||||
uint32_t localHeaderOffset; // Offset of local file header
|
||||
};
|
||||
|
||||
struct ZipDetails {
|
||||
uint32_t centralDirOffset;
|
||||
uint16_t totalEntries;
|
||||
bool isSet;
|
||||
};
|
||||
|
||||
// Target for batch uncompressed size lookup (sorted by hash, then len)
|
||||
struct SizeTarget {
|
||||
uint64_t hash; // FNV-1a 64-bit hash of normalized path
|
||||
uint16_t len; // Length of path for collision reduction
|
||||
uint16_t index; // Caller's index (e.g. spine index)
|
||||
};
|
||||
|
||||
// FNV-1a 64-bit hash computed from char buffer (no std::string allocation)
|
||||
static uint64_t fnvHash64(const char* s, size_t len) {
|
||||
uint64_t hash = 14695981039346656037ull;
|
||||
for (size_t i = 0; i < len; i++) {
|
||||
hash ^= static_cast<uint8_t>(s[i]);
|
||||
hash *= 1099511628211ull;
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string& filePath;
|
||||
HalFile file;
|
||||
ZipDetails zipDetails = {0, 0, false};
|
||||
std::unordered_map<std::string, FileStatSlim> fileStatSlimCache;
|
||||
|
||||
// Cursor for sequential central-dir scanning optimization
|
||||
uint32_t lastCentralDirPos = 0;
|
||||
bool lastCentralDirPosValid = false;
|
||||
|
||||
bool loadFileStatSlim(const char* filename, FileStatSlim* fileStat);
|
||||
long getDataOffset(const FileStatSlim& fileStat);
|
||||
bool loadZipDetails();
|
||||
|
||||
public:
|
||||
explicit ZipFile(const std::string& filePath) : filePath(filePath) {}
|
||||
~ZipFile() = default;
|
||||
// Zip file can be opened and closed by hand in order to allow for quick calculation of inflated file size
|
||||
// It is NOT recommended to pre-open it for any kind of inflation due to memory constraints
|
||||
bool isOpen() const { return !!file; }
|
||||
bool open();
|
||||
bool close();
|
||||
bool loadAllFileStatSlims();
|
||||
bool getInflatedFileSize(const char* filename, size_t* size);
|
||||
// Batch lookup: scan ZIP central dir once and fill sizes for matching targets.
|
||||
// targets must be sorted by (hash, len). sizes[target.index] receives uncompressedSize.
|
||||
// Returns number of targets matched.
|
||||
int fillUncompressedSizes(std::deque<SizeTarget>& targets, std::deque<uint32_t>& sizes);
|
||||
// Due to the memory required to run each of these, it is recommended to not preopen the zip file for multiple
|
||||
// These functions will open and close the zip as needed
|
||||
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false);
|
||||
bool readFileToStream(const char* filename, Print& out, size_t chunkSize);
|
||||
|
||||
template <typename F>
|
||||
bool enumerateFilePaths(F&& callback) {
|
||||
if (!fileStatSlimCache.empty()) {
|
||||
for (const auto& entry : fileStatSlimCache) {
|
||||
callback(std::string_view{entry.first});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool wasOpen = isOpen();
|
||||
if (!wasOpen && !open()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!loadZipDetails()) {
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
file.seek(zipDetails.centralDirOffset);
|
||||
|
||||
uint32_t sig;
|
||||
char itemName[256];
|
||||
|
||||
while (file.available()) {
|
||||
file.read(&sig, 4);
|
||||
if (sig != 0x02014b50) {
|
||||
break;
|
||||
}
|
||||
|
||||
file.seekCur(24);
|
||||
uint16_t nameLen, m, k;
|
||||
file.read(&nameLen, 2);
|
||||
file.read(&m, 2);
|
||||
file.read(&k, 2);
|
||||
file.seekCur(12);
|
||||
|
||||
if (nameLen < sizeof(itemName)) {
|
||||
file.read(itemName, nameLen);
|
||||
itemName[nameLen] = '\0';
|
||||
callback(std::string_view{itemName, nameLen});
|
||||
} else {
|
||||
file.seekCur(nameLen);
|
||||
}
|
||||
|
||||
file.seekCur(m + k);
|
||||
}
|
||||
|
||||
if (!wasOpen) {
|
||||
close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -13,7 +13,7 @@ process() {
|
||||
|
||||
python scripts/generate_hyphenation_trie.py \
|
||||
--input "build/$lang.bin" \
|
||||
--output "lib/Epub/Epub/hyphenation/generated/hyph-${lang}.trie.h"
|
||||
--output "lib/Epub/Epub/hyphenation/generated/hyph-${lang}.trie.h" # OBSOLETE: legacy engine removed
|
||||
}
|
||||
|
||||
process en
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "RecentBooksStore.h"
|
||||
|
||||
#include <Epub.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JsonSettingsIO.h>
|
||||
@@ -11,6 +10,8 @@
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
|
||||
#include "util/BookCoverUtils.h"
|
||||
|
||||
namespace {
|
||||
constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 3;
|
||||
constexpr char RECENT_BOOKS_FILE_BIN[] = "/.crosspoint/recent.bin";
|
||||
@@ -106,13 +107,12 @@ RecentBook RecentBooksStore::getDataFromBook(std::string path) const {
|
||||
|
||||
LOG_DBG("RBS", "Loading recent book: %s", path.c_str());
|
||||
|
||||
// If epub, try to load the metadata for title/author and cover.
|
||||
// Use buildIfMissing=false to avoid heavy epub loading on boot; getTitle()/getAuthor() may be
|
||||
// blank until the book is opened, and entries with missing title are omitted from recent list.
|
||||
// If epub, read title/author straight from the OPF (Book::open is a light
|
||||
// container parse — no pagination, no CSS).
|
||||
if (FsHelpers::hasEpubExtension(lastBookFileName)) {
|
||||
Epub epub(path, "/.crosspoint");
|
||||
epub.load(false, true);
|
||||
return RecentBook{path, epub.getTitle(), epub.getAuthor(), epub.getThumbBmpPath()};
|
||||
std::string title, author;
|
||||
BookCoverUtils::readMetadata(path, &title, &author);
|
||||
return RecentBook{path, title, author, BookCoverUtils::thumbBmpPathTemplate(path)};
|
||||
} else if (FsHelpers::hasXtcExtension(lastBookFileName)) {
|
||||
// Handle XTC file
|
||||
Xtc xtc(path, "/.crosspoint");
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "SleepActivity.h"
|
||||
|
||||
#include <Epub.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
@@ -15,6 +14,7 @@
|
||||
#include "fontIds.h"
|
||||
#include "images/Logo120.h"
|
||||
#include "images/MoonIcon.h"
|
||||
#include "util/BookCoverUtils.h"
|
||||
|
||||
void SleepActivity::onEnter() {
|
||||
Activity::onEnter();
|
||||
@@ -293,19 +293,12 @@ void SleepActivity::renderCoverSleepScreen() const {
|
||||
coverBmpPath = lastTxt.getCoverBmpPath();
|
||||
} else if (FsHelpers::hasEpubExtension(APP_STATE.openEpubPath)) {
|
||||
// Handle EPUB file
|
||||
Epub lastEpub(APP_STATE.openEpubPath, "/.crosspoint");
|
||||
// Skip loading css since we only need metadata here
|
||||
if (!lastEpub.load(true, true)) {
|
||||
LOG_ERR("SLP", "Failed to load last epub");
|
||||
return (this->*renderNoCoverSleepScreen)();
|
||||
}
|
||||
|
||||
if (!lastEpub.generateCoverBmp(cropped)) {
|
||||
if (!BookCoverUtils::generateCoverBmp(APP_STATE.openEpubPath, cropped)) {
|
||||
LOG_ERR("SLP", "Failed to generate cover bmp");
|
||||
return (this->*renderNoCoverSleepScreen)();
|
||||
}
|
||||
|
||||
coverBmpPath = lastEpub.getCoverBmpPath(cropped);
|
||||
coverBmpPath = BookCoverUtils::coverBmpPath(APP_STATE.openEpubPath, cropped);
|
||||
} else {
|
||||
return (this->*renderNoCoverSleepScreen)();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "HomeActivity.h"
|
||||
|
||||
#include <Bitmap.h>
|
||||
#include <Epub.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <GfxRenderer.h>
|
||||
#include <HalStorage.h>
|
||||
@@ -19,6 +18,7 @@
|
||||
#include "RecentBooksStore.h"
|
||||
#include "components/UITheme.h"
|
||||
#include "fontIds.h"
|
||||
#include "util/BookCoverUtils.h"
|
||||
|
||||
int HomeActivity::getMenuItemCount() const {
|
||||
int count = 4; // File Browser, Recents, File transfer, Settings
|
||||
@@ -61,19 +61,14 @@ void HomeActivity::loadRecentCovers(int coverHeight) {
|
||||
if (!book.coverBmpPath.empty()) {
|
||||
std::string coverPath = UITheme::getCoverThumbPath(book.coverBmpPath, coverHeight);
|
||||
if (!Storage.exists(coverPath.c_str())) {
|
||||
// If epub, try to load the metadata for title/author and cover
|
||||
// If epub, generate the Continue Reading thumbnail from its cover
|
||||
if (FsHelpers::hasEpubExtension(book.path)) {
|
||||
Epub epub(book.path, "/.crosspoint");
|
||||
// Skip loading css since we only need metadata here
|
||||
epub.load(false, true);
|
||||
|
||||
// Try to generate thumbnail image for Continue Reading card
|
||||
if (!showingLoading) {
|
||||
showingLoading = true;
|
||||
popupRect = GUI.drawPopup(renderer, tr(STR_LOADING_POPUP));
|
||||
}
|
||||
GUI.fillPopupProgress(renderer, popupRect, 10 + progress * (90 / recentBooks.size()));
|
||||
bool success = epub.generateThumbBmp(coverHeight);
|
||||
bool success = BookCoverUtils::generateThumbBmp(book.path, coverHeight);
|
||||
if (!success) {
|
||||
RECENT_BOOKS.updateBook(book.path, book.title, book.author, "");
|
||||
book.coverBmpPath = "";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#pragma once
|
||||
#include <Epub/FootnoteEntry.h>
|
||||
#include "FootnoteEntry.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <Epub/FootnoteEntry.h>
|
||||
#include "FootnoteEntry.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#pragma once
|
||||
#include <Epub.h>
|
||||
#include <I18n.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
// including per-band strip re-renders — streams that file instead of
|
||||
// re-decoding the PNG/JPEG.
|
||||
|
||||
#include <Epub/FootnoteEntry.h>
|
||||
#include "FootnoteEntry.h"
|
||||
#include <layout/ChapterLayout.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include <Arduino.h>
|
||||
#include <Epub.h>
|
||||
#include <FontCacheManager.h>
|
||||
#include <FontDecompressor.h>
|
||||
#include <GfxRenderer.h>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
#include "BookCacheUtils.h"
|
||||
|
||||
#include <Epub.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <HalStorage.h>
|
||||
#include <Logging.h>
|
||||
#include <Txt.h>
|
||||
#include <Xtc.h>
|
||||
|
||||
#include "activities/reader/EpubReaderUtils.h"
|
||||
|
||||
bool isBookCacheDirectoryName(const char* name) {
|
||||
if (!name) {
|
||||
return false;
|
||||
@@ -22,7 +24,10 @@ bool isBookCacheDirectoryName(const char* name) {
|
||||
|
||||
void clearBookCache(const std::string& path) {
|
||||
if (FsHelpers::hasEpubExtension(path)) {
|
||||
Epub(path, "/.crosspoint").clearCache();
|
||||
const std::string cacheDir = EpubReaderUtils::cacheDirForBook(path);
|
||||
if (Storage.exists(cacheDir.c_str())) {
|
||||
Storage.removeDir(cacheDir.c_str());
|
||||
}
|
||||
} else if (FsHelpers::hasXtcExtension(path)) {
|
||||
Xtc(path, "/.crosspoint").clearCache();
|
||||
} else if (FsHelpers::hasTxtExtension(path)) {
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
#include "BookCoverUtils.h"
|
||||
|
||||
#include <FreeInkBook.h>
|
||||
#include <FsHelpers.h>
|
||||
#include <HalStorage.h>
|
||||
#include <JpegToBmpConverter.h>
|
||||
#include <Logging.h>
|
||||
#include <Memory.h>
|
||||
#include <PngToBmpConverter.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "activities/reader/EpubReaderUtils.h" // cacheDirForBook
|
||||
#include "activities/reader/FreeInkBookStorage.h" // SdBookSource
|
||||
|
||||
namespace {
|
||||
|
||||
using EpubReaderUtils::cacheDirForBook;
|
||||
using freeink::book::Arena;
|
||||
using freeink::book::Book;
|
||||
using freeink::book::BookStatus;
|
||||
using freeink::book::ManifestItem;
|
||||
using freeink::book::ZipEntryReader;
|
||||
|
||||
// Container open is transient: metadata + ZIP catalog only, freed on return.
|
||||
constexpr size_t kBookArenaSize = 48 * 1024;
|
||||
constexpr size_t kScratchSize = 64 * 1024;
|
||||
|
||||
// Opens the book file's container long enough to run `fn(book, source)`.
|
||||
template <typename Fn>
|
||||
bool withOpenBook(const std::string& epubPath, Fn&& fn) {
|
||||
auto bookBuf = makeUniqueNoThrow<uint8_t[]>(kBookArenaSize);
|
||||
auto scratchBuf = makeUniqueNoThrow<uint8_t[]>(kScratchSize);
|
||||
if (!bookBuf || !scratchBuf) {
|
||||
LOG_ERR("COVER", "OOM: book open arenas");
|
||||
return false;
|
||||
}
|
||||
Arena bookArena(bookBuf.get(), kBookArenaSize);
|
||||
Arena scratch(scratchBuf.get(), kScratchSize);
|
||||
|
||||
SdBookSource source;
|
||||
if (!source.open(epubPath.c_str())) {
|
||||
LOG_ERR("COVER", "Cannot open: %s", epubPath.c_str());
|
||||
return false;
|
||||
}
|
||||
Book book;
|
||||
const BookStatus st = book.open(source, bookArena, scratch);
|
||||
if (st != BookStatus::Ok) {
|
||||
LOG_ERR("COVER", "Book open failed: %d (%s)", static_cast<int>(st), epubPath.c_str());
|
||||
return false;
|
||||
}
|
||||
return fn(book, source, scratch);
|
||||
}
|
||||
|
||||
const ManifestItem* findCoverItem(const Book& book) {
|
||||
for (size_t m = 0; m < book.manifestCount(); ++m) {
|
||||
const ManifestItem* item = book.manifestItem(m);
|
||||
if (item != nullptr && item->isCoverImage) return item;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool isJpegItem(const ManifestItem& item) {
|
||||
if (item.mediaType != nullptr && strcmp(item.mediaType, "image/jpeg") == 0) return true;
|
||||
return FsHelpers::hasJpgExtension(std::string_view(item.href));
|
||||
}
|
||||
|
||||
bool isPngItem(const ManifestItem& item) {
|
||||
if (item.mediaType != nullptr && strcmp(item.mediaType, "image/png") == 0) return true;
|
||||
return FsHelpers::hasPngExtension(std::string_view(item.href));
|
||||
}
|
||||
|
||||
// Streams the cover entry out of the ZIP into `tempPath`.
|
||||
bool extractItem(const Book& book, freeink::book::BookSource& source, Arena& scratch, const ManifestItem& item,
|
||||
const std::string& tempPath) {
|
||||
const freeink::book::ZipEntry* entry = book.zip().find(item.href);
|
||||
if (entry == nullptr) {
|
||||
LOG_ERR("COVER", "Cover entry missing: %s", item.href);
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t marked = scratch.mark();
|
||||
ZipEntryReader reader;
|
||||
uint8_t* buf = static_cast<uint8_t*>(scratch.alloc(4096, 1));
|
||||
if (buf == nullptr || reader.open(source, *entry, scratch) != BookStatus::Ok) {
|
||||
scratch.release(marked);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = true;
|
||||
{
|
||||
HalFile out;
|
||||
if (!Storage.openFileForWrite("COVER", tempPath, out)) {
|
||||
scratch.release(marked);
|
||||
return false;
|
||||
}
|
||||
for (;;) {
|
||||
const int32_t n = reader.read(buf, 4096);
|
||||
if (n < 0) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
if (n == 0) break;
|
||||
if (out.write(buf, n) != static_cast<size_t>(n)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// `out` closes at scope exit, before the converter reopens the path.
|
||||
}
|
||||
scratch.release(marked);
|
||||
if (!ok) Storage.remove(tempPath.c_str());
|
||||
return ok;
|
||||
}
|
||||
|
||||
// Shared shape of both generators: extract the cover beside the output, run
|
||||
// `convert(coverFile, bmpOut)`, clean up the temp, drop the output on failure.
|
||||
template <typename ConvertFn>
|
||||
bool generateFromCover(const std::string& epubPath, const std::string& outPath, ConvertFn&& convert,
|
||||
bool* hadCoverOut) {
|
||||
if (hadCoverOut != nullptr) *hadCoverOut = false;
|
||||
const std::string cacheDir = cacheDirForBook(epubPath);
|
||||
Storage.ensureDirectoryExists(cacheDir.c_str());
|
||||
|
||||
bool converted = false;
|
||||
bool hadCover = false;
|
||||
const bool opened = withOpenBook(epubPath, [&](Book& book, freeink::book::BookSource& source, Arena& scratch) {
|
||||
const ManifestItem* cover = findCoverItem(book);
|
||||
if (cover == nullptr) {
|
||||
LOG_DBG("COVER", "No cover image in manifest: %s", epubPath.c_str());
|
||||
return true; // opened fine, just coverless
|
||||
}
|
||||
const bool jpeg = isJpegItem(*cover);
|
||||
const bool png = !jpeg && isPngItem(*cover);
|
||||
if (!jpeg && !png) {
|
||||
LOG_ERR("COVER", "Unsupported cover format: %s", cover->href);
|
||||
return true;
|
||||
}
|
||||
hadCover = true;
|
||||
|
||||
const std::string tempPath = cacheDir + (jpeg ? "/.cover.jpg" : "/.cover.png");
|
||||
if (!extractItem(book, source, scratch, *cover, tempPath)) return true;
|
||||
|
||||
{
|
||||
HalFile coverFile;
|
||||
HalFile bmpOut;
|
||||
if (Storage.openFileForRead("COVER", tempPath, coverFile) && Storage.openFileForWrite("COVER", outPath, bmpOut)) {
|
||||
converted = convert(jpeg, coverFile, bmpOut);
|
||||
}
|
||||
// Both close at scope exit, before the temp file is removed below.
|
||||
}
|
||||
Storage.remove(tempPath.c_str());
|
||||
return true;
|
||||
});
|
||||
|
||||
if (hadCoverOut != nullptr) *hadCoverOut = hadCover;
|
||||
if (!converted) Storage.remove(outPath.c_str());
|
||||
return opened && converted;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace BookCoverUtils {
|
||||
|
||||
std::string coverBmpPath(const std::string& epubPath, const bool cropped) {
|
||||
return cacheDirForBook(epubPath) + (cropped ? "/cover_crop.bmp" : "/cover.bmp");
|
||||
}
|
||||
|
||||
std::string thumbBmpPath(const std::string& epubPath, const int height) {
|
||||
return cacheDirForBook(epubPath) + "/thumb_" + std::to_string(height) + ".bmp";
|
||||
}
|
||||
|
||||
std::string thumbBmpPathTemplate(const std::string& epubPath) {
|
||||
return cacheDirForBook(epubPath) + "/thumb_[HEIGHT].bmp";
|
||||
}
|
||||
|
||||
bool generateCoverBmp(const std::string& epubPath, const bool cropped) {
|
||||
const std::string outPath = coverBmpPath(epubPath, cropped);
|
||||
if (Storage.exists(outPath.c_str())) return true;
|
||||
return generateFromCover(
|
||||
epubPath, outPath,
|
||||
[cropped](const bool jpeg, HalFile& coverFile, HalFile& bmpOut) {
|
||||
return jpeg ? JpegToBmpConverter::jpegFileToBmpStream(coverFile, bmpOut, cropped)
|
||||
: PngToBmpConverter::pngFileToBmpStream(coverFile, bmpOut, cropped);
|
||||
},
|
||||
nullptr);
|
||||
}
|
||||
|
||||
bool generateThumbBmp(const std::string& epubPath, const int height) {
|
||||
const std::string outPath = thumbBmpPath(epubPath, height);
|
||||
if (Storage.exists(outPath.c_str())) return true;
|
||||
|
||||
const int targetWidth = height * 6 / 10; // Continue Reading card aspect (legacy)
|
||||
bool hadCover = false;
|
||||
const bool ok = generateFromCover(
|
||||
epubPath, outPath,
|
||||
[targetWidth, height](const bool jpeg, HalFile& coverFile, HalFile& bmpOut) {
|
||||
return jpeg ? JpegToBmpConverter::jpegFileTo1BitBmpStreamWithSize(coverFile, bmpOut, targetWidth, height)
|
||||
: PngToBmpConverter::pngFileTo1BitBmpStreamWithSize(coverFile, bmpOut, targetWidth, height);
|
||||
},
|
||||
&hadCover);
|
||||
if (ok) return true;
|
||||
|
||||
// Legacy behavior: an empty sentinel BMP stops re-generation attempts on
|
||||
// every home screen visit for coverless/unsupported books.
|
||||
HalFile sentinel;
|
||||
Storage.openFileForWrite("COVER", outPath, sentinel);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool readMetadata(const std::string& epubPath, std::string* titleOut, std::string* authorOut) {
|
||||
return withOpenBook(epubPath, [&](Book& book, freeink::book::BookSource&, Arena&) {
|
||||
if (titleOut != nullptr) *titleOut = book.metadata().title;
|
||||
if (authorOut != nullptr) *authorOut = book.metadata().author;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace BookCoverUtils
|
||||
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
|
||||
// EPUB cover artwork for UI chrome (home screen thumbnails, sleep screen),
|
||||
// on the FreeInkBook container: the cover manifest item (EPUB 3
|
||||
// properties="cover-image" or EPUB 2 <meta name="cover">) is streamed out of
|
||||
// the ZIP into a temp file and fed to the existing PNG/JPEG-to-BMP
|
||||
// converters, so the generated files are byte-compatible with what the
|
||||
// legacy Epub pipeline produced (same cache paths, same formats).
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace BookCoverUtils {
|
||||
|
||||
// "<cacheDir>/cover.bmp" or "<cacheDir>/cover_crop.bmp" (legacy naming).
|
||||
std::string coverBmpPath(const std::string& epubPath, bool cropped);
|
||||
// "<cacheDir>/thumb_<height>.bmp" (legacy naming).
|
||||
std::string thumbBmpPath(const std::string& epubPath, int height);
|
||||
// The [HEIGHT]-templated form RecentBooksStore carries.
|
||||
std::string thumbBmpPathTemplate(const std::string& epubPath);
|
||||
|
||||
// Screen-sized cover for the sleep screen. No-op when the file exists.
|
||||
bool generateCoverBmp(const std::string& epubPath, bool cropped);
|
||||
|
||||
// 1-bit thumbnail for the home screen (fast BW blit). No-op when the file
|
||||
// exists. On failure or missing cover an empty sentinel file is written so
|
||||
// the generation is not retried every visit (legacy behavior).
|
||||
bool generateThumbBmp(const std::string& epubPath, int height);
|
||||
|
||||
// Book title/author straight from the OPF (for recents entries).
|
||||
bool readMetadata(const std::string& epubPath, std::string* titleOut, std::string* authorOut);
|
||||
|
||||
} // namespace BookCoverUtils
|
||||
@@ -41,7 +41,6 @@ include(GoogleTest)
|
||||
add_subdirectory(streaming_json_parser)
|
||||
add_subdirectory(release_json_parser)
|
||||
add_subdirectory(differential_rounding)
|
||||
add_subdirectory(hyphenation_eval)
|
||||
add_subdirectory(utf8_compose)
|
||||
add_subdirectory(cpfont_adapter)
|
||||
add_subdirectory(book_xpath)
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
add_executable(HyphenationEvaluationTest
|
||||
HyphenationEvaluationTest.cpp
|
||||
${REPO_ROOT}/lib/Epub/Epub/hyphenation/Hyphenator.cpp
|
||||
${REPO_ROOT}/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp
|
||||
${REPO_ROOT}/lib/Epub/Epub/hyphenation/LiangHyphenation.cpp
|
||||
${REPO_ROOT}/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp
|
||||
${REPO_ROOT}/lib/Utf8/Utf8.cpp
|
||||
)
|
||||
|
||||
target_include_directories(HyphenationEvaluationTest PRIVATE
|
||||
${REPO_ROOT}/lib/Epub
|
||||
${REPO_ROOT}/lib/Utf8
|
||||
)
|
||||
|
||||
target_compile_definitions(HyphenationEvaluationTest PRIVATE
|
||||
HYPHENATION_RESOURCES_DIR="${CMAKE_CURRENT_SOURCE_DIR}/resources"
|
||||
)
|
||||
|
||||
target_link_libraries(HyphenationEvaluationTest PRIVATE
|
||||
crosspoint_test_common
|
||||
GTest::gtest_main
|
||||
)
|
||||
|
||||
gtest_discover_tests(HyphenationEvaluationTest)
|
||||
@@ -1,234 +0,0 @@
|
||||
#include <Utf8.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "lib/Epub/Epub/hyphenation/HyphenationCommon.h"
|
||||
#include "lib/Epub/Epub/hyphenation/LanguageHyphenator.h"
|
||||
#include "lib/Epub/Epub/hyphenation/LanguageRegistry.h"
|
||||
|
||||
#ifndef HYPHENATION_RESOURCES_DIR
|
||||
#error "HYPHENATION_RESOURCES_DIR must be defined by the build system"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
struct TestCase {
|
||||
std::string word;
|
||||
std::string hyphenated;
|
||||
std::vector<size_t> expectedPositions;
|
||||
int frequency;
|
||||
};
|
||||
|
||||
struct EvaluationResult {
|
||||
int truePositives = 0;
|
||||
int falsePositives = 0;
|
||||
int falseNegatives = 0;
|
||||
double precision = 0.0;
|
||||
double recall = 0.0;
|
||||
double f1Score = 0.0;
|
||||
double weightedScore = 0.0;
|
||||
};
|
||||
|
||||
std::vector<size_t> expectedPositionsFromAnnotatedWord(const std::string& annotated) {
|
||||
std::vector<size_t> positions;
|
||||
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(annotated.c_str());
|
||||
size_t codepointIndex = 0;
|
||||
|
||||
while (*ptr != 0) {
|
||||
if (*ptr == '=') {
|
||||
positions.push_back(codepointIndex);
|
||||
++ptr;
|
||||
continue;
|
||||
}
|
||||
|
||||
utf8NextCodepoint(&ptr);
|
||||
++codepointIndex;
|
||||
}
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
std::vector<TestCase> loadTestData(const std::string& filename) {
|
||||
std::vector<TestCase> testCases;
|
||||
std::ifstream file(filename);
|
||||
|
||||
if (!file.is_open()) {
|
||||
return testCases;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(file, line)) {
|
||||
if (line.empty() || line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::istringstream iss(line);
|
||||
std::string word, hyphenated, freqStr;
|
||||
|
||||
if (std::getline(iss, word, '|') && std::getline(iss, hyphenated, '|') && std::getline(iss, freqStr, '|')) {
|
||||
TestCase testCase;
|
||||
testCase.word = word;
|
||||
testCase.hyphenated = hyphenated;
|
||||
testCase.frequency = std::stoi(freqStr);
|
||||
testCase.expectedPositions = expectedPositionsFromAnnotatedWord(hyphenated);
|
||||
testCases.push_back(testCase);
|
||||
}
|
||||
}
|
||||
|
||||
return testCases;
|
||||
}
|
||||
|
||||
std::string positionsToHyphenated(const std::string& word, const std::vector<size_t>& positions) {
|
||||
std::string result;
|
||||
std::vector<size_t> sortedPositions = positions;
|
||||
std::sort(sortedPositions.begin(), sortedPositions.end());
|
||||
|
||||
const unsigned char* ptr = reinterpret_cast<const unsigned char*>(word.c_str());
|
||||
size_t codepointIndex = 0;
|
||||
size_t posIdx = 0;
|
||||
|
||||
while (*ptr != 0) {
|
||||
while (posIdx < sortedPositions.size() && sortedPositions[posIdx] == codepointIndex) {
|
||||
result.push_back('=');
|
||||
++posIdx;
|
||||
}
|
||||
|
||||
const unsigned char* current = ptr;
|
||||
utf8NextCodepoint(&ptr);
|
||||
result.append(reinterpret_cast<const char*>(current), reinterpret_cast<const char*>(ptr));
|
||||
++codepointIndex;
|
||||
}
|
||||
|
||||
while (posIdx < sortedPositions.size() && sortedPositions[posIdx] == codepointIndex) {
|
||||
result.push_back('=');
|
||||
++posIdx;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<size_t> hyphenateWordWithHyphenator(const std::string& word, const LanguageHyphenator& hyphenator) {
|
||||
auto cps = collectCodepoints(word);
|
||||
trimSurroundingPunctuationAndFootnote(cps);
|
||||
return hyphenator.breakIndexes(cps);
|
||||
}
|
||||
|
||||
EvaluationResult evaluateWord(const TestCase& testCase, const std::vector<size_t>& actualPositions) {
|
||||
EvaluationResult result;
|
||||
|
||||
std::vector<size_t> expected = testCase.expectedPositions;
|
||||
std::vector<size_t> actual = actualPositions;
|
||||
|
||||
std::sort(expected.begin(), expected.end());
|
||||
std::sort(actual.begin(), actual.end());
|
||||
|
||||
for (size_t pos : actual) {
|
||||
if (std::find(expected.begin(), expected.end(), pos) != expected.end()) {
|
||||
result.truePositives++;
|
||||
} else {
|
||||
result.falsePositives++;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t pos : expected) {
|
||||
if (std::find(actual.begin(), actual.end(), pos) == actual.end()) {
|
||||
result.falseNegatives++;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.truePositives + result.falsePositives > 0) {
|
||||
result.precision = static_cast<double>(result.truePositives) / (result.truePositives + result.falsePositives);
|
||||
}
|
||||
|
||||
if (result.truePositives + result.falseNegatives > 0) {
|
||||
result.recall = static_cast<double>(result.truePositives) / (result.truePositives + result.falseNegatives);
|
||||
}
|
||||
|
||||
if (result.precision + result.recall > 0) {
|
||||
result.f1Score = 2 * result.precision * result.recall / (result.precision + result.recall);
|
||||
}
|
||||
|
||||
// Treat words with no expected and no actual hyphenation marks as perfect.
|
||||
if (expected.empty() && actual.empty()) {
|
||||
result.precision = 1.0;
|
||||
result.recall = 1.0;
|
||||
result.f1Score = 1.0;
|
||||
}
|
||||
|
||||
double fpPenalty = 2.0;
|
||||
double fnPenalty = 1.0;
|
||||
int totalErrors = result.falsePositives * fpPenalty + result.falseNegatives * fnPenalty;
|
||||
int totalPossible = static_cast<int>(expected.size() * fpPenalty);
|
||||
|
||||
if (totalPossible > 0) {
|
||||
result.weightedScore = 1.0 - (static_cast<double>(totalErrors) / totalPossible);
|
||||
result.weightedScore = std::max(0.0, result.weightedScore);
|
||||
} else if (result.falsePositives == 0) {
|
||||
result.weightedScore = 1.0;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Runs the evaluation for a single language and asserts the per-word average F1
|
||||
// is at or above `minF1Percent`. Thresholds are set ~1pp below measured
|
||||
// baselines so unrelated tweaks don't fail CI but real regressions still trip.
|
||||
void runLanguageEval(const char* langName, const char* primaryTag, const char* resourceFile, double minF1Percent) {
|
||||
const auto* hyphenator = getLanguageHyphenatorForPrimaryTag(primaryTag);
|
||||
ASSERT_NE(hyphenator, nullptr) << "No hyphenator registered for tag: " << primaryTag;
|
||||
|
||||
std::string path = std::string(HYPHENATION_RESOURCES_DIR) + "/" + resourceFile;
|
||||
std::vector<TestCase> testCases = loadTestData(path);
|
||||
ASSERT_FALSE(testCases.empty()) << "No test cases loaded from " << path;
|
||||
|
||||
double totalF1 = 0.0;
|
||||
std::vector<std::pair<TestCase, EvaluationResult>> imperfect;
|
||||
|
||||
for (const auto& tc : testCases) {
|
||||
std::vector<size_t> actual = hyphenateWordWithHyphenator(tc.word, *hyphenator);
|
||||
EvaluationResult res = evaluateWord(tc, actual);
|
||||
totalF1 += res.f1Score;
|
||||
if (res.weightedScore < 0.999999) {
|
||||
imperfect.emplace_back(tc, res);
|
||||
}
|
||||
}
|
||||
|
||||
double averageF1Percent = totalF1 / testCases.size() * 100.0;
|
||||
::testing::Test::RecordProperty("avg_f1_percent", std::to_string(averageF1Percent));
|
||||
::testing::Test::RecordProperty("test_cases", std::to_string(testCases.size()));
|
||||
|
||||
std::cout << langName << ": F1=" << averageF1Percent << "% (threshold " << minF1Percent << "%, " << testCases.size()
|
||||
<< " cases)\n";
|
||||
|
||||
if (averageF1Percent < minF1Percent) {
|
||||
std::sort(imperfect.begin(), imperfect.end(),
|
||||
[](const auto& a, const auto& b) { return a.second.weightedScore < b.second.weightedScore; });
|
||||
std::cout << "Worst cases for " << langName << ":\n";
|
||||
int show = std::min<int>(10, static_cast<int>(imperfect.size()));
|
||||
for (int i = 0; i < show; ++i) {
|
||||
const TestCase& tc = imperfect[i].first;
|
||||
std::vector<size_t> actual = hyphenateWordWithHyphenator(tc.word, *hyphenator);
|
||||
std::cout << " " << tc.word << " | expected=" << tc.hyphenated
|
||||
<< " | got=" << positionsToHyphenated(tc.word, actual) << "\n";
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_GE(averageF1Percent, minF1Percent) << "Hyphenation quality regressed for " << langName;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(HyphenationEval, English) { runLanguageEval("english", "en", "english_hyphenation_tests.txt", 98.10); }
|
||||
TEST(HyphenationEval, French) { runLanguageEval("french", "fr", "french_hyphenation_tests.txt", 99.00); }
|
||||
TEST(HyphenationEval, German) { runLanguageEval("german", "de", "german_hyphenation_tests.txt", 96.73); }
|
||||
TEST(HyphenationEval, Russian) { runLanguageEval("russian", "ru", "russian_hyphenation_tests.txt", 96.22); }
|
||||
TEST(HyphenationEval, Spanish) { runLanguageEval("spanish", "es", "spanish_hyphenation_tests.txt", 98.02); }
|
||||
TEST(HyphenationEval, Italian) { runLanguageEval("italian", "it", "italian_hyphenation_tests.txt", 98.99); }
|
||||
TEST(HyphenationEval, Polish) { runLanguageEval("polish", "pl", "polish_hyphenation_tests.txt", 98.92); }
|
||||
TEST(HyphenationEval, Swedish) { runLanguageEval("swedish", "sv", "swedish_hyphenation_tests.txt", 94.01); }
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,233 +0,0 @@
|
||||
"""
|
||||
Generate hyphenation test data from a text file.
|
||||
|
||||
This script extracts unique words from a book and generates ground truth
|
||||
hyphenations using the pyphen library, which can be used to test and validate
|
||||
the hyphenation implementations (e.g., German, English, Russian).
|
||||
|
||||
Usage:
|
||||
python generate_hyphenation_test_data.py <input_file> <output_file>
|
||||
[--language de_DE] [--max-words 5000] [--min-prefix 2] [--min-suffix 2]
|
||||
|
||||
Requirements:
|
||||
pip install pyphen
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
|
||||
def extract_text_from_epub(epub_path):
|
||||
"""Extract textual content from an .epub archive by concatenating HTML/XHTML files."""
|
||||
texts = []
|
||||
with zipfile.ZipFile(epub_path, "r") as z:
|
||||
for name in z.namelist():
|
||||
lower = name.lower()
|
||||
if (
|
||||
lower.endswith(".xhtml")
|
||||
or lower.endswith(".html")
|
||||
or lower.endswith(".htm")
|
||||
):
|
||||
try:
|
||||
data = z.read(name).decode("utf-8", errors="ignore")
|
||||
except Exception:
|
||||
continue
|
||||
# Remove tags
|
||||
text = re.sub(r"<[^>]+>", " ", data)
|
||||
texts.append(text)
|
||||
return "\n".join(texts)
|
||||
|
||||
|
||||
def extract_words(text):
|
||||
"""Extract all words from text, preserving original case."""
|
||||
# Match runs of Unicode letters (any script) while excluding digits/underscores
|
||||
return re.findall(r"[^\W\d_]+", text, flags=re.UNICODE)
|
||||
|
||||
|
||||
def clean_word(word):
|
||||
"""Normalize word for hyphenation testing."""
|
||||
# Keep original case but strip any non-letter characters
|
||||
return word.strip()
|
||||
|
||||
|
||||
def generate_hyphenation_data(
|
||||
input_file,
|
||||
output_file,
|
||||
language="de_DE",
|
||||
min_length=6,
|
||||
max_words=5000,
|
||||
min_prefix=2,
|
||||
min_suffix=2,
|
||||
):
|
||||
"""
|
||||
Generate hyphenation test data from a text file.
|
||||
|
||||
Args:
|
||||
input_file: Path to input text file
|
||||
output_file: Path to output file with hyphenation data
|
||||
language: Language code for pyphen (e.g., 'de_DE', 'en_US')
|
||||
min_length: Minimum word length to include
|
||||
max_words: Maximum number of words to include (default: 5000)
|
||||
min_prefix: Minimum characters allowed before the first hyphen (default: 2)
|
||||
min_suffix: Minimum characters allowed after the last hyphen (default: 2)
|
||||
"""
|
||||
import pyphen
|
||||
|
||||
print(f"Reading from: {input_file}")
|
||||
|
||||
# Read the input file
|
||||
if str(input_file).lower().endswith(".epub"):
|
||||
print("Detected .epub input; extracting HTML content")
|
||||
text = extract_text_from_epub(input_file)
|
||||
else:
|
||||
with open(input_file, "r", encoding="utf-8") as f:
|
||||
text = f.read()
|
||||
|
||||
# Extract words
|
||||
print("Extracting words...")
|
||||
words = extract_words(text)
|
||||
print(f"Found {len(words)} total words")
|
||||
|
||||
# Count word frequencies
|
||||
word_counts = Counter(words)
|
||||
print(f"Found {len(word_counts)} unique words")
|
||||
|
||||
# Initialize pyphen hyphenator
|
||||
print(
|
||||
f"Initializing hyphenator for language: {language} (min_prefix={min_prefix}, min_suffix={min_suffix})"
|
||||
)
|
||||
try:
|
||||
hyphenator = pyphen.Pyphen(lang=language, left=min_prefix, right=min_suffix)
|
||||
except KeyError:
|
||||
print(f"Error: Language '{language}' not found in pyphen.")
|
||||
print("Available languages include: de_DE, en_US, en_GB, fr_FR, etc.")
|
||||
return
|
||||
|
||||
# Generate hyphenations
|
||||
print("Generating hyphenations...")
|
||||
hyphenation_data = []
|
||||
|
||||
# Sort by frequency (most common first) then alphabetically
|
||||
sorted_words = sorted(word_counts.items(), key=lambda x: (-x[1], x[0].lower()))
|
||||
|
||||
for word, count in sorted_words:
|
||||
# Filter by minimum length
|
||||
if len(word) < min_length:
|
||||
continue
|
||||
|
||||
# Get hyphenation (may produce no '=' characters)
|
||||
hyphenated = hyphenator.inserted(word, hyphen="=")
|
||||
|
||||
# Include all words (so we can take the top N most common words even if
|
||||
# they don't have hyphenation points). This replaces the previous filter
|
||||
# which dropped words without '='.
|
||||
hyphenation_data.append(
|
||||
{"word": word, "hyphenated": hyphenated, "count": count}
|
||||
)
|
||||
|
||||
# Stop if we've reached max_words
|
||||
if max_words and len(hyphenation_data) >= max_words:
|
||||
break
|
||||
|
||||
print(f"Generated {len(hyphenation_data)} hyphenated words")
|
||||
|
||||
# Write output file
|
||||
print(f"Writing to: {output_file}")
|
||||
with open(output_file, "w", encoding="utf-8") as f:
|
||||
# Write header with metadata
|
||||
f.write(f"# Hyphenation Test Data\n")
|
||||
f.write(f"# Source: {Path(input_file).name}\n")
|
||||
f.write(f"# Language: {language}\n")
|
||||
f.write(f"# Min prefix: {min_prefix}\n")
|
||||
f.write(f"# Min suffix: {min_suffix}\n")
|
||||
f.write(f"# Total words: {len(hyphenation_data)}\n")
|
||||
f.write(f"# Format: word | hyphenated_form | frequency_in_source\n")
|
||||
f.write(f"#\n")
|
||||
f.write(f"# Hyphenation points are marked with '='\n")
|
||||
f.write(f"# Example: Silbentrennung -> Sil=ben=tren=nung\n")
|
||||
f.write(f"#\n\n")
|
||||
|
||||
# Write data
|
||||
for item in hyphenation_data:
|
||||
f.write(f"{item['word']}|{item['hyphenated']}|{item['count']}\n")
|
||||
|
||||
print("Done!")
|
||||
|
||||
# Print some statistics
|
||||
print("\n=== Statistics ===")
|
||||
print(f"Total unique words extracted: {len(word_counts)}")
|
||||
print(f"Words with hyphenation points: {len(hyphenation_data)}")
|
||||
print(
|
||||
f"Average hyphenation points per word: {sum(h['hyphenated'].count('=') for h in hyphenation_data) / len(hyphenation_data):.2f}"
|
||||
)
|
||||
|
||||
# Print some examples
|
||||
print("\n=== Examples (first 10) ===")
|
||||
for item in hyphenation_data[:10]:
|
||||
print(
|
||||
f" {item['word']:20} -> {item['hyphenated']:30} (appears {item['count']}x)"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate hyphenation test data from a text file",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Generate test data from a German book
|
||||
python generate_hyphenation_test_data.py ../data/books/bobiverse_1.txt hyphenation_test_data.txt
|
||||
|
||||
# Limit to 500 most common words
|
||||
python generate_hyphenation_test_data.py ../data/books/bobiverse_1.txt hyphenation_test_data.txt --max-words 500
|
||||
|
||||
# Use English hyphenation (when available)
|
||||
python generate_hyphenation_test_data.py book.txt test_en.txt --language en_US
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument("input_file", help="Input text file to extract words from")
|
||||
parser.add_argument("output_file", help="Output file for hyphenation test data")
|
||||
parser.add_argument(
|
||||
"--language", default="de_DE", help="Language code (default: de_DE)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min-length", type=int, default=6, help="Minimum word length (default: 6)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-words",
|
||||
type=int,
|
||||
default=5000,
|
||||
help="Maximum number of words to include (default: 5000)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min-prefix",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Minimum characters permitted before the first hyphen (default: 2)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--min-suffix",
|
||||
type=int,
|
||||
default=2,
|
||||
help="Minimum characters permitted after the last hyphen (default: 2)",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
generate_hyphenation_data(
|
||||
args.input_file,
|
||||
args.output_file,
|
||||
language=args.language,
|
||||
min_length=args.min_length,
|
||||
max_words=args.max_words,
|
||||
min_prefix=args.min_prefix,
|
||||
min_suffix=args.min_suffix,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
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
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user