From e3d6e3260946917be860b42332f6babe2d38cc15 Mon Sep 17 00:00:00 2001 From: Juan Biondi Date: Wed, 21 Jan 2026 14:29:39 +0100 Subject: [PATCH 01/36] docs: Add detailed webserver documentation (#446) ## More detailed documentation * **What is the goal of this PR?** Add more information about the exposed webserver. * **What changes are included?** Detailed documentation for the webserver endpoints (`./docs/webserver-endpoints.md`) Adding a table of content so it is easier to navigate directly to the section you're interested on (Almost all `.md` files or at least all those relevant) ## Additional Context Not sure if this would get accepted but I thought it might be useful for those trying to create separate apps that would sync files to the device. It was at least to me trying to upload files using python as stated [here](https://github.com/crosspoint-reader/crosspoint-reader/discussions/434#discussioncomment-15545349) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_ --- USER_GUIDE.md | 21 +++ docs/troubleshooting.md | 57 +++++++ docs/webserver-endpoints.md | 331 ++++++++++++++++++++++++++++++++++++ docs/webserver.md | 85 +-------- 4 files changed, 411 insertions(+), 83 deletions(-) create mode 100644 docs/troubleshooting.md create mode 100644 docs/webserver-endpoints.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 0c852691..d670abb7 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -2,6 +2,27 @@ Welcome to the **CrossPoint** firmware. This guide outlines the hardware controls, navigation, and reading features of the device. +- [CrossPoint User Guide](#crosspoint-user-guide) + - [1. Hardware Overview](#1-hardware-overview) + - [Button Layout](#button-layout) + - [2. Power \& Startup](#2-power--startup) + - [Power On / Off](#power-on--off) + - [First Launch](#first-launch) + - [3. Screens](#3-screens) + - [3.1 Home Screen](#31-home-screen) + - [3.2 Book Selection](#32-book-selection) + - [3.3 Reading Mode](#33-reading-mode) + - [3.4 File Upload Screen](#34-file-upload-screen) + - [3.5 Settings](#35-settings) + - [3.6 Sleep Screen](#36-sleep-screen) + - [4. Reading Mode](#4-reading-mode) + - [Page Turning](#page-turning) + - [Chapter Navigation](#chapter-navigation) + - [System Navigation](#system-navigation) + - [5. Chapter Selection Screen](#5-chapter-selection-screen) + - [6. Current Limitations \& Roadmap](#6-current-limitations--roadmap) + + ## 1. Hardware Overview The device utilises the standard buttons on the Xtink X4 (in the same layout as the manufacturer firmware, by default): diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..7c50a307 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,57 @@ +# Troubleshooting + +This document show most common issues and possible solutions while using the device features. + +- [Troubleshooting](#troubleshooting) + - [Cannot See the Device on the Network](#cannot-see-the-device-on-the-network) + - [Connection Drops or Times Out](#connection-drops-or-times-out) + - [Upload Fails](#upload-fails) + - [Saved Password Not Working](#saved-password-not-working) + +### Cannot See the Device on the Network + +**Problem:** Browser shows "Cannot connect" or "Site can't be reached" + +**Solutions:** + +1. Verify both devices are on the **same WiFi network** + - Check your computer/phone WiFi settings + - Confirm the CrossPoint Reader shows "Connected" status +2. Double-check the IP address + - Make sure you typed it correctly + - Include `http://` at the beginning +3. Try disabling VPN if you're using one +4. Some networks have "client isolation" enabled - check with your network administrator + +### Connection Drops or Times Out + +**Problem:** WiFi connection is unstable + +**Solutions:** + +1. Move closer to the WiFi router +2. Check signal strength on the device (should be at least `||` or better) +3. Avoid interference from other devices +4. Try a different WiFi network if available + +### Upload Fails + +**Problem:** File upload doesn't complete or shows an error + +**Solutions:** + +1. Ensure the file is a valid `.epub` file +2. Check that the SD card has enough free space +3. Try uploading a smaller file first to test +4. Refresh the browser page and try again + +### Saved Password Not Working + +**Problem:** Device fails to connect with saved credentials + +**Solutions:** + +1. When connection fails, you'll be prompted to "Forget Network" +2. Select **Yes** to remove the saved password +3. Reconnect and enter the password again +4. Choose to save the new password diff --git a/docs/webserver-endpoints.md b/docs/webserver-endpoints.md new file mode 100644 index 00000000..0abe9df4 --- /dev/null +++ b/docs/webserver-endpoints.md @@ -0,0 +1,331 @@ +# Webserver Endpoints + +This document describes all HTTP and WebSocket endpoints available on the CrossPoint Reader webserver. + +- [Webserver Endpoints](#webserver-endpoints) + - [Overview](#overview) + - [HTTP Endpoints](#http-endpoints) + - [GET `/` - Home Page](#get----home-page) + - [GET `/files` - File Browser Page](#get-files---file-browser-page) + - [GET `/api/status` - Device Status](#get-apistatus---device-status) + - [GET `/api/files` - List Files](#get-apifiles---list-files) + - [POST `/upload` - Upload File](#post-upload---upload-file) + - [POST `/mkdir` - Create Folder](#post-mkdir---create-folder) + - [POST `/delete` - Delete File or Folder](#post-delete---delete-file-or-folder) + - [WebSocket Endpoint](#websocket-endpoint) + - [Port 81 - Fast Binary Upload](#port-81---fast-binary-upload) + - [Network Modes](#network-modes) + - [Station Mode (STA)](#station-mode-sta) + - [Access Point Mode (AP)](#access-point-mode-ap) + - [Notes](#notes) + + +## Overview + +The CrossPoint Reader exposes a webserver for file management and device monitoring: + +- **HTTP Server**: Port 80 +- **WebSocket Server**: Port 81 (for fast binary uploads) + +--- + +## HTTP Endpoints + +### GET `/` - Home Page + +Serves the home page HTML interface. + +**Request:** +```bash +curl http://crosspoint.local/ +``` + +**Response:** HTML page (200 OK) + +--- + +### GET `/files` - File Browser Page + +Serves the file browser HTML interface. + +**Request:** +```bash +curl http://crosspoint.local/files +``` + +**Response:** HTML page (200 OK) + +--- + +### GET `/api/status` - Device Status + +Returns JSON with device status information. + +**Request:** +```bash +curl http://crosspoint.local/api/status +``` + +**Response (200 OK):** +```json +{ + "version": "1.0.0", + "ip": "192.168.1.100", + "mode": "STA", + "rssi": -45, + "freeHeap": 123456, + "uptime": 3600 +} +``` + +| Field | Type | Description | +| ---------- | ------ | --------------------------------------------------------- | +| `version` | string | CrossPoint firmware version | +| `ip` | string | Device IP address | +| `mode` | string | `"STA"` (connected to WiFi) or `"AP"` (access point mode) | +| `rssi` | number | WiFi signal strength in dBm (0 in AP mode) | +| `freeHeap` | number | Free heap memory in bytes | +| `uptime` | number | Seconds since device boot | + +--- + +### GET `/api/files` - List Files + +Returns a JSON array of files and folders in the specified directory. + +**Request:** +```bash +# List root directory +curl http://crosspoint.local/api/files + +# List specific directory +curl "http://crosspoint.local/api/files?path=/Books" +``` + +**Query Parameters:** + +| Parameter | Required | Default | Description | +| --------- | -------- | ------- | ---------------------- | +| `path` | No | `/` | Directory path to list | + +**Response (200 OK):** +```json +[ + {"name": "MyBook.epub", "size": 1234567, "isDirectory": false, "isEpub": true}, + {"name": "Notes", "size": 0, "isDirectory": true, "isEpub": false}, + {"name": "document.pdf", "size": 54321, "isDirectory": false, "isEpub": false} +] +``` + +| Field | Type | Description | +| ------------- | ------- | ---------------------------------------- | +| `name` | string | File or folder name | +| `size` | number | Size in bytes (0 for directories) | +| `isDirectory` | boolean | `true` if the item is a folder | +| `isEpub` | boolean | `true` if the file has `.epub` extension | + +**Notes:** +- Hidden files (starting with `.`) are automatically filtered out +- System folders (`System Volume Information`, `XTCache`) are hidden + +--- + +### POST `/upload` - Upload File + +Uploads a file to the SD card via multipart form data. + +**Request:** +```bash +# Upload to root directory +curl -X POST -F "file=@mybook.epub" http://crosspoint.local/upload + +# Upload to specific directory +curl -X POST -F "file=@mybook.epub" "http://crosspoint.local/upload?path=/Books" +``` + +**Query Parameters:** + +| Parameter | Required | Default | Description | +| --------- | -------- | ------- | ------------------------------- | +| `path` | No | `/` | Target directory for the upload | + +**Response (200 OK):** +``` +File uploaded successfully: mybook.epub +``` + +**Error Responses:** + +| Status | Body | Cause | +| ------ | ----------------------------------------------- | --------------------------- | +| 400 | `Failed to create file on SD card` | Cannot create file | +| 400 | `Failed to write to SD card - disk may be full` | Write error during upload | +| 400 | `Failed to write final data to SD card` | Error flushing final buffer | +| 400 | `Upload aborted` | Client aborted the upload | +| 400 | `Unknown error during upload` | Unspecified error | + +**Notes:** +- Existing files with the same name will be overwritten +- Uses a 4KB buffer for efficient SD card writes + +--- + +### POST `/mkdir` - Create Folder + +Creates a new folder on the SD card. + +**Request:** +```bash +curl -X POST -d "name=NewFolder&path=/" http://crosspoint.local/mkdir +``` + +**Form Parameters:** + +| Parameter | Required | Default | Description | +| --------- | -------- | ------- | ---------------------------- | +| `name` | Yes | - | Name of the folder to create | +| `path` | No | `/` | Parent directory path | + +**Response (200 OK):** +``` +Folder created: NewFolder +``` + +**Error Responses:** + +| Status | Body | Cause | +| ------ | ----------------------------- | ----------------------------- | +| 400 | `Missing folder name` | `name` parameter not provided | +| 400 | `Folder name cannot be empty` | Empty folder name | +| 400 | `Folder already exists` | Folder with same name exists | +| 500 | `Failed to create folder` | SD card error | + +--- + +### POST `/delete` - Delete File or Folder + +Deletes a file or folder from the SD card. + +**Request:** +```bash +# Delete a file +curl -X POST -d "path=/Books/mybook.epub&type=file" http://crosspoint.local/delete + +# Delete an empty folder +curl -X POST -d "path=/OldFolder&type=folder" http://crosspoint.local/delete +``` + +**Form Parameters:** + +| Parameter | Required | Default | Description | +| --------- | -------- | ------- | -------------------------------- | +| `path` | Yes | - | Path to the item to delete | +| `type` | No | `file` | Type of item: `file` or `folder` | + +**Response (200 OK):** +``` +Deleted successfully +``` + +**Error Responses:** + +| Status | Body | Cause | +| ------ | --------------------------------------------- | ----------------------------- | +| 400 | `Missing path` | `path` parameter not provided | +| 400 | `Cannot delete root directory` | Attempted to delete `/` | +| 400 | `Folder is not empty. Delete contents first.` | Non-empty folder | +| 403 | `Cannot delete system files` | Hidden file (starts with `.`) | +| 403 | `Cannot delete protected items` | Protected system folder | +| 404 | `Item not found` | Path does not exist | +| 500 | `Failed to delete item` | SD card error | + +**Protected Items:** +- Files/folders starting with `.` +- `System Volume Information` +- `XTCache` + +--- + +## WebSocket Endpoint + +### Port 81 - Fast Binary Upload + +A WebSocket endpoint for high-speed binary file uploads. More efficient than HTTP multipart for large files. + +**Connection:** +``` +ws://crosspoint.local:81/ +``` + +**Protocol:** + +1. **Client** sends TEXT message: `START:::` +2. **Server** responds with TEXT: `READY` +3. **Client** sends BINARY messages with file data chunks +4. **Server** sends TEXT progress updates: `PROGRESS::` +5. **Server** sends TEXT when complete: `DONE` or `ERROR:` + +**Example Session:** + +``` +Client -> "START:mybook.epub:1234567:/Books" +Server -> "READY" +Client -> [binary chunk 1] +Client -> [binary chunk 2] +Server -> "PROGRESS:65536:1234567" +Client -> [binary chunk 3] +... +Server -> "PROGRESS:1234567:1234567" +Server -> "DONE" +``` + +**Error Messages:** + +| Message | Cause | +| --------------------------------- | ---------------------------------- | +| `ERROR:Failed to create file` | Cannot create file on SD card | +| `ERROR:Invalid START format` | Malformed START message | +| `ERROR:No upload in progress` | Binary data received without START | +| `ERROR:Write failed - disk full?` | SD card write error | + +**Example with `websocat`:** +```bash +# Interactive session +websocat ws://crosspoint.local:81 + +# Then type: +START:mybook.epub:1234567:/Books +# Wait for READY, then send binary data +``` + +**Notes:** +- Progress updates are sent every 64KB or at completion +- Disconnection during upload will delete the incomplete file +- Existing files with the same name will be overwritten + +--- + +## Network Modes + +The device can operate in two network modes: + +### Station Mode (STA) +- Device connects to an existing WiFi network +- IP address assigned by router/DHCP +- `mode` field in `/api/status` returns `"STA"` +- `rssi` field shows signal strength + +### Access Point Mode (AP) +- Device creates its own WiFi hotspot +- Default IP is typically `192.168.4.1` +- `mode` field in `/api/status` returns `"AP"` +- `rssi` field returns `0` + +--- + +## Notes + +- These examples use `crosspoint.local`. If your network does not support mDNS or the address does not resolve, replace it with the specific **IP Address** displayed on your device screen (e.g., `http://192.168.1.102/`). +- All paths on the SD card start with `/` +- Trailing slashes are automatically stripped (except for root `/`) +- The webserver uses chunked transfer encoding for file listings diff --git a/docs/webserver.md b/docs/webserver.md index 2285a927..355bac41 100644 --- a/docs/webserver.md +++ b/docs/webserver.md @@ -172,89 +172,7 @@ This is useful for organizing your ebooks by genre, author, or series. ## Command Line File Management -For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode. - -### Uploading a File -To upload a file to the root directory, use the following command: -```bash -curl -F "file=@book.epub" "http://crosspoint.local/upload?path=/" -``` - -* **`-F "file=@filename"`**: Points to the local file on your computer. -* **`path=/`**: The destination folder on the device SD card. - -### Deleting a File - -To delete a specific file, provide the full path on the SD card: - -```bash -curl -F "path=/folder/file.epub" "http://crosspoint.local/delete" -``` - -### Advanced Flags - -For more reliable transfers of large EPUB files, consider adding these flags: - -* `-#`: Shows a simple progress bar. -* `--connect-timeout 30`: Limits how long curl waits to establish a connection (in seconds). -* `--max-time 300`: Sets a maximum duration for the entire transfer (5 minutes). - -> [!NOTE] -> These examples use `crosspoint.local`. If your network does not support mDNS or the address does not resolve, replace it with the specific **IP Address** displayed on your device screen (e.g., `http://192.168.1.102/`). - ---- - -## Troubleshooting - -### Cannot See the Device on the Network - -**Problem:** Browser shows "Cannot connect" or "Site can't be reached" - -**Solutions:** - -1. Verify both devices are on the **same WiFi network** - - Check your computer/phone WiFi settings - - Confirm the CrossPoint Reader shows "Connected" status -2. Double-check the IP address - - Make sure you typed it correctly - - Include `http://` at the beginning -3. Try disabling VPN if you're using one -4. Some networks have "client isolation" enabled - check with your network administrator - -### Connection Drops or Times Out - -**Problem:** WiFi connection is unstable - -**Solutions:** - -1. Move closer to the WiFi router -2. Check signal strength on the device (should be at least `||` or better) -3. Avoid interference from other devices -4. Try a different WiFi network if available - -### Upload Fails - -**Problem:** File upload doesn't complete or shows an error - -**Solutions:** - -1. Ensure the file is a valid `.epub` file -2. Check that the SD card has enough free space -3. Try uploading a smaller file first to test -4. Refresh the browser page and try again - -### Saved Password Not Working - -**Problem:** Device fails to connect with saved credentials - -**Solutions:** - -1. When connection fails, you'll be prompted to "Forget Network" -2. Select **Yes** to remove the saved password -3. Reconnect and enter the password again -4. Choose to save the new password - ---- +For power users, you can manage files directly from your terminal using `curl` while the device is in File Upload mode a detailed documentation can be found [here](./webserver-endpoints.md). ## Security Notes @@ -303,4 +221,5 @@ Your uploaded files will be immediately available in the file browser! ## Related Documentation - [User Guide](../USER_GUIDE.md) - General device operation +- [Troubleshooting](./troubleshooting.md) - Troubleshooting - [README](../README.md) - Project overview and features From 47ef92e8fd7d204fcf6ed0d20498f4886a83bbd8 Mon Sep 17 00:00:00 2001 From: KasyanDiGris Date: Wed, 21 Jan 2026 17:43:51 +0300 Subject: [PATCH 02/36] fix: OPDS browser OOM (#403) ## Summary - Rewrite OpdsParser to stream parsing instead of full content - Fix OOM due to big http xml response Closes #385 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- lib/OpdsParser/OpdsParser.cpp | 47 ++++++++++++------- lib/OpdsParser/OpdsParser.h | 25 ++++++---- lib/OpdsParser/OpdsStream.cpp | 15 ++++++ lib/OpdsParser/OpdsStream.h | 23 +++++++++ .../browser/OpdsBookBrowserActivity.cpp | 23 +++++---- src/network/HttpDownloader.cpp | 17 +++++-- src/network/HttpDownloader.h | 2 + 7 files changed, 112 insertions(+), 40 deletions(-) create mode 100644 lib/OpdsParser/OpdsStream.cpp create mode 100644 lib/OpdsParser/OpdsStream.h diff --git a/lib/OpdsParser/OpdsParser.cpp b/lib/OpdsParser/OpdsParser.cpp index da4042f0..4b58d8f8 100644 --- a/lib/OpdsParser/OpdsParser.cpp +++ b/lib/OpdsParser/OpdsParser.cpp @@ -4,6 +4,14 @@ #include +OpdsParser::OpdsParser() { + parser = XML_ParserCreate(nullptr); + if (!parser) { + errorOccured = true; + Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis()); + } +} + OpdsParser::~OpdsParser() { if (parser) { XML_StopParser(parser, XML_FALSE); @@ -14,13 +22,11 @@ OpdsParser::~OpdsParser() { } } -bool OpdsParser::parse(const char* xmlData, const size_t length) { - clear(); +size_t OpdsParser::write(uint8_t c) { return write(&c, 1); } - parser = XML_ParserCreate(nullptr); - if (!parser) { - Serial.printf("[%lu] [OPDS] Couldn't allocate memory for parser\n", millis()); - return false; +size_t OpdsParser::write(const uint8_t* xmlData, const size_t length) { + if (errorOccured) { + return length; } XML_SetUserData(parser, this); @@ -28,43 +34,48 @@ bool OpdsParser::parse(const char* xmlData, const size_t length) { XML_SetCharacterDataHandler(parser, characterData); // Parse in chunks to avoid large buffer allocations - const char* currentPos = xmlData; + const char* currentPos = reinterpret_cast(xmlData); size_t remaining = length; constexpr size_t chunkSize = 1024; while (remaining > 0) { void* const buf = XML_GetBuffer(parser, chunkSize); if (!buf) { + errorOccured = true; Serial.printf("[%lu] [OPDS] Couldn't allocate memory for buffer\n", millis()); XML_ParserFree(parser); parser = nullptr; - return false; + return length; } const size_t toRead = remaining < chunkSize ? remaining : chunkSize; memcpy(buf, currentPos, toRead); - const bool isFinal = (remaining == toRead); - if (XML_ParseBuffer(parser, static_cast(toRead), isFinal) == XML_STATUS_ERROR) { + if (XML_ParseBuffer(parser, static_cast(toRead), 0) == XML_STATUS_ERROR) { + errorOccured = true; Serial.printf("[%lu] [OPDS] Parse error at line %lu: %s\n", millis(), XML_GetCurrentLineNumber(parser), XML_ErrorString(XML_GetErrorCode(parser))); XML_ParserFree(parser); parser = nullptr; - return false; + return length; } currentPos += toRead; remaining -= toRead; } - - // Clean up parser - XML_ParserFree(parser); - parser = nullptr; - - Serial.printf("[%lu] [OPDS] Parsed %zu entries\n", millis(), entries.size()); - return true; + return length; } +void OpdsParser::flush() { + if (XML_Parse(parser, nullptr, 0, XML_TRUE) != XML_STATUS_OK) { + errorOccured = true; + XML_ParserFree(parser); + parser = nullptr; + } +} + +bool OpdsParser::error() const { return errorOccured; } + void OpdsParser::clear() { entries.clear(); currentEntry = OpdsEntry{}; diff --git a/lib/OpdsParser/OpdsParser.h b/lib/OpdsParser/OpdsParser.h index acb4b694..570ac4cc 100644 --- a/lib/OpdsParser/OpdsParser.h +++ b/lib/OpdsParser/OpdsParser.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -42,28 +43,30 @@ using OpdsBook = OpdsEntry; * } * } */ -class OpdsParser { +class OpdsParser final : public Print { public: - OpdsParser() = default; + OpdsParser(); ~OpdsParser(); // Disable copy OpdsParser(const OpdsParser&) = delete; OpdsParser& operator=(const OpdsParser&) = delete; - /** - * Parse an OPDS XML feed. - * @param xmlData Pointer to the XML data - * @param length Length of the XML data - * @return true if parsing succeeded, false on error - */ - bool parse(const char* xmlData, size_t length); + size_t write(uint8_t) override; + size_t write(const uint8_t*, size_t) override; + + void flush() override; + + bool error() const; + + operator bool() { return !error(); } /** * Get the parsed entries (both navigation and book entries). * @return Vector of OpdsEntry entries */ - const std::vector& getEntries() const { return entries; } + const std::vector& getEntries() const& { return entries; } + std::vector getEntries() && { return std::move(entries); } /** * Get only book entries (legacy compatibility). @@ -96,4 +99,6 @@ class OpdsParser { bool inAuthor = false; bool inAuthorName = false; bool inId = false; + + bool errorOccured = false; }; diff --git a/lib/OpdsParser/OpdsStream.cpp b/lib/OpdsParser/OpdsStream.cpp new file mode 100644 index 00000000..742624a8 --- /dev/null +++ b/lib/OpdsParser/OpdsStream.cpp @@ -0,0 +1,15 @@ +#include "OpdsStream.h" + +OpdsParserStream::OpdsParserStream(OpdsParser& parser) : parser(parser) {} + +int OpdsParserStream::available() { return 0; } + +int OpdsParserStream::peek() { abort(); } + +int OpdsParserStream::read() { abort(); } + +size_t OpdsParserStream::write(uint8_t c) { return parser.write(c); } + +size_t OpdsParserStream::write(const uint8_t* buffer, size_t size) { return parser.write(buffer, size); } + +OpdsParserStream::~OpdsParserStream() { parser.flush(); } diff --git a/lib/OpdsParser/OpdsStream.h b/lib/OpdsParser/OpdsStream.h new file mode 100644 index 00000000..c72f2b6b --- /dev/null +++ b/lib/OpdsParser/OpdsStream.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +#include "OpdsParser.h" + +class OpdsParserStream : public Stream { + public: + explicit OpdsParserStream(OpdsParser& parser); + + // That functions are not implimented for that stream + int available() override; + int peek() override; + int read() override; + + virtual size_t write(uint8_t c) override; + virtual size_t write(const uint8_t* buffer, size_t size) override; + + ~OpdsParserStream() override; + + private: + OpdsParser& parser; +}; diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 677f9cac..555cba91 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include "CrossPointSettings.h" @@ -265,23 +266,27 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) { std::string url = UrlUtils::buildUrl(serverUrl, path); Serial.printf("[%lu] [OPDS] Fetching: %s\n", millis(), url.c_str()); - std::string content; - if (!HttpDownloader::fetchUrl(url, content)) { - state = BrowserState::ERROR; - errorMessage = "Failed to fetch feed"; - updateRequired = true; - return; + OpdsParser parser; + + { + OpdsParserStream stream{parser}; + if (!HttpDownloader::fetchUrl(url, stream)) { + state = BrowserState::ERROR; + errorMessage = "Failed to fetch feed"; + updateRequired = true; + return; + } } - OpdsParser parser; - if (!parser.parse(content.c_str(), content.size())) { + if (!parser) { state = BrowserState::ERROR; errorMessage = "Failed to parse feed"; updateRequired = true; return; } - entries = parser.getEntries(); + entries = std::move(parser).getEntries(); + Serial.printf("[%lu] [OPDS] Found %d entries\n", millis(), entries.size()); selectorIndex = 0; if (entries.empty()) { diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index c4de3a05..fe65ea6b 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -9,7 +10,7 @@ #include "util/UrlUtils.h" -bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) { +bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) { // Use WiFiClientSecure for HTTPS, regular WiFiClient for HTTP std::unique_ptr client; if (UrlUtils::isHttpsUrl(url)) { @@ -34,10 +35,20 @@ bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) { return false; } - outContent = http.getString().c_str(); + http.writeToStream(&outContent); + http.end(); - Serial.printf("[%lu] [HTTP] Fetched %zu bytes\n", millis(), outContent.size()); + Serial.printf("[%lu] [HTTP] Fetch success\n", millis()); + return true; +} + +bool HttpDownloader::fetchUrl(const std::string& url, std::string& outContent) { + StreamString stream; + if (!fetchUrl(url, stream)) { + return false; + } + outContent = stream.c_str(); return true; } diff --git a/src/network/HttpDownloader.h b/src/network/HttpDownloader.h index e6e0f163..ac520a42 100644 --- a/src/network/HttpDownloader.h +++ b/src/network/HttpDownloader.h @@ -27,6 +27,8 @@ class HttpDownloader { */ static bool fetchUrl(const std::string& url, std::string& outContent); + static bool fetchUrl(const std::string& url, Stream& stream); + /** * Download a file to the SD card. * @param url The URL to download From 3ce11f14ce7bc3ce1f2f040bfb09a9b3d9f87f72 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Thu, 22 Jan 2026 02:20:22 +1100 Subject: [PATCH 03/36] chore: Cut release 0.15.0 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index ef27ffd5..7f42637d 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,7 @@ default_envs = default [crosspoint] -version = 0.14.0 +version = 0.15.0 [base] platform = espressif32 @ 6.12.0 From 7a53342f9d308a0476b5be6632eec9a0d48b9c45 Mon Sep 17 00:00:00 2001 From: Luke Stein <44452336+lukestein@users.noreply.github.com> Date: Tue, 27 Jan 2026 04:18:09 -0500 Subject: [PATCH 04/36] fix: Allow line break after ellipsis and underscore (#425) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Add additional punctuation marks to the list of characters that can be immediately followed by a line break even where there is no explicit space ## Additional Context * Huge appreciation to @osteotek for his amazing work on hyphenation. Reading on the device is so much better now. * I am getting bad line breaks when ellipses (…) are between words and book file does not explicitly include some kind of breaking space. * Per [discussion](https://github.com/crosspoint-reader/crosspoint-reader/pull/305#issuecomment-3765411406), several new characters are added in this PR to the `isExplicitHyphen` list to allow line breaks immediately after them: Character | Unicode | Usage | Why include it? -- | -- | -- | -- Solidus (Slash) | U+002F | / | Essential for breaking URLs and "and/or" constructs. Backslash | U+005C | \ | Critical for technical text, file paths, and coding documentation. Underscore | U+005F | _ | Prevents "runaway" line lengths in usernames or code snippets. Middle Dot | U+00B7 | · | Acts as a semantic separator in dictionaries or stylistic lists. Ellipsis | U+2026 | … | Prevents justification failure when dialogue lacks following spaces. Midline Horizontal Ellipsis | U+22EF | ⋯ | Useful for mathematical sequences and technical notation. ### Example: This shows an example of what line breaking looks like *with* this PR. Note the line break after "matter…" (which would not previously have been allowed). It's particularly important here because the book includes non-breaking spaces in "Mr. Aldrich" and "Mr. Rockefeller." ![IMG_2917](https://github.com/user-attachments/assets/8fa610a9-91dd-407f-8526-0019a8a7195f) --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **PARTIALLY** --- lib/Epub/Epub/hyphenation/HyphenationCommon.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp b/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp index 99584fde..0a6b7a92 100644 --- a/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp +++ b/lib/Epub/Epub/hyphenation/HyphenationCommon.cpp @@ -125,6 +125,8 @@ bool isExplicitHyphen(const uint32_t cp) { 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; From 67a679ab41053506ebfa17e2ee94d55b1690e10a Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Tue, 27 Jan 2026 12:20:48 +0300 Subject: [PATCH 05/36] fix: Add .vs folder to .gitignore (#466) ## Summary * Adds Visual Studio project files folder to .gitignore Otherwise: image --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0cc30a26..754c9f68 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ .vscode lib/EpdFont/fontsrc *.generated.h +.vs build **/__pycache__/ \ No newline at end of file From 9224bc3f8c53cda861b2e1a60ec1d00642b1cb88 Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Tue, 27 Jan 2026 10:21:15 +0100 Subject: [PATCH 06/36] fix: #348 fit cover artifacts 2 (#465) Supersedes #358 and includes the bugfix from #351 --- lib/Epub/Epub.cpp | 6 +++--- lib/JpegToBmpConverter/JpegToBmpConverter.cpp | 14 +++++++++----- lib/JpegToBmpConverter/JpegToBmpConverter.h | 4 ++-- src/activities/boot_sleep/SleepActivity.cpp | 1 + 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 78607573..33f920b4 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -359,7 +359,7 @@ const std::string& Epub::getLanguage() const { } std::string Epub::getCoverBmpPath(bool cropped) const { - const auto coverFileName = "cover" + cropped ? "_crop" : ""; + const auto coverFileName = std::string("cover") + (cropped ? "_crop" : ""); return cachePath + "/" + coverFileName + ".bmp"; } @@ -382,7 +382,7 @@ bool Epub::generateCoverBmp(bool cropped) const { if (coverImageHref.substr(coverImageHref.length() - 4) == ".jpg" || coverImageHref.substr(coverImageHref.length() - 5) == ".jpeg") { - Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image\n", millis()); + Serial.printf("[%lu] [EBP] Generating BMP from JPG cover image (%s mode)\n", millis(), cropped ? "cropped" : "fit"); const auto coverJpgTempPath = getCachePath() + "/.cover.jpg"; FsFile coverJpg; @@ -401,7 +401,7 @@ bool Epub::generateCoverBmp(bool cropped) const { coverJpg.close(); return false; } - const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp); + const bool success = JpegToBmpConverter::jpegFileToBmpStream(coverJpg, coverBmp, cropped); coverJpg.close(); coverBmp.close(); SdMan.remove(coverJpgTempPath.c_str()); diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp index 01451a05..84ac1d58 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.cpp +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.cpp @@ -200,7 +200,7 @@ unsigned char JpegToBmpConverter::jpegReadCallback(unsigned char* pBuf, const un // Internal implementation with configurable target size and bit depth bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight, - bool oneBit) { + bool oneBit, bool crop) { Serial.printf("[%lu] [JPG] Converting JPEG to %s BMP (target: %dx%d)\n", millis(), oneBit ? "1-bit" : "2-bit", targetWidth, targetHeight); @@ -242,8 +242,12 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm const float scaleToFitWidth = static_cast(targetWidth) / imageInfo.m_width; const float scaleToFitHeight = static_cast(targetHeight) / imageInfo.m_height; // We scale to the smaller dimension, so we can potentially crop later. - // TODO: ideally, we already crop here. - const float scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; + float scale = 1.0; + if (crop) { // if we will crop, scale to the smaller dimension + scale = (scaleToFitWidth > scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; + } else { // else, scale to the larger dimension to fit + scale = (scaleToFitWidth < scaleToFitHeight) ? scaleToFitWidth : scaleToFitHeight; + } outWidth = static_cast(imageInfo.m_width * scale); outHeight = static_cast(imageInfo.m_height * scale); @@ -550,8 +554,8 @@ bool JpegToBmpConverter::jpegFileToBmpStreamInternal(FsFile& jpegFile, Print& bm } // Core function: Convert JPEG file to 2-bit BMP (uses default target size) -bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut) { - return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false); +bool JpegToBmpConverter::jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut, bool crop) { + return jpegFileToBmpStreamInternal(jpegFile, bmpOut, TARGET_MAX_WIDTH, TARGET_MAX_HEIGHT, false, crop); } // Convert with custom target size (for thumbnails, 2-bit) diff --git a/lib/JpegToBmpConverter/JpegToBmpConverter.h b/lib/JpegToBmpConverter/JpegToBmpConverter.h index d5e9b950..9b92bb6d 100644 --- a/lib/JpegToBmpConverter/JpegToBmpConverter.h +++ b/lib/JpegToBmpConverter/JpegToBmpConverter.h @@ -8,10 +8,10 @@ class JpegToBmpConverter { static unsigned char jpegReadCallback(unsigned char* pBuf, unsigned char buf_size, unsigned char* pBytes_actually_read, void* pCallback_data); static bool jpegFileToBmpStreamInternal(class FsFile& jpegFile, Print& bmpOut, int targetWidth, int targetHeight, - bool oneBit); + bool oneBit, bool crop = true); public: - static bool jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut); + static bool jpegFileToBmpStream(FsFile& jpegFile, Print& bmpOut, bool crop = true); // Convert with custom target size (for thumbnails) static bool jpegFileToBmpStreamWithSize(FsFile& jpegFile, Print& bmpOut, int targetMaxWidth, int targetMaxHeight); // Convert to 1-bit BMP (black and white only, no grays) for fast home screen rendering diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index c0d6844f..40341e5f 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -260,6 +260,7 @@ void SleepActivity::renderCoverSleepScreen() const { if (SdMan.openFileForRead("SLP", coverBmpPath, file)) { Bitmap bitmap(file); if (bitmap.parseHeaders() == BmpReaderError::Ok) { + Serial.printf("[SLP] Rendering sleep cover: %s\n", coverBmpPath); renderBitmapSleepScreen(bitmap); return; } From e858ebbe88f0f95642aeca72b47672022b7937b8 Mon Sep 17 00:00:00 2001 From: Boris Faure Date: Tue, 27 Jan 2026 11:15:42 +0100 Subject: [PATCH 07/36] feat: add new configuration for front buttons, more usable on landscape ccw (#460) When reading on Landscape Counter ClockWise mode, the left/right button appear inverted: the upper button (left) goes down and the lower button (right) goes up. Discussion: #449 ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Add a new configuration for the front buttons: Back, Confirm, Right, Left --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- USER_GUIDE.md | 1 + src/CrossPointSettings.h | 7 ++++++- src/MappedInputManager.cpp | 16 ++++++++++++++++ src/activities/settings/SettingsActivity.cpp | 5 +++-- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index d670abb7..67dee480 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -116,6 +116,7 @@ The Settings screen allows you to configure the device's behavior. There are a f - Back, Confirm, Left, Right (default) - Left, Right, Back, Confirm - Left, Back, Confirm, Right + - Back, Confirm, Right, Left - **Side Button Layout (reader)**: Swap the order of the up and down volume buttons from Previous/Next to Next/Previous. This change is only in effect when reading. - **Long-press Chapter Skip**: Set whether long-pressing page turn buttons skip to the next/previous chapter. - "Chapter Skip" (default) - Long-pressing skips to next/previous chapter diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 8ce32a2c..2c33beb3 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -32,7 +32,12 @@ class CrossPointSettings { // Front button layout options // Default: Back, Confirm, Left, Right // Swapped: Left, Right, Back, Confirm - enum FRONT_BUTTON_LAYOUT { BACK_CONFIRM_LEFT_RIGHT = 0, LEFT_RIGHT_BACK_CONFIRM = 1, LEFT_BACK_CONFIRM_RIGHT = 2 }; + enum FRONT_BUTTON_LAYOUT { + BACK_CONFIRM_LEFT_RIGHT = 0, + LEFT_RIGHT_BACK_CONFIRM = 1, + LEFT_BACK_CONFIRM_RIGHT = 2, + BACK_CONFIRM_RIGHT_LEFT = 3 + }; // Side button layout options // Default: Previous, Next diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 1b038446..994dda5f 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -14,6 +14,9 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: return InputManager::BTN_CONFIRM; case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: + /* fall through */ + case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: + /* fall through */ default: return InputManager::BTN_BACK; } @@ -24,15 +27,22 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: return InputManager::BTN_LEFT; case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: + /* fall through */ + case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: + /* fall through */ default: return InputManager::BTN_CONFIRM; } case Button::Left: switch (frontLayout) { case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: + /* fall through */ case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: return InputManager::BTN_BACK; + case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: + return InputManager::BTN_RIGHT; case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: + /* fall through */ default: return InputManager::BTN_LEFT; } @@ -40,8 +50,12 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt switch (frontLayout) { case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: return InputManager::BTN_CONFIRM; + case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: + return InputManager::BTN_LEFT; case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: + /* fall through */ case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: + /* fall through */ default: return InputManager::BTN_RIGHT; } @@ -56,6 +70,7 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt case CrossPointSettings::NEXT_PREV: return InputManager::BTN_DOWN; case CrossPointSettings::PREV_NEXT: + /* fall through */ default: return InputManager::BTN_UP; } @@ -64,6 +79,7 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt case CrossPointSettings::NEXT_PREV: return InputManager::BTN_UP; case CrossPointSettings::PREV_NEXT: + /* fall through */ default: return InputManager::BTN_DOWN; } diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 943fdb4c..45b7a12d 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -37,8 +37,9 @@ const SettingInfo readerSettings[readerSettingsCount] = { constexpr int controlsSettingsCount = 4; const SettingInfo controlsSettings[controlsSettingsCount] = { - SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout, - {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}), + SettingInfo::Enum( + "Front Button Layout", &CrossPointSettings::frontButtonLayout, + {"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght", "Bck, Cnfrm, Rght, Lft"}), SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, {"Prev, Next", "Next, Prev"}), SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), From b8ebcf5867abd22ba472ab7420fa6933efeb661a Mon Sep 17 00:00:00 2001 From: Sam Davis Date: Tue, 27 Jan 2026 21:24:39 +1100 Subject: [PATCH 08/36] fix: remove decimal places from progress % (#507) ## Summary Addresses https://github.com/crosspoint-reader/crosspoint-reader/issues/504 - Reverts book progress % to showing as an integer instead of with a decimal place - This was changed to 1 decimal point of precision in https://github.com/crosspoint-reader/crosspoint-reader/pull/232 from what I can tell - As this wasn't the primary intention of that PR, I'm assuming it was left in accidentally IMO having a decimal place of precision is too much for something as vague as book completion percent. This de-clutters the status bar and prevents extra updates as you change pages. --- ### AI Usage YES --- src/activities/reader/EpubReaderActivity.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6ff39c5e..a2e14259 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -448,7 +448,7 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in // Right aligned text for progress counter char progressStr[32]; - snprintf(progressStr, sizeof(progressStr), "%d/%d %.1f%%", section->currentPage + 1, section->pageCount, + snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount, bookProgress); const std::string progress = progressStr; progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str()); From 5d369df6bee6db2eb6afd207824d7c023e80bdf2 Mon Sep 17 00:00:00 2001 From: Luke Stein <44452336+lukestein@users.noreply.github.com> Date: Tue, 27 Jan 2026 05:25:25 -0500 Subject: [PATCH 09/36] fix: Chapter Selection UI bugs when koreader sync is enabled, and clarify default kosync URL (#501) ## Summary * Fixes #475 * Fixes #477 * Closes #428 ## Additional Context * Updates to `src/activities/reader/EpubReaderChapterSelectionActivity.cpp` are copied verbatim from #433 (thanks to @jonasdiemer) * Update to `src/activities/settings/KOReaderSettingsActivity.cpp` per discussion with @itsthisjustin at #428 Tested on my device with several books and koreader sync turned on and off. --- ### AI Usage Did you use AI tools to help write this code? _NO_ --- .../reader/EpubReaderChapterSelectionActivity.cpp | 13 +++++++------ .../settings/KOReaderSettingsActivity.cpp | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp index ad4dd2ff..1b35e143 100644 --- a/src/activities/reader/EpubReaderChapterSelectionActivity.cpp +++ b/src/activities/reader/EpubReaderChapterSelectionActivity.cpp @@ -188,22 +188,23 @@ void EpubReaderChapterSelectionActivity::renderScreen() { const auto pageStartIndex = selectorIndex / pageItems * pageItems; renderer.fillRect(0, 60 + (selectorIndex % pageItems) * 30 - 2, pageWidth - 1, 30); - for (int itemIndex = pageStartIndex; itemIndex < totalItems && itemIndex < pageStartIndex + pageItems; itemIndex++) { - const int displayY = 60 + (itemIndex % pageItems) * 30; + for (int i = 0; i < pageItems; i++) { + int itemIndex = pageStartIndex + i; + if (itemIndex >= totalItems) break; + const int displayY = 60 + i * 30; const bool isSelected = (itemIndex == selectorIndex); if (isSyncItem(itemIndex)) { - // Draw sync option (at top or bottom) renderer.drawText(UI_10_FONT_ID, 20, displayY, ">> Sync Progress", !isSelected); } else { - // Draw TOC item (account for top sync offset) const int tocIndex = tocIndexFromItemIndex(itemIndex); auto item = epub->getTocItem(tocIndex); + const int indentSize = 20 + (item.level - 1) * 15; const std::string chapterName = renderer.truncatedText(UI_10_FONT_ID, item.title.c_str(), pageWidth - 40 - indentSize); - renderer.drawText(UI_10_FONT_ID, indentSize, 60 + (tocIndex % pageItems) * 30, chapterName.c_str(), - tocIndex != selectorIndex); + + renderer.drawText(UI_10_FONT_ID, indentSize, displayY, chapterName.c_str(), !isSelected); } } diff --git a/src/activities/settings/KOReaderSettingsActivity.cpp b/src/activities/settings/KOReaderSettingsActivity.cpp index 6eb22c8e..71003433 100644 --- a/src/activities/settings/KOReaderSettingsActivity.cpp +++ b/src/activities/settings/KOReaderSettingsActivity.cpp @@ -194,7 +194,7 @@ void KOReaderSettingsActivity::render() { } else if (i == 1) { status = KOREADER_STORE.getPassword().empty() ? "[Not Set]" : "[Set]"; } else if (i == 2) { - status = KOREADER_STORE.getServerUrl().empty() ? "[Not Set]" : "[Set]"; + status = KOREADER_STORE.getServerUrl().empty() ? "[Default]" : "[Custom]"; } else if (i == 3) { status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "[Filename]" : "[Binary]"; } else if (i == 4) { From 0bc0baa966b543960e3402a0c6ea0f7ff9567bb7 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 27 Jan 2026 15:25:48 +0500 Subject: [PATCH 10/36] feat: treat .md files as .txt (#498) ## Summary * Quick fix for markdown reading - open them as txt files --- src/activities/home/MyLibraryActivity.cpp | 3 ++- src/activities/reader/ReaderActivity.cpp | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 9e6f3734..1db32397 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -120,7 +120,8 @@ void MyLibraryActivity::loadFiles() { } else { auto filename = std::string(name); if (StringUtils::checkFileExtension(filename, ".epub") || StringUtils::checkFileExtension(filename, ".xtch") || - StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt")) { + StringUtils::checkFileExtension(filename, ".xtc") || StringUtils::checkFileExtension(filename, ".txt") || + StringUtils::checkFileExtension(filename, ".md")) { files.emplace_back(filename); } } diff --git a/src/activities/reader/ReaderActivity.cpp b/src/activities/reader/ReaderActivity.cpp index 14d6623c..04240b3c 100644 --- a/src/activities/reader/ReaderActivity.cpp +++ b/src/activities/reader/ReaderActivity.cpp @@ -22,9 +22,8 @@ bool ReaderActivity::isXtcFile(const std::string& path) { } bool ReaderActivity::isTxtFile(const std::string& path) { - if (path.length() < 4) return false; - std::string ext4 = path.substr(path.length() - 4); - return ext4 == ".txt" || ext4 == ".TXT"; + return StringUtils::checkFileExtension(path, ".txt") || + StringUtils::checkFileExtension(path, ".md"); // Treat .md as txt files (until we have a markdown reader) } std::unique_ptr ReaderActivity::loadEpub(const std::string& path) { From 13f0ebed9695566ec699482b0f390fddae2efa31 Mon Sep 17 00:00:00 2001 From: Brendan O'Leary Date: Tue, 27 Jan 2026 11:26:17 +0100 Subject: [PATCH 11/36] UX improvment to Forget Network page (#484) ## Summary On the Forget Network page * Update the default option to be DON'T forget the network * Make the options clearer ("Cancel" and "Forget network") * Unify the button hints to match the rest of the UI ## Additional Context Closes #427 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? PARTIALLY --- .../network/WifiSelectionActivity.cpp | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/activities/network/WifiSelectionActivity.cpp b/src/activities/network/WifiSelectionActivity.cpp index 07d44418..5c45223b 100644 --- a/src/activities/network/WifiSelectionActivity.cpp +++ b/src/activities/network/WifiSelectionActivity.cpp @@ -354,8 +354,8 @@ void WifiSelectionActivity::loop() { updateRequired = true; } } else if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - if (forgetPromptSelection == 0) { - // User chose "Yes" - forget the network + if (forgetPromptSelection == 1) { + // User chose "Forget network" - forget the network xSemaphoreTake(renderingMutex, portMAX_DELAY); WIFI_STORE.removeCredential(selectedSSID); xSemaphoreGive(renderingMutex); @@ -366,7 +366,7 @@ void WifiSelectionActivity::loop() { network->hasSavedPassword = false; } } - // Go back to network list + // Go back to network list (whether Cancel or Forget network was selected) state = WifiSelectionState::NETWORK_LIST; updateRequired = true; } else if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { @@ -391,7 +391,7 @@ void WifiSelectionActivity::loop() { // If we used saved credentials, offer to forget the network if (usedSavedPassword) { state = WifiSelectionState::FORGET_PROMPT; - forgetPromptSelection = 0; // Default to "Yes" + forgetPromptSelection = 0; // Default to "Cancel" } else { // Go back to network list on failure state = WifiSelectionState::NETWORK_LIST; @@ -623,7 +623,9 @@ void WifiSelectionActivity::renderConnected() const { const std::string ipInfo = "IP Address: " + connectedIP; renderer.drawCenteredText(UI_10_FONT_ID, top + 40, ipInfo.c_str()); - renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue"); + // Use centralized button hints + const auto labels = mappedInput.mapLabels("", "Continue", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } void WifiSelectionActivity::renderSavePrompt() const { @@ -663,7 +665,9 @@ void WifiSelectionActivity::renderSavePrompt() const { renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No"); } - renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm"); + // Use centralized button hints + const auto labels = mappedInput.mapLabels("« Skip", "Select", "Left", "Right"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } void WifiSelectionActivity::renderConnectionFailed() const { @@ -673,7 +677,10 @@ void WifiSelectionActivity::renderConnectionFailed() const { renderer.drawCenteredText(UI_12_FONT_ID, top - 20, "Connection Failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, top + 20, connectionError.c_str()); - renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue"); + + // Use centralized button hints + const auto labels = mappedInput.mapLabels("« Back", "Continue", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } void WifiSelectionActivity::renderForgetPrompt() const { @@ -692,26 +699,28 @@ void WifiSelectionActivity::renderForgetPrompt() const { renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Remove saved password?"); - // Draw Yes/No buttons + // Draw Cancel/Forget network buttons const int buttonY = top + 80; - constexpr int buttonWidth = 60; + constexpr int buttonWidth = 120; constexpr int buttonSpacing = 30; constexpr int totalWidth = buttonWidth * 2 + buttonSpacing; const int startX = (pageWidth - totalWidth) / 2; - // Draw "Yes" button + // Draw "Cancel" button if (forgetPromptSelection == 0) { - renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Yes]"); + renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Cancel]"); } else { - renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Yes"); + renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Cancel"); } - // Draw "No" button + // Draw "Forget network" button if (forgetPromptSelection == 1) { - renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[No]"); + renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[Forget network]"); } else { - renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No"); + renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "Forget network"); } - renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm"); + // Use centralized button hints + const auto labels = mappedInput.mapLabels("« Back", "Select", "Left", "Right"); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); } From 3a761b18af6d5f287a43b1c9477714dbb7fc8198 Mon Sep 17 00:00:00 2001 From: Justin Mitchell Date: Tue, 27 Jan 2026 06:02:38 -0500 Subject: [PATCH 12/36] Refactors Calibre Wireless Device & Calibre Library (#404) Our esp32 consistently dropped the last few packets of the TCP transfer in the old implementation. Only about 1/5 transfers would complete. I've refactored that entire system into an actual Calibre Device Plugin that basically uses the exact same system as the web server's file transfer protocol. I kept them separate so that we don't muddy up the existing file transfer stuff even if it's basically the same at the end of the day I didn't want to limit our ability to change it later. I've also added basic auth to OPDS and renamed that feature to OPDS Browser to just disassociate it from Calibre. --------- Co-authored-by: Arthur Tazhitdinov Co-authored-by: Dave Allie --- USER_GUIDE.md | 14 +- src/CrossPointSettings.cpp | 20 +- src/CrossPointSettings.h | 2 + .../browser/OpdsBookBrowserActivity.cpp | 5 +- src/activities/home/HomeActivity.cpp | 4 +- .../network/CalibreConnectActivity.cpp | 276 +++++++ .../network/CalibreConnectActivity.h | 55 ++ .../network/CalibreWirelessActivity.cpp | 756 ------------------ .../network/CalibreWirelessActivity.h | 135 ---- .../network/CrossPointWebServerActivity.cpp | 22 +- .../network/CrossPointWebServerActivity.h | 2 +- .../network/NetworkModeSelectionActivity.cpp | 18 +- .../network/NetworkModeSelectionActivity.h | 3 +- .../settings/CalibreSettingsActivity.cpp | 84 +- .../settings/CalibreSettingsActivity.h | 4 +- .../settings/CategorySettingsActivity.cpp | 2 +- src/activities/settings/SettingsActivity.cpp | 2 +- src/network/CrossPointWebServer.cpp | 118 ++- src/network/CrossPointWebServer.h | 20 +- src/network/HttpDownloader.cpp | 17 + 20 files changed, 614 insertions(+), 945 deletions(-) create mode 100644 src/activities/network/CalibreConnectActivity.cpp create mode 100644 src/activities/network/CalibreConnectActivity.h delete mode 100644 src/activities/network/CalibreWirelessActivity.cpp delete mode 100644 src/activities/network/CalibreWirelessActivity.h diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 67dee480..f160af74 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -81,6 +81,18 @@ See the [webserver docs](./docs/webserver.md) for more information on how to con > [!TIP] > Advanced users can also manage files programmatically or via the command line using `curl`. See the [webserver docs](./docs/webserver.md) for details. +### 3.4.1 Calibre Wireless Transfers + +CrossPoint supports sending books from Calibre using the CrossPoint Reader device plugin. + +1. Install the plugin in Calibre: + - Head to https://github.com/crosspoint-reader/calibre-plugins/releases to download the latest version of the crosspoint_reader plugin. + - Download the zip file. + - Open Calibre → Preferences → Plugins → Load plugin from file → Select the zip file. +2. On the device: File Transfer → Connect to Calibre → Join a network. +3. Make sure your computer is on the same WiFi network. +4. In Calibre, click "Send to device" to transfer books. + ### 3.5 Settings The Settings screen allows you to configure the device's behavior. There are a few settings you can adjust: @@ -132,7 +144,7 @@ The Settings screen allows you to configure the device's behavior. There are a f - **Reader Paragraph Alignment**: Set the alignment of paragraphs; options are "Justified" (default), "Left", "Center", or "Right". - **Time to Sleep**: Set the duration of inactivity before the device automatically goes to sleep. - **Refresh Frequency**: Set how often the screen does a full refresh while reading to reduce ghosting. -- **Calibre Settings**: Set up integration for accessing a Calibre web library or connecting to Calibre as a wireless device. +- **OPDS Browser**: Configure OPDS server settings for browsing and downloading books. Set the server URL (for Calibre Content Server, add `/opds` to the end), and optionally configure username and password for servers requiring authentication. Note: Only HTTP Basic authentication is supported. If using Calibre Content Server with authentication enabled, you must set it to use Basic authentication instead of the default Digest authentication. - **Check for updates**: Check for firmware updates over WiFi. ### 3.6 Sleep Screen diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index f5e8ded5..ea26ad91 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -14,7 +14,7 @@ CrossPointSettings CrossPointSettings::instance; namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 20; +constexpr uint8_t SETTINGS_COUNT = 22; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -49,6 +49,9 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, hideBatteryPercentage); serialization::writePod(outputFile, longPressChapterSkip); serialization::writePod(outputFile, hyphenationEnabled); + // New fields added at end for backward compatibility + serialization::writeString(outputFile, std::string(opdsUsername)); + serialization::writeString(outputFile, std::string(opdsPassword)); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -120,6 +123,21 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, hyphenationEnabled); if (++settingsRead >= fileSettingsCount) break; + // New fields added at end for backward compatibility + { + std::string usernameStr; + serialization::readString(inputFile, usernameStr); + strncpy(opdsUsername, usernameStr.c_str(), sizeof(opdsUsername) - 1); + opdsUsername[sizeof(opdsUsername) - 1] = '\0'; + } + if (++settingsRead >= fileSettingsCount) break; + { + std::string passwordStr; + serialization::readString(inputFile, passwordStr); + strncpy(opdsPassword, passwordStr.c_str(), sizeof(opdsPassword) - 1); + opdsPassword[sizeof(opdsPassword) - 1] = '\0'; + } + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 2c33beb3..f8892bef 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -95,6 +95,8 @@ class CrossPointSettings { uint8_t screenMargin = 5; // OPDS browser settings char opdsServerUrl[128] = ""; + char opdsUsername[64] = ""; + char opdsPassword[64] = ""; // Hide battery percentage uint8_t hideBatteryPercentage = HIDE_NEVER; // Long-press chapter skip on side buttons diff --git a/src/activities/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 555cba91..2bde74de 100644 --- a/src/activities/browser/OpdsBookBrowserActivity.cpp +++ b/src/activities/browser/OpdsBookBrowserActivity.cpp @@ -18,7 +18,6 @@ namespace { constexpr int PAGE_ITEMS = 23; constexpr int SKIP_PAGE_MS = 700; -constexpr char OPDS_ROOT_PATH[] = "opds"; // No leading slash - relative to server URL } // namespace void OpdsBookBrowserActivity::taskTrampoline(void* param) { @@ -33,7 +32,7 @@ void OpdsBookBrowserActivity::onEnter() { state = BrowserState::CHECK_WIFI; entries.clear(); navigationHistory.clear(); - currentPath = OPDS_ROOT_PATH; + currentPath = ""; // Root path - user provides full URL in settings selectorIndex = 0; errorMessage.clear(); statusMessage = "Checking WiFi..."; @@ -172,7 +171,7 @@ void OpdsBookBrowserActivity::render() const { const auto pageWidth = renderer.getScreenWidth(); const auto pageHeight = renderer.getScreenHeight(); - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Calibre Library", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "OPDS Browser", true, EpdFontFamily::BOLD); if (state == BrowserState::CHECK_WIFI) { renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index eb11ba95..3389e80d 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -502,8 +502,8 @@ void HomeActivity::render() { // Build menu items dynamically std::vector menuItems = {"My Library", "File Transfer", "Settings"}; if (hasOpdsUrl) { - // Insert Calibre Library after My Library - menuItems.insert(menuItems.begin() + 1, "Calibre Library"); + // Insert OPDS Browser after My Library + menuItems.insert(menuItems.begin() + 1, "OPDS Browser"); } const int menuTileWidth = pageWidth - 2 * margin; diff --git a/src/activities/network/CalibreConnectActivity.cpp b/src/activities/network/CalibreConnectActivity.cpp new file mode 100644 index 00000000..8aa60c40 --- /dev/null +++ b/src/activities/network/CalibreConnectActivity.cpp @@ -0,0 +1,276 @@ +#include "CalibreConnectActivity.h" + +#include +#include +#include +#include + +#include "MappedInputManager.h" +#include "ScreenComponents.h" +#include "WifiSelectionActivity.h" +#include "fontIds.h" + +namespace { +constexpr const char* HOSTNAME = "crosspoint"; +} // namespace + +void CalibreConnectActivity::taskTrampoline(void* param) { + auto* self = static_cast(param); + self->displayTaskLoop(); +} + +void CalibreConnectActivity::onEnter() { + ActivityWithSubactivity::onEnter(); + + renderingMutex = xSemaphoreCreateMutex(); + updateRequired = true; + state = CalibreConnectState::WIFI_SELECTION; + connectedIP.clear(); + connectedSSID.clear(); + lastHandleClientTime = 0; + lastProgressReceived = 0; + lastProgressTotal = 0; + currentUploadName.clear(); + lastCompleteName.clear(); + lastCompleteAt = 0; + exitRequested = false; + + xTaskCreate(&CalibreConnectActivity::taskTrampoline, "CalibreConnectTask", + 2048, // Stack size + this, // Parameters + 1, // Priority + &displayTaskHandle // Task handle + ); + + if (WiFi.status() != WL_CONNECTED) { + enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, + [this](const bool connected) { onWifiSelectionComplete(connected); })); + } else { + connectedIP = WiFi.localIP().toString().c_str(); + connectedSSID = WiFi.SSID().c_str(); + startWebServer(); + } +} + +void CalibreConnectActivity::onExit() { + ActivityWithSubactivity::onExit(); + + stopWebServer(); + MDNS.end(); + + delay(50); + WiFi.disconnect(false); + delay(30); + WiFi.mode(WIFI_OFF); + delay(30); + + xSemaphoreTake(renderingMutex, portMAX_DELAY); + if (displayTaskHandle) { + vTaskDelete(displayTaskHandle); + displayTaskHandle = nullptr; + } + vSemaphoreDelete(renderingMutex); + renderingMutex = nullptr; +} + +void CalibreConnectActivity::onWifiSelectionComplete(const bool connected) { + if (!connected) { + exitActivity(); + onComplete(); + return; + } + + if (subActivity) { + connectedIP = static_cast(subActivity.get())->getConnectedIP(); + } else { + connectedIP = WiFi.localIP().toString().c_str(); + } + connectedSSID = WiFi.SSID().c_str(); + exitActivity(); + startWebServer(); +} + +void CalibreConnectActivity::startWebServer() { + state = CalibreConnectState::SERVER_STARTING; + updateRequired = true; + + if (MDNS.begin(HOSTNAME)) { + // mDNS is optional for the Calibre plugin but still helpful for users. + Serial.printf("[%lu] [CAL] mDNS started: http://%s.local/\n", millis(), HOSTNAME); + } + + webServer.reset(new CrossPointWebServer()); + webServer->begin(); + + if (webServer->isRunning()) { + state = CalibreConnectState::SERVER_RUNNING; + updateRequired = true; + } else { + state = CalibreConnectState::ERROR; + updateRequired = true; + } +} + +void CalibreConnectActivity::stopWebServer() { + if (webServer) { + webServer->stop(); + webServer.reset(); + } +} + +void CalibreConnectActivity::loop() { + if (subActivity) { + subActivity->loop(); + return; + } + + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + exitRequested = true; + } + + if (webServer && webServer->isRunning()) { + const unsigned long timeSinceLastHandleClient = millis() - lastHandleClientTime; + if (lastHandleClientTime > 0 && timeSinceLastHandleClient > 100) { + Serial.printf("[%lu] [CAL] WARNING: %lu ms gap since last handleClient\n", millis(), timeSinceLastHandleClient); + } + + esp_task_wdt_reset(); + constexpr int MAX_ITERATIONS = 80; + for (int i = 0; i < MAX_ITERATIONS && webServer->isRunning(); i++) { + webServer->handleClient(); + if ((i & 0x07) == 0x07) { + esp_task_wdt_reset(); + } + if ((i & 0x0F) == 0x0F) { + yield(); + if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { + exitRequested = true; + break; + } + } + } + lastHandleClientTime = millis(); + + const auto status = webServer->getWsUploadStatus(); + bool changed = false; + if (status.inProgress) { + if (status.received != lastProgressReceived || status.total != lastProgressTotal || + status.filename != currentUploadName) { + lastProgressReceived = status.received; + lastProgressTotal = status.total; + currentUploadName = status.filename; + changed = true; + } + } else if (lastProgressReceived != 0 || lastProgressTotal != 0) { + lastProgressReceived = 0; + lastProgressTotal = 0; + currentUploadName.clear(); + changed = true; + } + if (status.lastCompleteAt != 0 && status.lastCompleteAt != lastCompleteAt) { + lastCompleteAt = status.lastCompleteAt; + lastCompleteName = status.lastCompleteName; + changed = true; + } + if (lastCompleteAt > 0 && (millis() - lastCompleteAt) >= 6000) { + lastCompleteAt = 0; + lastCompleteName.clear(); + changed = true; + } + if (changed) { + updateRequired = true; + } + } + + if (exitRequested) { + onComplete(); + return; + } +} + +void CalibreConnectActivity::displayTaskLoop() { + while (true) { + if (updateRequired) { + updateRequired = false; + xSemaphoreTake(renderingMutex, portMAX_DELAY); + render(); + xSemaphoreGive(renderingMutex); + } + vTaskDelay(10 / portTICK_PERIOD_MS); + } +} + +void CalibreConnectActivity::render() const { + if (state == CalibreConnectState::SERVER_RUNNING) { + renderer.clearScreen(); + renderServerRunning(); + renderer.displayBuffer(); + return; + } + + renderer.clearScreen(); + const auto pageHeight = renderer.getScreenHeight(); + if (state == CalibreConnectState::SERVER_STARTING) { + renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Calibre...", true, EpdFontFamily::BOLD); + } else if (state == CalibreConnectState::ERROR) { + renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Calibre setup failed", true, EpdFontFamily::BOLD); + } + renderer.displayBuffer(); +} + +void CalibreConnectActivity::renderServerRunning() const { + constexpr int LINE_SPACING = 24; + constexpr int SMALL_SPACING = 20; + constexpr int SECTION_SPACING = 40; + constexpr int TOP_PADDING = 14; + renderer.drawCenteredText(UI_12_FONT_ID, 15, "Connect to Calibre", true, EpdFontFamily::BOLD); + + int y = 55 + TOP_PADDING; + renderer.drawCenteredText(UI_10_FONT_ID, y, "Network", true, EpdFontFamily::BOLD); + y += LINE_SPACING; + std::string ssidInfo = "Network: " + connectedSSID; + if (ssidInfo.length() > 28) { + ssidInfo.replace(25, ssidInfo.length() - 25, "..."); + } + renderer.drawCenteredText(UI_10_FONT_ID, y, ssidInfo.c_str()); + renderer.drawCenteredText(UI_10_FONT_ID, y + LINE_SPACING, ("IP: " + connectedIP).c_str()); + + y += LINE_SPACING * 2 + SECTION_SPACING; + renderer.drawCenteredText(UI_10_FONT_ID, y, "Setup", true, EpdFontFamily::BOLD); + y += LINE_SPACING; + renderer.drawCenteredText(SMALL_FONT_ID, y, "1) Install CrossPoint Reader plugin"); + renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING, "2) Be on the same WiFi network"); + renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 2, "3) In Calibre: \"Send to device\""); + renderer.drawCenteredText(SMALL_FONT_ID, y + SMALL_SPACING * 3, "Keep this screen open while sending"); + + y += SMALL_SPACING * 3 + SECTION_SPACING; + renderer.drawCenteredText(UI_10_FONT_ID, y, "Status", true, EpdFontFamily::BOLD); + y += LINE_SPACING; + if (lastProgressTotal > 0 && lastProgressReceived <= lastProgressTotal) { + std::string label = "Receiving"; + if (!currentUploadName.empty()) { + label += ": " + currentUploadName; + if (label.length() > 34) { + label.replace(31, label.length() - 31, "..."); + } + } + renderer.drawCenteredText(SMALL_FONT_ID, y, label.c_str()); + constexpr int barWidth = 300; + constexpr int barHeight = 16; + constexpr int barX = (480 - barWidth) / 2; + ScreenComponents::drawProgressBar(renderer, barX, y + 22, barWidth, barHeight, lastProgressReceived, + lastProgressTotal); + y += 40; + } + + if (lastCompleteAt > 0 && (millis() - lastCompleteAt) < 6000) { + std::string msg = "Received: " + lastCompleteName; + if (msg.length() > 36) { + msg.replace(33, msg.length() - 33, "..."); + } + renderer.drawCenteredText(SMALL_FONT_ID, y, msg.c_str()); + } + + const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); + renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); +} diff --git a/src/activities/network/CalibreConnectActivity.h b/src/activities/network/CalibreConnectActivity.h new file mode 100644 index 00000000..08cf4bb4 --- /dev/null +++ b/src/activities/network/CalibreConnectActivity.h @@ -0,0 +1,55 @@ +#pragma once +#include +#include +#include + +#include +#include +#include + +#include "activities/ActivityWithSubactivity.h" +#include "network/CrossPointWebServer.h" + +enum class CalibreConnectState { WIFI_SELECTION, SERVER_STARTING, SERVER_RUNNING, ERROR }; + +/** + * CalibreConnectActivity starts the file transfer server in STA mode, + * but renders Calibre-specific instructions instead of the web transfer UI. + */ +class CalibreConnectActivity final : public ActivityWithSubactivity { + TaskHandle_t displayTaskHandle = nullptr; + SemaphoreHandle_t renderingMutex = nullptr; + bool updateRequired = false; + CalibreConnectState state = CalibreConnectState::WIFI_SELECTION; + const std::function onComplete; + + std::unique_ptr webServer; + std::string connectedIP; + std::string connectedSSID; + unsigned long lastHandleClientTime = 0; + size_t lastProgressReceived = 0; + size_t lastProgressTotal = 0; + std::string currentUploadName; + std::string lastCompleteName; + unsigned long lastCompleteAt = 0; + bool exitRequested = false; + + static void taskTrampoline(void* param); + [[noreturn]] void displayTaskLoop(); + void render() const; + void renderServerRunning() const; + + void onWifiSelectionComplete(bool connected); + void startWebServer(); + void stopWebServer(); + + public: + explicit CalibreConnectActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, + const std::function& onComplete) + : ActivityWithSubactivity("CalibreConnect", renderer, mappedInput), onComplete(onComplete) {} + void onEnter() override; + void onExit() override; + void loop() override; + bool skipLoopDelay() override { return webServer && webServer->isRunning(); } + bool preventAutoSleep() override { return webServer && webServer->isRunning(); } +}; diff --git a/src/activities/network/CalibreWirelessActivity.cpp b/src/activities/network/CalibreWirelessActivity.cpp deleted file mode 100644 index 0ad9094a..00000000 --- a/src/activities/network/CalibreWirelessActivity.cpp +++ /dev/null @@ -1,756 +0,0 @@ -#include "CalibreWirelessActivity.h" - -#include -#include -#include -#include - -#include - -#include "MappedInputManager.h" -#include "ScreenComponents.h" -#include "fontIds.h" -#include "util/StringUtils.h" - -namespace { -constexpr uint16_t UDP_PORTS[] = {54982, 48123, 39001, 44044, 59678}; -constexpr uint16_t LOCAL_UDP_PORT = 8134; // Port to receive responses -} // namespace - -void CalibreWirelessActivity::displayTaskTrampoline(void* param) { - auto* self = static_cast(param); - self->displayTaskLoop(); -} - -void CalibreWirelessActivity::networkTaskTrampoline(void* param) { - auto* self = static_cast(param); - self->networkTaskLoop(); -} - -void CalibreWirelessActivity::onEnter() { - Activity::onEnter(); - - renderingMutex = xSemaphoreCreateMutex(); - stateMutex = xSemaphoreCreateMutex(); - - state = WirelessState::DISCOVERING; - statusMessage = "Discovering Calibre..."; - errorMessage.clear(); - calibreHostname.clear(); - calibreHost.clear(); - calibrePort = 0; - calibreAltPort = 0; - currentFilename.clear(); - currentFileSize = 0; - bytesReceived = 0; - inBinaryMode = false; - recvBuffer.clear(); - - updateRequired = true; - - // Start UDP listener for Calibre responses - udp.begin(LOCAL_UDP_PORT); - - // Create display task - xTaskCreate(&CalibreWirelessActivity::displayTaskTrampoline, "CalDisplayTask", 2048, this, 1, &displayTaskHandle); - - // Create network task with larger stack for JSON parsing - xTaskCreate(&CalibreWirelessActivity::networkTaskTrampoline, "CalNetworkTask", 12288, this, 2, &networkTaskHandle); -} - -void CalibreWirelessActivity::onExit() { - Activity::onExit(); - - // Turn off WiFi when exiting - WiFi.mode(WIFI_OFF); - - // Stop UDP listening - udp.stop(); - - // Close TCP client if connected - if (tcpClient.connected()) { - tcpClient.stop(); - } - - // Close any open file - if (currentFile) { - currentFile.close(); - } - - // Acquire stateMutex before deleting network task to avoid race condition - xSemaphoreTake(stateMutex, portMAX_DELAY); - if (networkTaskHandle) { - vTaskDelete(networkTaskHandle); - networkTaskHandle = nullptr; - } - xSemaphoreGive(stateMutex); - - // Acquire renderingMutex before deleting display task - xSemaphoreTake(renderingMutex, portMAX_DELAY); - if (displayTaskHandle) { - vTaskDelete(displayTaskHandle); - displayTaskHandle = nullptr; - } - vSemaphoreDelete(renderingMutex); - renderingMutex = nullptr; - - vSemaphoreDelete(stateMutex); - stateMutex = nullptr; -} - -void CalibreWirelessActivity::loop() { - if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { - onComplete(); - return; - } -} - -void CalibreWirelessActivity::displayTaskLoop() { - while (true) { - if (updateRequired) { - updateRequired = false; - xSemaphoreTake(renderingMutex, portMAX_DELAY); - render(); - xSemaphoreGive(renderingMutex); - } - vTaskDelay(50 / portTICK_PERIOD_MS); - } -} - -void CalibreWirelessActivity::networkTaskLoop() { - while (true) { - xSemaphoreTake(stateMutex, portMAX_DELAY); - const auto currentState = state; - xSemaphoreGive(stateMutex); - - switch (currentState) { - case WirelessState::DISCOVERING: - listenForDiscovery(); - break; - - case WirelessState::CONNECTING: - case WirelessState::WAITING: - case WirelessState::RECEIVING: - handleTcpClient(); - break; - - case WirelessState::COMPLETE: - case WirelessState::DISCONNECTED: - case WirelessState::ERROR: - // Just wait, user will exit - vTaskDelay(100 / portTICK_PERIOD_MS); - break; - } - - vTaskDelay(10 / portTICK_PERIOD_MS); - } -} - -void CalibreWirelessActivity::listenForDiscovery() { - // Broadcast "hello" on all UDP discovery ports to find Calibre - for (const uint16_t port : UDP_PORTS) { - udp.beginPacket("255.255.255.255", port); - udp.write(reinterpret_cast("hello"), 5); - udp.endPacket(); - } - - // Wait for Calibre's response - vTaskDelay(500 / portTICK_PERIOD_MS); - - // Check for response - const int packetSize = udp.parsePacket(); - if (packetSize > 0) { - char buffer[256]; - const int len = udp.read(buffer, sizeof(buffer) - 1); - if (len > 0) { - buffer[len] = '\0'; - - // Parse Calibre's response format: - // "calibre wireless device client (on hostname);port,content_server_port" - // or just the hostname and port info - std::string response(buffer); - - // Try to extract host and port - // Format: "calibre wireless device client (on HOSTNAME);PORT,..." - size_t onPos = response.find("(on "); - size_t closePos = response.find(')'); - size_t semiPos = response.find(';'); - size_t commaPos = response.find(',', semiPos); - - if (semiPos != std::string::npos) { - // Get ports after semicolon (format: "port1,port2") - std::string portStr; - if (commaPos != std::string::npos && commaPos > semiPos) { - portStr = response.substr(semiPos + 1, commaPos - semiPos - 1); - // Get alternative port after comma - std::string altPortStr = response.substr(commaPos + 1); - // Trim whitespace and non-digits from alt port - size_t altEnd = 0; - while (altEnd < altPortStr.size() && altPortStr[altEnd] >= '0' && altPortStr[altEnd] <= '9') { - altEnd++; - } - if (altEnd > 0) { - calibreAltPort = static_cast(std::stoi(altPortStr.substr(0, altEnd))); - } - } else { - portStr = response.substr(semiPos + 1); - } - - // Trim whitespace from main port - while (!portStr.empty() && (portStr[0] == ' ' || portStr[0] == '\t')) { - portStr = portStr.substr(1); - } - - if (!portStr.empty()) { - calibrePort = static_cast(std::stoi(portStr)); - } - - // Get hostname if present, otherwise use sender IP - if (onPos != std::string::npos && closePos != std::string::npos && closePos > onPos + 4) { - calibreHostname = response.substr(onPos + 4, closePos - onPos - 4); - } - } - - // Use the sender's IP as the host to connect to - calibreHost = udp.remoteIP().toString().c_str(); - if (calibreHostname.empty()) { - calibreHostname = calibreHost; - } - - if (calibrePort > 0) { - // Connect to Calibre's TCP server - try main port first, then alt port - setState(WirelessState::CONNECTING); - setStatus("Connecting to " + calibreHostname + "..."); - - // Small delay before connecting - vTaskDelay(100 / portTICK_PERIOD_MS); - - bool connected = false; - - // Try main port first - if (tcpClient.connect(calibreHost.c_str(), calibrePort, 5000)) { - connected = true; - } - - // Try alternative port if main failed - if (!connected && calibreAltPort > 0) { - vTaskDelay(200 / portTICK_PERIOD_MS); - if (tcpClient.connect(calibreHost.c_str(), calibreAltPort, 5000)) { - connected = true; - } - } - - if (connected) { - setState(WirelessState::WAITING); - setStatus("Connected to " + calibreHostname + "\nWaiting for commands..."); - } else { - // Don't set error yet, keep trying discovery - setState(WirelessState::DISCOVERING); - setStatus("Discovering Calibre...\n(Connection failed, retrying)"); - calibrePort = 0; - calibreAltPort = 0; - } - } - } - } -} - -void CalibreWirelessActivity::handleTcpClient() { - if (!tcpClient.connected()) { - setState(WirelessState::DISCONNECTED); - setStatus("Calibre disconnected"); - return; - } - - if (inBinaryMode) { - receiveBinaryData(); - return; - } - - std::string message; - if (readJsonMessage(message)) { - // Parse opcode from JSON array format: [opcode, {...}] - // Find the opcode (first number after '[') - size_t start = message.find('['); - if (start != std::string::npos) { - start++; - size_t end = message.find(',', start); - if (end != std::string::npos) { - const int opcodeInt = std::stoi(message.substr(start, end - start)); - if (opcodeInt < 0 || opcodeInt >= OpCode::ERROR) { - Serial.printf("[%lu] [CAL] Invalid opcode: %d\n", millis(), opcodeInt); - sendJsonResponse(OpCode::OK, "{}"); - return; - } - const auto opcode = static_cast(opcodeInt); - - // Extract data object (everything after the comma until the last ']') - size_t dataStart = end + 1; - size_t dataEnd = message.rfind(']'); - std::string data = ""; - if (dataEnd != std::string::npos && dataEnd > dataStart) { - data = message.substr(dataStart, dataEnd - dataStart); - } - - handleCommand(opcode, data); - } - } - } -} - -bool CalibreWirelessActivity::readJsonMessage(std::string& message) { - // Read available data into buffer - int available = tcpClient.available(); - if (available > 0) { - // Limit buffer growth to prevent memory issues - if (recvBuffer.size() > 100000) { - recvBuffer.clear(); - return false; - } - // Read in chunks - char buf[1024]; - while (available > 0) { - int toRead = std::min(available, static_cast(sizeof(buf))); - int bytesRead = tcpClient.read(reinterpret_cast(buf), toRead); - if (bytesRead > 0) { - recvBuffer.append(buf, bytesRead); - available -= bytesRead; - } else { - break; - } - } - } - - if (recvBuffer.empty()) { - return false; - } - - // Find '[' which marks the start of JSON - size_t bracketPos = recvBuffer.find('['); - if (bracketPos == std::string::npos) { - // No '[' found - if buffer is getting large, something is wrong - if (recvBuffer.size() > 1000) { - recvBuffer.clear(); - } - return false; - } - - // Try to extract length from digits before '[' - // Calibre ALWAYS sends a length prefix, so if it's not valid digits, it's garbage - size_t msgLen = 0; - bool validPrefix = false; - - if (bracketPos > 0 && bracketPos <= 12) { - // Check if prefix is all digits - bool allDigits = true; - for (size_t i = 0; i < bracketPos; i++) { - char c = recvBuffer[i]; - if (c < '0' || c > '9') { - allDigits = false; - break; - } - } - if (allDigits) { - msgLen = std::stoul(recvBuffer.substr(0, bracketPos)); - validPrefix = true; - } - } - - if (!validPrefix) { - // Not a valid length prefix - discard everything up to '[' and treat '[' as start - if (bracketPos > 0) { - recvBuffer = recvBuffer.substr(bracketPos); - } - // Without length prefix, we can't reliably parse - wait for more data - // that hopefully starts with a proper length prefix - return false; - } - - // Sanity check the message length - if (msgLen > 1000000) { - recvBuffer = recvBuffer.substr(bracketPos + 1); // Skip past this '[' and try again - return false; - } - - // Check if we have the complete message - size_t totalNeeded = bracketPos + msgLen; - if (recvBuffer.size() < totalNeeded) { - // Not enough data yet - wait for more - return false; - } - - // Extract the message - message = recvBuffer.substr(bracketPos, msgLen); - - // Keep the rest in buffer (may contain binary data or next message) - if (recvBuffer.size() > totalNeeded) { - recvBuffer = recvBuffer.substr(totalNeeded); - } else { - recvBuffer.clear(); - } - - return true; -} - -void CalibreWirelessActivity::sendJsonResponse(const OpCode opcode, const std::string& data) { - // Format: length + [opcode, {data}] - std::string json = "[" + std::to_string(opcode) + "," + data + "]"; - const std::string lengthPrefix = std::to_string(json.length()); - json.insert(0, lengthPrefix); - - tcpClient.write(reinterpret_cast(json.c_str()), json.length()); - tcpClient.flush(); -} - -void CalibreWirelessActivity::handleCommand(const OpCode opcode, const std::string& data) { - switch (opcode) { - case OpCode::GET_INITIALIZATION_INFO: - handleGetInitializationInfo(data); - break; - case OpCode::GET_DEVICE_INFORMATION: - handleGetDeviceInformation(); - break; - case OpCode::FREE_SPACE: - handleFreeSpace(); - break; - case OpCode::GET_BOOK_COUNT: - handleGetBookCount(); - break; - case OpCode::SEND_BOOK: - handleSendBook(data); - break; - case OpCode::SEND_BOOK_METADATA: - handleSendBookMetadata(data); - break; - case OpCode::DISPLAY_MESSAGE: - handleDisplayMessage(data); - break; - case OpCode::NOOP: - handleNoop(data); - break; - case OpCode::SET_CALIBRE_DEVICE_INFO: - case OpCode::SET_CALIBRE_DEVICE_NAME: - // These set metadata about the connected Calibre instance. - // We don't need this info, just acknowledge receipt. - sendJsonResponse(OpCode::OK, "{}"); - break; - case OpCode::SET_LIBRARY_INFO: - // Library metadata (name, UUID) - not needed for receiving books - sendJsonResponse(OpCode::OK, "{}"); - break; - case OpCode::SEND_BOOKLISTS: - // Calibre asking us to send our book list. We report 0 books in - // handleGetBookCount, so this is effectively a no-op. - sendJsonResponse(OpCode::OK, "{}"); - break; - case OpCode::TOTAL_SPACE: - handleFreeSpace(); - break; - default: - Serial.printf("[%lu] [CAL] Unknown opcode: %d\n", millis(), opcode); - sendJsonResponse(OpCode::OK, "{}"); - break; - } -} - -void CalibreWirelessActivity::handleGetInitializationInfo(const std::string& data) { - setState(WirelessState::WAITING); - setStatus("Connected to " + calibreHostname + - "\nWaiting for transfer...\n\nIf transfer fails, enable\n'Ignore free space' in Calibre's\nSmartDevice " - "plugin settings."); - - // Build response with device capabilities - // Format must match what Calibre expects from a smart device - std::string response = "{"; - response += "\"appName\":\"CrossPoint\","; - response += "\"acceptedExtensions\":[\"epub\"],"; - response += "\"cacheUsesLpaths\":true,"; - response += "\"canAcceptLibraryInfo\":true,"; - response += "\"canDeleteMultipleBooks\":true,"; - response += "\"canReceiveBookBinary\":true,"; - response += "\"canSendOkToSendbook\":true,"; - response += "\"canStreamBooks\":true,"; - response += "\"canStreamMetadata\":true,"; - response += "\"canUseCachedMetadata\":true,"; - // ccVersionNumber: Calibre Companion protocol version. 212 matches CC 5.4.20+. - // Using a known version ensures compatibility with Calibre's feature detection. - response += "\"ccVersionNumber\":212,"; - // coverHeight: Max cover image height. We don't process covers, so this is informational only. - response += "\"coverHeight\":800,"; - response += "\"deviceKind\":\"CrossPoint\","; - response += "\"deviceName\":\"CrossPoint\","; - response += "\"extensionPathLengths\":{\"epub\":37},"; - response += "\"maxBookContentPacketLen\":4096,"; - response += "\"passwordHash\":\"\","; - response += "\"useUuidFileNames\":false,"; - response += "\"versionOK\":true"; - response += "}"; - - sendJsonResponse(OpCode::OK, response); -} - -void CalibreWirelessActivity::handleGetDeviceInformation() { - std::string response = "{"; - response += "\"device_info\":{"; - response += "\"device_store_uuid\":\"" + getDeviceUuid() + "\","; - response += "\"device_name\":\"CrossPoint Reader\","; - response += "\"device_version\":\"" CROSSPOINT_VERSION "\""; - response += "},"; - response += "\"version\":1,"; - response += "\"device_version\":\"" CROSSPOINT_VERSION "\""; - response += "}"; - - sendJsonResponse(OpCode::OK, response); -} - -void CalibreWirelessActivity::handleFreeSpace() { - // TODO: Report actual SD card free space instead of hardcoded value - // Report 10GB free space for now - sendJsonResponse(OpCode::OK, "{\"free_space_on_device\":10737418240}"); -} - -void CalibreWirelessActivity::handleGetBookCount() { - // We report 0 books - Calibre will send books without checking for duplicates - std::string response = "{\"count\":0,\"willStream\":true,\"willScan\":false}"; - sendJsonResponse(OpCode::OK, response); -} - -void CalibreWirelessActivity::handleSendBook(const std::string& data) { - // Manually extract lpath and length from SEND_BOOK data - // Full JSON parsing crashes on large metadata, so we just extract what we need - - // Extract "lpath" field - format: "lpath": "value" - std::string lpath; - size_t lpathPos = data.find("\"lpath\""); - if (lpathPos != std::string::npos) { - size_t colonPos = data.find(':', lpathPos + 7); - if (colonPos != std::string::npos) { - size_t quoteStart = data.find('"', colonPos + 1); - if (quoteStart != std::string::npos) { - size_t quoteEnd = data.find('"', quoteStart + 1); - if (quoteEnd != std::string::npos) { - lpath = data.substr(quoteStart + 1, quoteEnd - quoteStart - 1); - } - } - } - } - - // Extract top-level "length" field - must track depth to skip nested objects - // The metadata contains nested "length" fields (e.g., cover image length) - size_t length = 0; - int depth = 0; - for (size_t i = 0; i < data.size(); i++) { - char c = data[i]; - if (c == '{' || c == '[') { - depth++; - } else if (c == '}' || c == ']') { - depth--; - } else if (depth == 1 && c == '"') { - // At top level, check if this is "length" - if (i + 9 < data.size() && data.substr(i, 8) == "\"length\"") { - // Found top-level "length" - extract the number after ':' - size_t colonPos = data.find(':', i + 8); - if (colonPos != std::string::npos) { - size_t numStart = colonPos + 1; - while (numStart < data.size() && (data[numStart] == ' ' || data[numStart] == '\t')) { - numStart++; - } - size_t numEnd = numStart; - while (numEnd < data.size() && data[numEnd] >= '0' && data[numEnd] <= '9') { - numEnd++; - } - if (numEnd > numStart) { - length = std::stoul(data.substr(numStart, numEnd - numStart)); - break; - } - } - } - } - } - - if (lpath.empty() || length == 0) { - sendJsonResponse(OpCode::ERROR, "{\"message\":\"Invalid book data\"}"); - return; - } - - // Extract filename from lpath - std::string filename = lpath; - const size_t lastSlash = filename.rfind('/'); - if (lastSlash != std::string::npos) { - filename = filename.substr(lastSlash + 1); - } - - // Sanitize and create full path - currentFilename = "/" + StringUtils::sanitizeFilename(filename); - if (!StringUtils::checkFileExtension(currentFilename, ".epub")) { - currentFilename += ".epub"; - } - currentFileSize = length; - bytesReceived = 0; - - setState(WirelessState::RECEIVING); - setStatus("Receiving: " + filename); - - // Open file for writing - if (!SdMan.openFileForWrite("CAL", currentFilename.c_str(), currentFile)) { - setError("Failed to create file"); - sendJsonResponse(OpCode::ERROR, "{\"message\":\"Failed to create file\"}"); - return; - } - - // Send OK to start receiving binary data - sendJsonResponse(OpCode::OK, "{}"); - - // Switch to binary mode - inBinaryMode = true; - binaryBytesRemaining = length; - - // Check if recvBuffer has leftover data (binary file data that arrived with the JSON) - if (!recvBuffer.empty()) { - size_t toWrite = std::min(recvBuffer.size(), binaryBytesRemaining); - size_t written = currentFile.write(reinterpret_cast(recvBuffer.data()), toWrite); - bytesReceived += written; - binaryBytesRemaining -= written; - recvBuffer = recvBuffer.substr(toWrite); - updateRequired = true; - } -} - -void CalibreWirelessActivity::handleSendBookMetadata(const std::string& data) { - // We receive metadata after the book - just acknowledge - sendJsonResponse(OpCode::OK, "{}"); -} - -void CalibreWirelessActivity::handleDisplayMessage(const std::string& data) { - // Calibre may send messages to display - // Check messageKind - 1 means password error - if (data.find("\"messageKind\":1") != std::string::npos) { - setError("Password required"); - } - sendJsonResponse(OpCode::OK, "{}"); -} - -void CalibreWirelessActivity::handleNoop(const std::string& data) { - // Check for ejecting flag - if (data.find("\"ejecting\":true") != std::string::npos) { - setState(WirelessState::DISCONNECTED); - setStatus("Calibre disconnected"); - } - sendJsonResponse(OpCode::NOOP, "{}"); -} - -void CalibreWirelessActivity::receiveBinaryData() { - const int available = tcpClient.available(); - if (available == 0) { - // Check if connection is still alive - if (!tcpClient.connected()) { - currentFile.close(); - inBinaryMode = false; - setError("Transfer interrupted"); - } - return; - } - - uint8_t buffer[1024]; - const size_t toRead = std::min(sizeof(buffer), binaryBytesRemaining); - const size_t bytesRead = tcpClient.read(buffer, toRead); - - if (bytesRead > 0) { - currentFile.write(buffer, bytesRead); - bytesReceived += bytesRead; - binaryBytesRemaining -= bytesRead; - updateRequired = true; - - if (binaryBytesRemaining == 0) { - // Transfer complete - currentFile.flush(); - currentFile.close(); - inBinaryMode = false; - - setState(WirelessState::WAITING); - setStatus("Received: " + currentFilename + "\nWaiting for more..."); - - // Send OK to acknowledge completion - sendJsonResponse(OpCode::OK, "{}"); - } - } -} - -void CalibreWirelessActivity::render() const { - renderer.clearScreen(); - - const auto pageWidth = renderer.getScreenWidth(); - const auto pageHeight = renderer.getScreenHeight(); - - // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 30, "Calibre Wireless", true, EpdFontFamily::BOLD); - - // Draw IP address - const std::string ipAddr = WiFi.localIP().toString().c_str(); - renderer.drawCenteredText(UI_10_FONT_ID, 60, ("IP: " + ipAddr).c_str()); - - // Draw status message - int statusY = pageHeight / 2 - 40; - - // Split status message by newlines and draw each line - std::string status = statusMessage; - size_t pos = 0; - while ((pos = status.find('\n')) != std::string::npos) { - renderer.drawCenteredText(UI_10_FONT_ID, statusY, status.substr(0, pos).c_str()); - statusY += 25; - status = status.substr(pos + 1); - } - if (!status.empty()) { - renderer.drawCenteredText(UI_10_FONT_ID, statusY, status.c_str()); - statusY += 25; - } - - // Draw progress if receiving - if (state == WirelessState::RECEIVING && currentFileSize > 0) { - const int barWidth = pageWidth - 100; - constexpr int barHeight = 20; - constexpr int barX = 50; - const int barY = statusY + 20; - ScreenComponents::drawProgressBar(renderer, barX, barY, barWidth, barHeight, bytesReceived, currentFileSize); - } - - // Draw error if present - if (!errorMessage.empty()) { - renderer.drawCenteredText(UI_10_FONT_ID, pageHeight - 120, errorMessage.c_str()); - } - - // Draw button hints - const auto labels = mappedInput.mapLabels("Back", "", "", ""); - renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); - - renderer.displayBuffer(); -} - -std::string CalibreWirelessActivity::getDeviceUuid() const { - // Generate a consistent UUID based on MAC address - uint8_t mac[6]; - WiFi.macAddress(mac); - - char uuid[37]; - snprintf(uuid, sizeof(uuid), "%02x%02x%02x%02x-%02x%02x-4000-8000-%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], - mac[3], mac[4], mac[5], mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); - - return std::string(uuid); -} - -void CalibreWirelessActivity::setState(WirelessState newState) { - xSemaphoreTake(stateMutex, portMAX_DELAY); - state = newState; - xSemaphoreGive(stateMutex); - updateRequired = true; -} - -void CalibreWirelessActivity::setStatus(const std::string& message) { - statusMessage = message; - updateRequired = true; -} - -void CalibreWirelessActivity::setError(const std::string& message) { - errorMessage = message; - setState(WirelessState::ERROR); -} diff --git a/src/activities/network/CalibreWirelessActivity.h b/src/activities/network/CalibreWirelessActivity.h deleted file mode 100644 index ae2b1767..00000000 --- a/src/activities/network/CalibreWirelessActivity.h +++ /dev/null @@ -1,135 +0,0 @@ -#pragma once -#include -#include -#include -#include -#include -#include - -#include -#include - -#include "activities/Activity.h" - -/** - * CalibreWirelessActivity implements Calibre's "wireless device" protocol. - * This allows Calibre desktop to send books directly to the device over WiFi. - * - * Protocol specification sourced from Calibre's smart device driver: - * https://github.com/kovidgoyal/calibre/blob/master/src/calibre/devices/smart_device_app/driver.py - * - * Protocol overview: - * 1. Device broadcasts "hello" on UDP ports 54982, 48123, 39001, 44044, 59678 - * 2. Calibre responds with its TCP server address - * 3. Device connects to Calibre's TCP server - * 4. Calibre sends JSON commands with length-prefixed messages - * 5. Books are transferred as binary data after SEND_BOOK command - */ -class CalibreWirelessActivity final : public Activity { - // Calibre wireless device states - enum class WirelessState { - DISCOVERING, // Listening for Calibre server broadcasts - CONNECTING, // Establishing TCP connection - WAITING, // Connected, waiting for commands - RECEIVING, // Receiving a book file - COMPLETE, // Transfer complete - DISCONNECTED, // Calibre disconnected - ERROR // Connection/transfer error - }; - - // Calibre protocol opcodes (from calibre/devices/smart_device_app/driver.py) - enum OpCode : uint8_t { - OK = 0, - SET_CALIBRE_DEVICE_INFO = 1, - SET_CALIBRE_DEVICE_NAME = 2, - GET_DEVICE_INFORMATION = 3, - TOTAL_SPACE = 4, - FREE_SPACE = 5, - GET_BOOK_COUNT = 6, - SEND_BOOKLISTS = 7, - SEND_BOOK = 8, - GET_INITIALIZATION_INFO = 9, - BOOK_DONE = 11, - NOOP = 12, // Was incorrectly 18 - DELETE_BOOK = 13, - GET_BOOK_FILE_SEGMENT = 14, - GET_BOOK_METADATA = 15, - SEND_BOOK_METADATA = 16, - DISPLAY_MESSAGE = 17, - CALIBRE_BUSY = 18, - SET_LIBRARY_INFO = 19, - ERROR = 20, - }; - - TaskHandle_t displayTaskHandle = nullptr; - TaskHandle_t networkTaskHandle = nullptr; - SemaphoreHandle_t renderingMutex = nullptr; - SemaphoreHandle_t stateMutex = nullptr; - bool updateRequired = false; - - WirelessState state = WirelessState::DISCOVERING; - const std::function onComplete; - - // UDP discovery - WiFiUDP udp; - - // TCP connection (we connect to Calibre) - WiFiClient tcpClient; - std::string calibreHost; - uint16_t calibrePort = 0; - uint16_t calibreAltPort = 0; // Alternative port (content server) - std::string calibreHostname; - - // Transfer state - std::string currentFilename; - size_t currentFileSize = 0; - size_t bytesReceived = 0; - std::string statusMessage; - std::string errorMessage; - - // Protocol state - bool inBinaryMode = false; - size_t binaryBytesRemaining = 0; - FsFile currentFile; - std::string recvBuffer; // Buffer for incoming data (like KOReader) - - static void displayTaskTrampoline(void* param); - static void networkTaskTrampoline(void* param); - [[noreturn]] void displayTaskLoop(); - [[noreturn]] void networkTaskLoop(); - void render() const; - - // Network operations - void listenForDiscovery(); - void handleTcpClient(); - bool readJsonMessage(std::string& message); - void sendJsonResponse(OpCode opcode, const std::string& data); - void handleCommand(OpCode opcode, const std::string& data); - void receiveBinaryData(); - - // Protocol handlers - void handleGetInitializationInfo(const std::string& data); - void handleGetDeviceInformation(); - void handleFreeSpace(); - void handleGetBookCount(); - void handleSendBook(const std::string& data); - void handleSendBookMetadata(const std::string& data); - void handleDisplayMessage(const std::string& data); - void handleNoop(const std::string& data); - - // Utility - std::string getDeviceUuid() const; - void setState(WirelessState newState); - void setStatus(const std::string& message); - void setError(const std::string& message); - - public: - explicit CalibreWirelessActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, - const std::function& onComplete) - : Activity("CalibreWireless", renderer, mappedInput), onComplete(onComplete) {} - void onEnter() override; - void onExit() override; - void loop() override; - bool preventAutoSleep() override { return true; } - bool skipLoopDelay() override { return true; } -}; diff --git a/src/activities/network/CrossPointWebServerActivity.cpp b/src/activities/network/CrossPointWebServerActivity.cpp index 35ad58ba..c6af1497 100644 --- a/src/activities/network/CrossPointWebServerActivity.cpp +++ b/src/activities/network/CrossPointWebServerActivity.cpp @@ -12,6 +12,7 @@ #include "MappedInputManager.h" #include "NetworkModeSelectionActivity.h" #include "WifiSelectionActivity.h" +#include "activities/network/CalibreConnectActivity.h" #include "fontIds.h" namespace { @@ -125,8 +126,13 @@ void CrossPointWebServerActivity::onExit() { } void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) { - Serial.printf("[%lu] [WEBACT] Network mode selected: %s\n", millis(), - mode == NetworkMode::JOIN_NETWORK ? "Join Network" : "Create Hotspot"); + const char* modeName = "Join Network"; + if (mode == NetworkMode::CONNECT_CALIBRE) { + modeName = "Connect to Calibre"; + } else if (mode == NetworkMode::CREATE_HOTSPOT) { + modeName = "Create Hotspot"; + } + Serial.printf("[%lu] [WEBACT] Network mode selected: %s\n", millis(), modeName); networkMode = mode; isApMode = (mode == NetworkMode::CREATE_HOTSPOT); @@ -134,6 +140,18 @@ void CrossPointWebServerActivity::onNetworkModeSelected(const NetworkMode mode) // Exit mode selection subactivity exitActivity(); + if (mode == NetworkMode::CONNECT_CALIBRE) { + exitActivity(); + enterNewActivity(new CalibreConnectActivity(renderer, mappedInput, [this] { + exitActivity(); + state = WebServerActivityState::MODE_SELECTION; + enterNewActivity(new NetworkModeSelectionActivity( + renderer, mappedInput, [this](const NetworkMode nextMode) { onNetworkModeSelected(nextMode); }, + [this]() { onGoBack(); })); + })); + return; + } + if (mode == NetworkMode::JOIN_NETWORK) { // STA mode - launch WiFi selection Serial.printf("[%lu] [WEBACT] Turning on WiFi (STA mode)...\n", millis()); diff --git a/src/activities/network/CrossPointWebServerActivity.h b/src/activities/network/CrossPointWebServerActivity.h index 775a2474..a1189a57 100644 --- a/src/activities/network/CrossPointWebServerActivity.h +++ b/src/activities/network/CrossPointWebServerActivity.h @@ -23,7 +23,7 @@ enum class WebServerActivityState { /** * CrossPointWebServerActivity is the entry point for file transfer functionality. * It: - * - First presents a choice between "Join a Network" (STA) and "Create Hotspot" (AP) + * - First presents a choice between "Join a Network" (STA), "Connect to Calibre", and "Create Hotspot" (AP) * - For STA mode: Launches WifiSelectionActivity to connect to an existing network * - For AP mode: Creates an Access Point that clients can connect to * - Starts the CrossPointWebServer when connected diff --git a/src/activities/network/NetworkModeSelectionActivity.cpp b/src/activities/network/NetworkModeSelectionActivity.cpp index ad05f5b8..50767084 100644 --- a/src/activities/network/NetworkModeSelectionActivity.cpp +++ b/src/activities/network/NetworkModeSelectionActivity.cpp @@ -6,10 +6,13 @@ #include "fontIds.h" namespace { -constexpr int MENU_ITEM_COUNT = 2; -const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Join a Network", "Create Hotspot"}; -const char* MENU_DESCRIPTIONS[MENU_ITEM_COUNT] = {"Connect to an existing WiFi network", - "Create a WiFi network others can join"}; +constexpr int MENU_ITEM_COUNT = 3; +const char* MENU_ITEMS[MENU_ITEM_COUNT] = {"Join a Network", "Connect to Calibre", "Create Hotspot"}; +const char* MENU_DESCRIPTIONS[MENU_ITEM_COUNT] = { + "Connect to an existing WiFi network", + "Use Calibre wireless device transfers", + "Create a WiFi network others can join", +}; } // namespace void NetworkModeSelectionActivity::taskTrampoline(void* param) { @@ -58,7 +61,12 @@ void NetworkModeSelectionActivity::loop() { // Handle confirm button - select current option if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { - const NetworkMode mode = (selectedIndex == 0) ? NetworkMode::JOIN_NETWORK : NetworkMode::CREATE_HOTSPOT; + NetworkMode mode = NetworkMode::JOIN_NETWORK; + if (selectedIndex == 1) { + mode = NetworkMode::CONNECT_CALIBRE; + } else if (selectedIndex == 2) { + mode = NetworkMode::CREATE_HOTSPOT; + } onModeSelected(mode); return; } diff --git a/src/activities/network/NetworkModeSelectionActivity.h b/src/activities/network/NetworkModeSelectionActivity.h index b9f2e1ee..1b93b825 100644 --- a/src/activities/network/NetworkModeSelectionActivity.h +++ b/src/activities/network/NetworkModeSelectionActivity.h @@ -8,11 +8,12 @@ #include "../Activity.h" // Enum for network mode selection -enum class NetworkMode { JOIN_NETWORK, CREATE_HOTSPOT }; +enum class NetworkMode { JOIN_NETWORK, CONNECT_CALIBRE, CREATE_HOTSPOT }; /** * NetworkModeSelectionActivity presents the user with a choice: * - "Join a Network" - Connect to an existing WiFi network (STA mode) + * - "Connect to Calibre" - Use Calibre wireless device transfers * - "Create Hotspot" - Create an Access Point that others can connect to (AP mode) * * The onModeSelected callback is called with the user's choice. diff --git a/src/activities/settings/CalibreSettingsActivity.cpp b/src/activities/settings/CalibreSettingsActivity.cpp index 4f614ffc..d1df9d0e 100644 --- a/src/activities/settings/CalibreSettingsActivity.cpp +++ b/src/activities/settings/CalibreSettingsActivity.cpp @@ -1,20 +1,17 @@ #include "CalibreSettingsActivity.h" #include -#include #include #include "CrossPointSettings.h" #include "MappedInputManager.h" -#include "activities/network/CalibreWirelessActivity.h" -#include "activities/network/WifiSelectionActivity.h" #include "activities/util/KeyboardEntryActivity.h" #include "fontIds.h" namespace { -constexpr int MENU_ITEMS = 2; -const char* menuNames[MENU_ITEMS] = {"Calibre Web URL", "Connect as Wireless Device"}; +constexpr int MENU_ITEMS = 3; +const char* menuNames[MENU_ITEMS] = {"OPDS Server URL", "Username", "Password"}; } // namespace void CalibreSettingsActivity::taskTrampoline(void* param) { @@ -80,10 +77,10 @@ void CalibreSettingsActivity::handleSelection() { xSemaphoreTake(renderingMutex, portMAX_DELAY); if (selectedIndex == 0) { - // Calibre Web URL + // OPDS Server URL exitActivity(); enterNewActivity(new KeyboardEntryActivity( - renderer, mappedInput, "Calibre Web URL", SETTINGS.opdsServerUrl, 10, + renderer, mappedInput, "OPDS Server URL", SETTINGS.opdsServerUrl, 10, 127, // maxLength false, // not password [this](const std::string& url) { @@ -98,26 +95,41 @@ void CalibreSettingsActivity::handleSelection() { updateRequired = true; })); } else if (selectedIndex == 1) { - // Wireless Device - launch the activity (handles WiFi connection internally) + // Username exitActivity(); - if (WiFi.status() != WL_CONNECTED) { - enterNewActivity(new WifiSelectionActivity(renderer, mappedInput, [this](bool connected) { - exitActivity(); - if (connected) { - enterNewActivity(new CalibreWirelessActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - } else { + enterNewActivity(new KeyboardEntryActivity( + renderer, mappedInput, "Username", SETTINGS.opdsUsername, 10, + 63, // maxLength + false, // not password + [this](const std::string& username) { + strncpy(SETTINGS.opdsUsername, username.c_str(), sizeof(SETTINGS.opdsUsername) - 1); + SETTINGS.opdsUsername[sizeof(SETTINGS.opdsUsername) - 1] = '\0'; + SETTINGS.saveToFile(); + exitActivity(); updateRequired = true; - } - })); - } else { - enterNewActivity(new CalibreWirelessActivity(renderer, mappedInput, [this] { - exitActivity(); - updateRequired = true; - })); - } + }, + [this]() { + exitActivity(); + updateRequired = true; + })); + } else if (selectedIndex == 2) { + // Password + exitActivity(); + enterNewActivity(new KeyboardEntryActivity( + renderer, mappedInput, "Password", SETTINGS.opdsPassword, 10, + 63, // maxLength + false, // not password mode + [this](const std::string& password) { + strncpy(SETTINGS.opdsPassword, password.c_str(), sizeof(SETTINGS.opdsPassword) - 1); + SETTINGS.opdsPassword[sizeof(SETTINGS.opdsPassword) - 1] = '\0'; + SETTINGS.saveToFile(); + exitActivity(); + updateRequired = true; + }, + [this]() { + exitActivity(); + updateRequired = true; + })); } xSemaphoreGive(renderingMutex); @@ -141,24 +153,32 @@ void CalibreSettingsActivity::render() { const auto pageWidth = renderer.getScreenWidth(); // Draw header - renderer.drawCenteredText(UI_12_FONT_ID, 15, "Calibre", true, EpdFontFamily::BOLD); + renderer.drawCenteredText(UI_12_FONT_ID, 15, "OPDS Browser", true, EpdFontFamily::BOLD); + + // Draw info text about Calibre + renderer.drawCenteredText(UI_10_FONT_ID, 40, "For Calibre, add /opds to your URL"); // Draw selection highlight - renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30); + renderer.fillRect(0, 70 + selectedIndex * 30 - 2, pageWidth - 1, 30); // Draw menu items for (int i = 0; i < MENU_ITEMS; i++) { - const int settingY = 60 + i * 30; + const int settingY = 70 + i * 30; const bool isSelected = (i == selectedIndex); renderer.drawText(UI_10_FONT_ID, 20, settingY, menuNames[i], !isSelected); - // Draw status for URL setting + // Draw status for each setting + const char* status = "[Not Set]"; if (i == 0) { - const char* status = (strlen(SETTINGS.opdsServerUrl) > 0) ? "[Set]" : "[Not Set]"; - const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); - renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); + status = (strlen(SETTINGS.opdsServerUrl) > 0) ? "[Set]" : "[Not Set]"; + } else if (i == 1) { + status = (strlen(SETTINGS.opdsUsername) > 0) ? "[Set]" : "[Not Set]"; + } else if (i == 2) { + status = (strlen(SETTINGS.opdsPassword) > 0) ? "[Set]" : "[Not Set]"; } + const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); + renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); } // Draw button hints diff --git a/src/activities/settings/CalibreSettingsActivity.h b/src/activities/settings/CalibreSettingsActivity.h index 77b9218c..49695c62 100644 --- a/src/activities/settings/CalibreSettingsActivity.h +++ b/src/activities/settings/CalibreSettingsActivity.h @@ -8,8 +8,8 @@ #include "activities/ActivityWithSubactivity.h" /** - * Submenu for Calibre settings. - * Shows Calibre Web URL and Calibre Wireless Device options. + * Submenu for OPDS Browser settings. + * Shows OPDS Server URL and HTTP authentication options. */ class CalibreSettingsActivity final : public ActivityWithSubactivity { public: diff --git a/src/activities/settings/CategorySettingsActivity.cpp b/src/activities/settings/CategorySettingsActivity.cpp index a6182b5c..7fd5ef5f 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -103,7 +103,7 @@ void CategorySettingsActivity::toggleCurrentSetting() { updateRequired = true; })); xSemaphoreGive(renderingMutex); - } else if (strcmp(setting.name, "Calibre Settings") == 0) { + } else if (strcmp(setting.name, "OPDS Browser") == 0) { xSemaphoreTake(renderingMutex, portMAX_DELAY); exitActivity(); enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 45b7a12d..819115a5 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -49,7 +49,7 @@ constexpr int systemSettingsCount = 5; const SettingInfo systemSettings[systemSettingsCount] = { SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, {"1 min", "5 min", "10 min", "15 min", "30 min"}), - SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"), + SettingInfo::Action("KOReader Sync"), SettingInfo::Action("OPDS Browser"), SettingInfo::Action("Clear Cache"), SettingInfo::Action("Check for updates")}; } // namespace diff --git a/src/network/CrossPointWebServer.cpp b/src/network/CrossPointWebServer.cpp index 90dfed7b..a135c9f0 100644 --- a/src/network/CrossPointWebServer.cpp +++ b/src/network/CrossPointWebServer.cpp @@ -18,6 +18,8 @@ namespace { // Note: Items starting with "." are automatically hidden const char* HIDDEN_ITEMS[] = {"System Volume Information", "XTCache"}; constexpr size_t HIDDEN_ITEMS_COUNT = sizeof(HIDDEN_ITEMS) / sizeof(HIDDEN_ITEMS[0]); +constexpr uint16_t UDP_PORTS[] = {54982, 48123, 39001, 44044, 59678}; +constexpr uint16_t LOCAL_UDP_PORT = 8134; // Static pointer for WebSocket callback (WebSocketsServer requires C-style callback) CrossPointWebServer* wsInstance = nullptr; @@ -30,6 +32,9 @@ size_t wsUploadSize = 0; size_t wsUploadReceived = 0; unsigned long wsUploadStartTime = 0; bool wsUploadInProgress = false; +String wsLastCompleteName; +size_t wsLastCompleteSize = 0; +unsigned long wsLastCompleteAt = 0; // Helper function to clear epub cache after upload void clearEpubCacheIfNeeded(const String& filePath) { @@ -96,6 +101,7 @@ void CrossPointWebServer::begin() { server->on("/api/status", HTTP_GET, [this] { handleStatus(); }); server->on("/api/files", HTTP_GET, [this] { handleFileListData(); }); + server->on("/download", HTTP_GET, [this] { handleDownload(); }); // Upload endpoint with special handling for multipart form data server->on("/upload", HTTP_POST, [this] { handleUploadPost(); }, [this] { handleUpload(); }); @@ -119,6 +125,10 @@ void CrossPointWebServer::begin() { wsServer->onEvent(wsEventCallback); Serial.printf("[%lu] [WEB] WebSocket server started\n", millis()); + udpActive = udp.begin(LOCAL_UDP_PORT); + Serial.printf("[%lu] [WEB] Discovery UDP %s on port %d\n", millis(), udpActive ? "enabled" : "failed", + LOCAL_UDP_PORT); + running = true; Serial.printf("[%lu] [WEB] Web server started on port %d\n", millis(), port); @@ -156,6 +166,11 @@ void CrossPointWebServer::stop() { Serial.printf("[%lu] [WEB] WebSocket server stopped\n", millis()); } + if (udpActive) { + udp.stop(); + udpActive = false; + } + // Brief delay to allow any in-flight handleClient() calls to complete delay(20); @@ -174,7 +189,7 @@ void CrossPointWebServer::stop() { Serial.printf("[%lu] [WEB] [MEM] Free heap final: %d bytes\n", millis(), ESP.getFreeHeap()); } -void CrossPointWebServer::handleClient() const { +void CrossPointWebServer::handleClient() { static unsigned long lastDebugPrint = 0; // Check running flag FIRST before accessing server @@ -200,6 +215,40 @@ void CrossPointWebServer::handleClient() const { if (wsServer) { wsServer->loop(); } + + // Respond to discovery broadcasts + if (udpActive) { + int packetSize = udp.parsePacket(); + if (packetSize > 0) { + char buffer[16]; + int len = udp.read(buffer, sizeof(buffer) - 1); + if (len > 0) { + buffer[len] = '\0'; + if (strcmp(buffer, "hello") == 0) { + String hostname = WiFi.getHostname(); + if (hostname.isEmpty()) { + hostname = "crosspoint"; + } + String message = "crosspoint (on " + hostname + ");" + String(wsPort); + udp.beginPacket(udp.remoteIP(), udp.remotePort()); + udp.write(reinterpret_cast(message.c_str()), message.length()); + udp.endPacket(); + } + } + } + } +} + +CrossPointWebServer::WsUploadStatus CrossPointWebServer::getWsUploadStatus() const { + WsUploadStatus status; + status.inProgress = wsUploadInProgress; + status.received = wsUploadReceived; + status.total = wsUploadSize; + status.filename = wsUploadFileName.c_str(); + status.lastCompleteName = wsLastCompleteName.c_str(); + status.lastCompleteSize = wsLastCompleteSize; + status.lastCompleteAt = wsLastCompleteAt; + return status; } void CrossPointWebServer::handleRoot() const { @@ -346,6 +395,69 @@ void CrossPointWebServer::handleFileListData() const { Serial.printf("[%lu] [WEB] Served file listing page for path: %s\n", millis(), currentPath.c_str()); } +void CrossPointWebServer::handleDownload() const { + if (!server->hasArg("path")) { + server->send(400, "text/plain", "Missing path"); + return; + } + + String itemPath = server->arg("path"); + if (itemPath.isEmpty() || itemPath == "/") { + server->send(400, "text/plain", "Invalid path"); + return; + } + if (!itemPath.startsWith("/")) { + itemPath = "/" + itemPath; + } + + const String itemName = itemPath.substring(itemPath.lastIndexOf('/') + 1); + if (itemName.startsWith(".")) { + server->send(403, "text/plain", "Cannot access system files"); + return; + } + for (size_t i = 0; i < HIDDEN_ITEMS_COUNT; i++) { + if (itemName.equals(HIDDEN_ITEMS[i])) { + server->send(403, "text/plain", "Cannot access protected items"); + return; + } + } + + if (!SdMan.exists(itemPath.c_str())) { + server->send(404, "text/plain", "Item not found"); + return; + } + + FsFile file = SdMan.open(itemPath.c_str()); + if (!file) { + server->send(500, "text/plain", "Failed to open file"); + return; + } + if (file.isDirectory()) { + file.close(); + server->send(400, "text/plain", "Path is a directory"); + return; + } + + String contentType = "application/octet-stream"; + if (isEpubFile(itemPath)) { + contentType = "application/epub+zip"; + } + + char nameBuf[128] = {0}; + String filename = "download"; + if (file.getName(nameBuf, sizeof(nameBuf))) { + filename = nameBuf; + } + + server->setContentLength(file.size()); + server->sendHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); + server->send(200, contentType.c_str(), ""); + + WiFiClient client = server->client(); + client.write(file); + file.close(); +} + // Static variables for upload handling static FsFile uploadFile; static String uploadFileName; @@ -798,6 +910,10 @@ void CrossPointWebServer::onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* wsUploadFile.close(); wsUploadInProgress = false; + wsLastCompleteName = wsUploadFileName; + wsLastCompleteSize = wsUploadSize; + wsLastCompleteAt = millis(); + unsigned long elapsed = millis() - wsUploadStartTime; float kbps = (elapsed > 0) ? (wsUploadSize / 1024.0) / (elapsed / 1000.0) : 0; diff --git a/src/network/CrossPointWebServer.h b/src/network/CrossPointWebServer.h index ecc2d3d2..36030292 100644 --- a/src/network/CrossPointWebServer.h +++ b/src/network/CrossPointWebServer.h @@ -2,7 +2,10 @@ #include #include +#include +#include +#include #include // Structure to hold file information @@ -15,6 +18,16 @@ struct FileInfo { class CrossPointWebServer { public: + struct WsUploadStatus { + bool inProgress = false; + size_t received = 0; + size_t total = 0; + std::string filename; + std::string lastCompleteName; + size_t lastCompleteSize = 0; + unsigned long lastCompleteAt = 0; + }; + CrossPointWebServer(); ~CrossPointWebServer(); @@ -25,11 +38,13 @@ class CrossPointWebServer { void stop(); // Call this periodically to handle client requests - void handleClient() const; + void handleClient(); // Check if server is running bool isRunning() const { return running; } + WsUploadStatus getWsUploadStatus() const; + // Get the port number uint16_t getPort() const { return port; } @@ -40,6 +55,8 @@ class CrossPointWebServer { bool apMode = false; // true when running in AP mode, false for STA mode uint16_t port = 80; uint16_t wsPort = 81; // WebSocket port + WiFiUDP udp; + bool udpActive = false; // WebSocket upload state void onWebSocketEvent(uint8_t num, WStype_t type, uint8_t* payload, size_t length); @@ -56,6 +73,7 @@ class CrossPointWebServer { void handleStatus() const; void handleFileList() const; void handleFileListData() const; + void handleDownload() const; void handleUpload() const; void handleUploadPost() const; void handleCreateFolder() const; diff --git a/src/network/HttpDownloader.cpp b/src/network/HttpDownloader.cpp index fe65ea6b..b7718c2d 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -5,9 +5,12 @@ #include #include #include +#include +#include #include +#include "CrossPointSettings.h" #include "util/UrlUtils.h" bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) { @@ -28,6 +31,13 @@ bool HttpDownloader::fetchUrl(const std::string& url, Stream& outContent) { http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); + // Add Basic HTTP auth if credentials are configured + if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) { + std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword; + String encoded = base64::encode(credentials.c_str()); + http.addHeader("Authorization", "Basic " + encoded); + } + const int httpCode = http.GET(); if (httpCode != HTTP_CODE_OK) { Serial.printf("[%lu] [HTTP] Fetch failed: %d\n", millis(), httpCode); @@ -72,6 +82,13 @@ HttpDownloader::DownloadError HttpDownloader::downloadToFile(const std::string& http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); + // Add Basic HTTP auth if credentials are configured + if (strlen(SETTINGS.opdsUsername) > 0 && strlen(SETTINGS.opdsPassword) > 0) { + std::string credentials = std::string(SETTINGS.opdsUsername) + ":" + SETTINGS.opdsPassword; + String encoded = base64::encode(credentials.c_str()); + http.addHeader("Authorization", "Basic " + encoded); + } + const int httpCode = http.GET(); if (httpCode != HTTP_CODE_OK) { Serial.printf("[%lu] [HTTP] Download failed: %d\n", millis(), httpCode); From bf6cf83577c0229b3712a20f875fc12f271fbae1 Mon Sep 17 00:00:00 2001 From: Vincent Politzer Date: Tue, 27 Jan 2026 03:07:02 -0800 Subject: [PATCH 13/36] fix: line break (#525) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary * Fixes #519 * Refactors repeated code into new function: `ChapterHtmlSlimParser::flushPartWordBuffer()` ## Additional Context * The `
` tag is self closing and _in-line_, so the existing logic for closing block tags does not get applied to `
` tags. * This PR adds the _in-line_ logic to: * Flush the word preceding the `
` tag from `partWordBuffer` to `currentTextBlock` before calling `startNewTextBlock` * **New function**: `ChapterHtmlSlimParser::flushPartWordBuffer()` * **Purpose**: Consolidates the logic for flushing `partWordBuffer` to `currentTextBlock` * **Impact**: Simplifies `ChapterHtmlSlimParser::characterData(…)`, `ChapterHtmlSlimParser::startElement(…)`, and `ChapterHtmlSlimParser::endElement(…)` by integrating reused code into single function --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 51 +++++++++---------- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h | 1 + 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 1d7e2ab3..53359179 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -40,6 +40,23 @@ bool matches(const char* tag_name, const char* possible_tags[], const int possib return false; } +// flush the contents of partWordBuffer to currentTextBlock +void ChapterHtmlSlimParser::flushPartWordBuffer() { + // determine font style + EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR; + if (boldUntilDepth < depth && italicUntilDepth < depth) { + fontStyle = EpdFontFamily::BOLD_ITALIC; + } else if (boldUntilDepth < depth) { + fontStyle = EpdFontFamily::BOLD; + } else if (italicUntilDepth < depth) { + fontStyle = EpdFontFamily::ITALIC; + } + // flush the buffer + partWordBuffer[partWordBufferIndex] = '\0'; + currentTextBlock->addWord(partWordBuffer, fontStyle); + partWordBufferIndex = 0; +} + // start a new text block if needed void ChapterHtmlSlimParser::startNewTextBlock(const TextBlock::Style style) { if (currentTextBlock) { @@ -125,6 +142,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { if (strcmp(name, "br") == 0) { + if (self->partWordBufferIndex > 0) { + // flush word preceding
to currentTextBlock before calling startNewTextBlock + self->flushPartWordBuffer(); + } self->startNewTextBlock(self->currentTextBlock->getStyle()); } else { self->startNewTextBlock((TextBlock::Style)self->paragraphAlignment); @@ -149,22 +170,11 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char return; } - EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR; - if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) { - fontStyle = EpdFontFamily::BOLD_ITALIC; - } else if (self->boldUntilDepth < self->depth) { - fontStyle = EpdFontFamily::BOLD; - } else if (self->italicUntilDepth < self->depth) { - fontStyle = EpdFontFamily::ITALIC; - } - for (int i = 0; i < len; i++) { if (isWhitespace(s[i])) { // Currently looking at whitespace, if there's anything in the partWordBuffer, flush it if (self->partWordBufferIndex > 0) { - self->partWordBuffer[self->partWordBufferIndex] = '\0'; - self->currentTextBlock->addWord(self->partWordBuffer, fontStyle); - self->partWordBufferIndex = 0; + self->flushPartWordBuffer(); } // Skip the whitespace char continue; @@ -186,9 +196,7 @@ void XMLCALL ChapterHtmlSlimParser::characterData(void* userData, const XML_Char // If we're about to run out of space, then cut the word off and start a new one if (self->partWordBufferIndex >= MAX_WORD_SIZE) { - self->partWordBuffer[self->partWordBufferIndex] = '\0'; - self->currentTextBlock->addWord(self->partWordBuffer, fontStyle); - self->partWordBufferIndex = 0; + self->flushPartWordBuffer(); } self->partWordBuffer[self->partWordBufferIndex++] = s[i]; @@ -219,18 +227,7 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1; if (shouldBreakText) { - EpdFontFamily::Style fontStyle = EpdFontFamily::REGULAR; - if (self->boldUntilDepth < self->depth && self->italicUntilDepth < self->depth) { - fontStyle = EpdFontFamily::BOLD_ITALIC; - } else if (self->boldUntilDepth < self->depth) { - fontStyle = EpdFontFamily::BOLD; - } else if (self->italicUntilDepth < self->depth) { - fontStyle = EpdFontFamily::ITALIC; - } - - self->partWordBuffer[self->partWordBufferIndex] = '\0'; - self->currentTextBlock->addWord(self->partWordBuffer, fontStyle); - self->partWordBufferIndex = 0; + self->flushPartWordBuffer(); } } diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h index 5355211a..2d8ebe5c 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.h @@ -39,6 +39,7 @@ class ChapterHtmlSlimParser { bool hyphenationEnabled; void startNewTextBlock(TextBlock::Style style); + void flushPartWordBuffer(); void makePages(); // XML callbacks static void XMLCALL startElement(void* userData, const XML_Char* name, const XML_Char** atts); From 1b9c8ab5459071abec2ade414f2c7fa42fb60029 Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Tue, 27 Jan 2026 12:07:37 +0100 Subject: [PATCH 14/36] fix: short-press power button to wakeup (#482) ## Summary Fix https://github.com/crosspoint-reader/crosspoint-reader/issues/288 Based on my observation, it seems like the problem was that `inputManager.isPressed(InputManager::BTN_POWER)` takes a bit of time after waking up to report the correct value. I haven't tested this behavior with a standalone ESP32C3, but if you know more about this, feel free to comment. However, if we just want short press, I think it's enough to check for wake up source. If we plan to allow multiple buttons to wake up in the future, may consider using ext1 / `esp_sleep_get_ext1_wakeup_status()` to allow identify which pin triggered wake up. Note that I'm not particularly experienced in esp32 developments, just happen to have prior knowledge hacking esphome. ## Additional Context N/A --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? NO --------- Co-authored-by: Dave Allie --- src/main.cpp | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index c0222e0d..8a081fd8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -151,8 +151,15 @@ void enterNewActivity(Activity* activity) { currentActivity->onEnter(); } -// Verify long press on wake-up from deep sleep -void verifyWakeupLongPress() { +// Verify power button press duration on wake-up from deep sleep +// Pre-condition: isWakeupByPowerButton() == true +void verifyPowerButtonDuration() { + if (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::SLEEP) { + // Fast path for short press + // Needed because inputManager.isPressed() may take up to ~500ms to return the correct state + return; + } + // Give the user up to 1000ms to start holding the power button, and must hold for SETTINGS.getPowerButtonDuration() const auto start = millis(); bool abort = false; @@ -165,6 +172,7 @@ void verifyWakeupLongPress() { inputManager.update(); // Verify the user has actually pressed + // Needed because inputManager.isPressed() may take up to ~500ms to return the correct state while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) { delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration. inputManager.update(); @@ -281,11 +289,14 @@ bool isUsbConnected() { return digitalRead(UART0_RXD) == HIGH; } -bool isWakeupAfterFlashing() { +bool isWakeupByPowerButton() { const auto wakeupCause = esp_sleep_get_wakeup_cause(); const auto resetReason = esp_reset_reason(); - - return isUsbConnected() && (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_UNKNOWN); + if (isUsbConnected()) { + return wakeupCause == ESP_SLEEP_WAKEUP_GPIO; + } else { + return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON); + } } void setup() { @@ -322,9 +333,10 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); - if (!isWakeupAfterFlashing()) { - // For normal wakeups (not immediately after flashing), verify long press - verifyWakeupLongPress(); + if (isWakeupByPowerButton()) { + // For normal wakeups, verify power button press duration + Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); + verifyPowerButtonDuration(); } // First serial output only here to avoid timing inconsistencies for power button press duration verification From 6ca75c4653f5a7db0335c25af1a17ac45bcb6c42 Mon Sep 17 00:00:00 2001 From: GenesiaW <74142392+GenesiaW@users.noreply.github.com> Date: Tue, 27 Jan 2026 19:11:11 +0800 Subject: [PATCH 15/36] fix: goes to relative position when reader settings are changed (#486) ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) * Aims to fix Issue #220 * **What changes are included?** - Increased size of `progress.bin` such that total page count of current section can be stored - Comparison of total page count is done to determine if reader settings were changed - New position/page number is calculated using percentage calculated from read progress ## Additional Context * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- src/activities/reader/EpubReaderActivity.cpp | 26 +++++++++++++++++--- src/activities/reader/EpubReaderActivity.h | 2 ++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index a2e14259..a6d27d34 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -56,12 +56,17 @@ void EpubReaderActivity::onEnter() { FsFile f; if (SdMan.openFileForRead("ERS", epub->getCachePath() + "/progress.bin", f)) { - uint8_t data[4]; - if (f.read(data, 4) == 4) { + uint8_t data[6]; + int dataSize = f.read(data, 6); + if (dataSize == 4 || dataSize == 6) { currentSpineIndex = data[0] + (data[1] << 8); nextPageNumber = data[2] + (data[3] << 8); + cachedSpineIndex = currentSpineIndex; Serial.printf("[%lu] [ERS] Loaded cache: %d, %d\n", millis(), currentSpineIndex, nextPageNumber); } + if (dataSize == 6) { + cachedChapterTotalPageCount = data[4] + (data[5] << 8); + } f.close(); } // We may want a better condition to detect if we are opening for the first time. @@ -341,6 +346,17 @@ void EpubReaderActivity::renderScreen() { } else { section->currentPage = nextPageNumber; } + + // handles changes in reader settings and reset to approximate position based on cached progress + if (cachedChapterTotalPageCount > 0) { + // only goes to relative position if spine index matches cached value + if (currentSpineIndex == cachedSpineIndex && section->pageCount != cachedChapterTotalPageCount) { + float progress = static_cast(section->currentPage) / static_cast(cachedChapterTotalPageCount); + int newPage = static_cast(progress * section->pageCount); + section->currentPage = newPage; + } + cachedChapterTotalPageCount = 0; // resets to 0 to prevent reading cached progress again + } } renderer.clearScreen(); @@ -376,12 +392,14 @@ void EpubReaderActivity::renderScreen() { FsFile f; if (SdMan.openFileForWrite("ERS", epub->getCachePath() + "/progress.bin", f)) { - uint8_t data[4]; + uint8_t data[6]; data[0] = currentSpineIndex & 0xFF; data[1] = (currentSpineIndex >> 8) & 0xFF; data[2] = section->currentPage & 0xFF; data[3] = (section->currentPage >> 8) & 0xFF; - f.write(data, 4); + data[4] = section->pageCount & 0xFF; + data[5] = (section->pageCount >> 8) & 0xFF; + f.write(data, 6); f.close(); } } diff --git a/src/activities/reader/EpubReaderActivity.h b/src/activities/reader/EpubReaderActivity.h index 63d48872..ab4aff2d 100644 --- a/src/activities/reader/EpubReaderActivity.h +++ b/src/activities/reader/EpubReaderActivity.h @@ -15,6 +15,8 @@ class EpubReaderActivity final : public ActivityWithSubactivity { int currentSpineIndex = 0; int nextPageNumber = 0; int pagesUntilFullRefresh = 0; + int cachedSpineIndex = 0; + int cachedChapterTotalPageCount = 0; bool updateRequired = false; const std::function onGoBack; const std::function onGoHome; From aca6dceaa801019dcf59ad968d847f19e58337c6 Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Tue, 27 Jan 2026 12:12:40 +0100 Subject: [PATCH 16/36] fix: Make sure img alt text is treated as separate text block (#497) ## Summary Should address issues discussed in #168 and potentially fix #478. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**PARTIALLY**_ --- lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 53359179..f6d96be4 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -100,7 +100,10 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (atts != nullptr) { for (int i = 0; atts[i]; i += 2) { if (strcmp(atts[i], "alt") == 0) { - alt = "[Image: " + std::string(atts[i + 1]) + "]"; + // add " " (counts as whitespace) at the end of alt + // so the corresponding text block ends. + // TODO: A zero-width breaking space would be more appropriate (once/if we support it) + alt = "[Image: " + std::string(atts[i + 1]) + "] "; } } Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str()); @@ -109,7 +112,7 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* self->italicUntilDepth = min(self->italicUntilDepth, self->depth); self->depth += 1; self->characterData(userData, alt.c_str(), alt.length()); - + return; } else { // Skip for now self->skipUntilDepth = self->depth; From dfd7b615dccfb1a9263c9aea0e20fedbbc443df3 Mon Sep 17 00:00:00 2001 From: Carson Hicks Date: Tue, 27 Jan 2026 03:14:07 -0800 Subject: [PATCH 17/36] fix: Fix KOReader document md5 calculation for binary matching progress sync (#529) ## Summary * **What is the goal of this PR?** Resolve [KoSync progress does not sync between Crosspoint-reader and KOReader (Kindle)](https://github.com/crosspoint-reader/crosspoint-reader/issues/502) * **What changes are included?** KOReaderDocumentId::getOffset() - Update the value for the md5 offset calculation to match KOReader. ## Additional Context I've tested this with a couple of my ebooks and binary matching with KOReader sync seems to be working fine now for both pushing and pulling progress. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- lib/KOReaderSync/KOReaderDocumentId.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/KOReaderSync/KOReaderDocumentId.cpp b/lib/KOReaderSync/KOReaderDocumentId.cpp index 2c52464c..b33beb75 100644 --- a/lib/KOReaderSync/KOReaderDocumentId.cpp +++ b/lib/KOReaderSync/KOReaderDocumentId.cpp @@ -33,10 +33,10 @@ std::string KOReaderDocumentId::calculateFromFilename(const std::string& filePat size_t KOReaderDocumentId::getOffset(int i) { // Offset = 1024 << (2*i) - // For i = -1: 1024 >> 2 = 256 + // For i = -1: KOReader uses a value of 0 // For i >= 0: 1024 << (2*i) if (i < 0) { - return CHUNK_SIZE >> (-2 * i); + return 0; } return CHUNK_SIZE << (2 * i); } From c73fca26f5e45d672f72175dc55d980ea6bfaf42 Mon Sep 17 00:00:00 2001 From: Yaroslav Date: Tue, 27 Jan 2026 14:14:32 +0300 Subject: [PATCH 18/36] docs: Update README with supported languages for EPUB (#530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary - Update README with the list of supported languages for EPUB files. - Update USER_GUIDE with an extended list of supported and unsupported languages. ## Additional Context For weeks, I thought this firmware only supported English, because I remember you saying that full language support would only be possible after implementing proper font rendering. I also remember mentioning a separate Korean fork, Vietnamese issues and so on. All of this made it clear that this system doesn't support my languages. I was surprised when I saw a Reddit post with a photo of a book in my native language. Only then I did learn that such languages ​​are supported. Therefore, mentioning the supported languages ​​would help future buyers and new users. --- ### AI Usage Did you use AI tools to help write this code? _**NO**_ --- README.md | 4 +++- USER_GUIDE.md | 9 +++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d59df835..633ae3b8 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,9 @@ This project is **not affiliated with Xteink**; it's built as a community projec - [ ] Full UTF support - [x] Screen rotation -See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint. +Multi-language support: Read EPUBs in various languages, including English, Spanish, French, German, Italian, Portuguese, Russian, Ukrainian, Polish, Swedish, Norwegian, [and more](./USER_GUIDE.md#supported-languages). + +See [the user guide](./USER_GUIDE.md) for instructions on operating CrossPoint. ## Installing diff --git a/USER_GUIDE.md b/USER_GUIDE.md index f160af74..bdc0f036 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -190,6 +190,15 @@ This feature can be disabled in **[Settings](#35-settings)** to help avoid chang * **Return to Home:** Press and **hold** the **Back** button to close the book and return to the **[Home](#31-home-screen)** screen. * **Chapter Menu:** Press **Confirm** to open the **[Table of Contents/Chapter Selection](#5-chapter-selection-screen)**. +### Supported Languages + +CrossPoint renders text using the following Unicode character blocks, enabling support for a wide range of languages: + +* **Latin Script (Basic, Supplement, Extended-A):** Covers English, German, French, Spanish, Portuguese, Italian, Dutch, Swedish, Norwegian, Danish, Finnish, Polish, Czech, Hungarian, Romanian, Slovak, Slovenian, Turkish, and others. +* **Cyrillic Script (Standard and Extended):** Covers Russian, Ukrainian, Belarusian, Bulgarian, Serbian, Macedonian, Kazakh, Kyrgyz, Mongolian, and others. + +What is not supported: Chinese, Japanese, Korean, Vietnamese, Hebrew, Arabic and Farsi. + --- ## 5. Chapter Selection Screen From a4b9a43ca17b403660de7ae7e22a417675069580 Mon Sep 17 00:00:00 2001 From: Boris Faure Date: Tue, 27 Jan 2026 12:19:19 +0100 Subject: [PATCH 19/36] docs: add font generation commands to builtin font headers (#547) ## Summary * **What is the goal of this PR?** Simple quality of life, ease maintenance * **What changes are included?** Update fontconvert.py to include the command used to generate each font file in the header comment, making it easier to regenerate fonts when needed. I plan on adding options to this scripts (kerning, and maybe ligatures), thus knowing which command was used, even with already existing options like `--additional-intervals`, is important. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- lib/EpdFont/builtinFonts/bookerly_12_bold.h | 1 + lib/EpdFont/builtinFonts/bookerly_12_bolditalic.h | 1 + lib/EpdFont/builtinFonts/bookerly_12_italic.h | 1 + lib/EpdFont/builtinFonts/bookerly_12_regular.h | 1 + lib/EpdFont/builtinFonts/bookerly_14_bold.h | 1 + lib/EpdFont/builtinFonts/bookerly_14_bolditalic.h | 1 + lib/EpdFont/builtinFonts/bookerly_14_italic.h | 1 + lib/EpdFont/builtinFonts/bookerly_14_regular.h | 1 + lib/EpdFont/builtinFonts/bookerly_16_bold.h | 1 + lib/EpdFont/builtinFonts/bookerly_16_bolditalic.h | 1 + lib/EpdFont/builtinFonts/bookerly_16_italic.h | 1 + lib/EpdFont/builtinFonts/bookerly_16_regular.h | 1 + lib/EpdFont/builtinFonts/bookerly_18_bold.h | 1 + lib/EpdFont/builtinFonts/bookerly_18_bolditalic.h | 1 + lib/EpdFont/builtinFonts/bookerly_18_italic.h | 1 + lib/EpdFont/builtinFonts/bookerly_18_regular.h | 1 + lib/EpdFont/builtinFonts/notosans_12_bold.h | 1 + lib/EpdFont/builtinFonts/notosans_12_bolditalic.h | 1 + lib/EpdFont/builtinFonts/notosans_12_italic.h | 1 + lib/EpdFont/builtinFonts/notosans_12_regular.h | 1 + lib/EpdFont/builtinFonts/notosans_14_bold.h | 1 + lib/EpdFont/builtinFonts/notosans_14_bolditalic.h | 1 + lib/EpdFont/builtinFonts/notosans_14_italic.h | 1 + lib/EpdFont/builtinFonts/notosans_14_regular.h | 1 + lib/EpdFont/builtinFonts/notosans_16_bold.h | 1 + lib/EpdFont/builtinFonts/notosans_16_bolditalic.h | 1 + lib/EpdFont/builtinFonts/notosans_16_italic.h | 1 + lib/EpdFont/builtinFonts/notosans_16_regular.h | 1 + lib/EpdFont/builtinFonts/notosans_18_bold.h | 1 + lib/EpdFont/builtinFonts/notosans_18_bolditalic.h | 1 + lib/EpdFont/builtinFonts/notosans_18_italic.h | 1 + lib/EpdFont/builtinFonts/notosans_18_regular.h | 1 + lib/EpdFont/builtinFonts/notosans_8_regular.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_10_bold.h | 1 + .../builtinFonts/opendyslexic_10_bolditalic.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_10_italic.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_10_regular.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_12_bold.h | 1 + .../builtinFonts/opendyslexic_12_bolditalic.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_12_italic.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_12_regular.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_14_bold.h | 1 + .../builtinFonts/opendyslexic_14_bolditalic.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_14_italic.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_14_regular.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_8_bold.h | 1 + .../builtinFonts/opendyslexic_8_bolditalic.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_8_italic.h | 1 + lib/EpdFont/builtinFonts/opendyslexic_8_regular.h | 1 + lib/EpdFont/builtinFonts/ubuntu_10_bold.h | 1 + lib/EpdFont/builtinFonts/ubuntu_10_regular.h | 1 + lib/EpdFont/builtinFonts/ubuntu_12_bold.h | 1 + lib/EpdFont/builtinFonts/ubuntu_12_regular.h | 1 + lib/EpdFont/scripts/fontconvert.py | 14 +++++++++++--- 54 files changed, 64 insertions(+), 3 deletions(-) diff --git a/lib/EpdFont/builtinFonts/bookerly_12_bold.h b/lib/EpdFont/builtinFonts/bookerly_12_bold.h index c20b5742..2dd52ca0 100644 --- a/lib/EpdFont/builtinFonts/bookerly_12_bold.h +++ b/lib/EpdFont/builtinFonts/bookerly_12_bold.h @@ -3,6 +3,7 @@ * name: bookerly_12_bold * size: 12 * mode: 2-bit + * Command used: fontconvert.py bookerly_12_bold 12 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_12_bolditalic.h b/lib/EpdFont/builtinFonts/bookerly_12_bolditalic.h index 6e914f48..32b7510b 100644 --- a/lib/EpdFont/builtinFonts/bookerly_12_bolditalic.h +++ b/lib/EpdFont/builtinFonts/bookerly_12_bolditalic.h @@ -3,6 +3,7 @@ * name: bookerly_12_bolditalic * size: 12 * mode: 2-bit + * Command used: fontconvert.py bookerly_12_bolditalic 12 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_12_italic.h b/lib/EpdFont/builtinFonts/bookerly_12_italic.h index 1fbd43b0..0344d9dc 100644 --- a/lib/EpdFont/builtinFonts/bookerly_12_italic.h +++ b/lib/EpdFont/builtinFonts/bookerly_12_italic.h @@ -3,6 +3,7 @@ * name: bookerly_12_italic * size: 12 * mode: 2-bit + * Command used: fontconvert.py bookerly_12_italic 12 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_12_regular.h b/lib/EpdFont/builtinFonts/bookerly_12_regular.h index 1e788d41..a64cbb61 100644 --- a/lib/EpdFont/builtinFonts/bookerly_12_regular.h +++ b/lib/EpdFont/builtinFonts/bookerly_12_regular.h @@ -3,6 +3,7 @@ * name: bookerly_12_regular * size: 12 * mode: 2-bit + * Command used: fontconvert.py bookerly_12_regular 12 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_14_bold.h b/lib/EpdFont/builtinFonts/bookerly_14_bold.h index 793c6d38..98d280dd 100644 --- a/lib/EpdFont/builtinFonts/bookerly_14_bold.h +++ b/lib/EpdFont/builtinFonts/bookerly_14_bold.h @@ -3,6 +3,7 @@ * name: bookerly_14_bold * size: 14 * mode: 2-bit + * Command used: fontconvert.py bookerly_14_bold 14 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_14_bolditalic.h b/lib/EpdFont/builtinFonts/bookerly_14_bolditalic.h index 60da39be..21b55bfe 100644 --- a/lib/EpdFont/builtinFonts/bookerly_14_bolditalic.h +++ b/lib/EpdFont/builtinFonts/bookerly_14_bolditalic.h @@ -3,6 +3,7 @@ * name: bookerly_14_bolditalic * size: 14 * mode: 2-bit + * Command used: fontconvert.py bookerly_14_bolditalic 14 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_14_italic.h b/lib/EpdFont/builtinFonts/bookerly_14_italic.h index a8d196cb..592d2ed7 100644 --- a/lib/EpdFont/builtinFonts/bookerly_14_italic.h +++ b/lib/EpdFont/builtinFonts/bookerly_14_italic.h @@ -3,6 +3,7 @@ * name: bookerly_14_italic * size: 14 * mode: 2-bit + * Command used: fontconvert.py bookerly_14_italic 14 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_14_regular.h b/lib/EpdFont/builtinFonts/bookerly_14_regular.h index 8c8355fe..b1c77366 100644 --- a/lib/EpdFont/builtinFonts/bookerly_14_regular.h +++ b/lib/EpdFont/builtinFonts/bookerly_14_regular.h @@ -3,6 +3,7 @@ * name: bookerly_14_regular * size: 14 * mode: 2-bit + * Command used: fontconvert.py bookerly_14_regular 14 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_16_bold.h b/lib/EpdFont/builtinFonts/bookerly_16_bold.h index 139d37b1..63a791b2 100644 --- a/lib/EpdFont/builtinFonts/bookerly_16_bold.h +++ b/lib/EpdFont/builtinFonts/bookerly_16_bold.h @@ -3,6 +3,7 @@ * name: bookerly_16_bold * size: 16 * mode: 2-bit + * Command used: fontconvert.py bookerly_16_bold 16 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_16_bolditalic.h b/lib/EpdFont/builtinFonts/bookerly_16_bolditalic.h index c68f1208..46a0bb5a 100644 --- a/lib/EpdFont/builtinFonts/bookerly_16_bolditalic.h +++ b/lib/EpdFont/builtinFonts/bookerly_16_bolditalic.h @@ -3,6 +3,7 @@ * name: bookerly_16_bolditalic * size: 16 * mode: 2-bit + * Command used: fontconvert.py bookerly_16_bolditalic 16 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_16_italic.h b/lib/EpdFont/builtinFonts/bookerly_16_italic.h index bdbb6a65..2d699f61 100644 --- a/lib/EpdFont/builtinFonts/bookerly_16_italic.h +++ b/lib/EpdFont/builtinFonts/bookerly_16_italic.h @@ -3,6 +3,7 @@ * name: bookerly_16_italic * size: 16 * mode: 2-bit + * Command used: fontconvert.py bookerly_16_italic 16 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_16_regular.h b/lib/EpdFont/builtinFonts/bookerly_16_regular.h index c980928e..2948146a 100644 --- a/lib/EpdFont/builtinFonts/bookerly_16_regular.h +++ b/lib/EpdFont/builtinFonts/bookerly_16_regular.h @@ -3,6 +3,7 @@ * name: bookerly_16_regular * size: 16 * mode: 2-bit + * Command used: fontconvert.py bookerly_16_regular 16 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_18_bold.h b/lib/EpdFont/builtinFonts/bookerly_18_bold.h index ca8078bf..e281af85 100644 --- a/lib/EpdFont/builtinFonts/bookerly_18_bold.h +++ b/lib/EpdFont/builtinFonts/bookerly_18_bold.h @@ -3,6 +3,7 @@ * name: bookerly_18_bold * size: 18 * mode: 2-bit + * Command used: fontconvert.py bookerly_18_bold 18 ../builtinFonts/source/Bookerly/Bookerly-Bold.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_18_bolditalic.h b/lib/EpdFont/builtinFonts/bookerly_18_bolditalic.h index 42f46796..4562dc52 100644 --- a/lib/EpdFont/builtinFonts/bookerly_18_bolditalic.h +++ b/lib/EpdFont/builtinFonts/bookerly_18_bolditalic.h @@ -3,6 +3,7 @@ * name: bookerly_18_bolditalic * size: 18 * mode: 2-bit + * Command used: fontconvert.py bookerly_18_bolditalic 18 ../builtinFonts/source/Bookerly/Bookerly-BoldItalic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_18_italic.h b/lib/EpdFont/builtinFonts/bookerly_18_italic.h index 8534b03e..643b5cc1 100644 --- a/lib/EpdFont/builtinFonts/bookerly_18_italic.h +++ b/lib/EpdFont/builtinFonts/bookerly_18_italic.h @@ -3,6 +3,7 @@ * name: bookerly_18_italic * size: 18 * mode: 2-bit + * Command used: fontconvert.py bookerly_18_italic 18 ../builtinFonts/source/Bookerly/Bookerly-Italic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/bookerly_18_regular.h b/lib/EpdFont/builtinFonts/bookerly_18_regular.h index 6d638e65..a6297ea9 100644 --- a/lib/EpdFont/builtinFonts/bookerly_18_regular.h +++ b/lib/EpdFont/builtinFonts/bookerly_18_regular.h @@ -3,6 +3,7 @@ * name: bookerly_18_regular * size: 18 * mode: 2-bit + * Command used: fontconvert.py bookerly_18_regular 18 ../builtinFonts/source/Bookerly/Bookerly-Regular.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_12_bold.h b/lib/EpdFont/builtinFonts/notosans_12_bold.h index 57107166..65ade32a 100644 --- a/lib/EpdFont/builtinFonts/notosans_12_bold.h +++ b/lib/EpdFont/builtinFonts/notosans_12_bold.h @@ -3,6 +3,7 @@ * name: notosans_12_bold * size: 12 * mode: 2-bit + * Command used: fontconvert.py notosans_12_bold 12 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_12_bolditalic.h b/lib/EpdFont/builtinFonts/notosans_12_bolditalic.h index 1b485f7d..6ef7ef4a 100644 --- a/lib/EpdFont/builtinFonts/notosans_12_bolditalic.h +++ b/lib/EpdFont/builtinFonts/notosans_12_bolditalic.h @@ -3,6 +3,7 @@ * name: notosans_12_bolditalic * size: 12 * mode: 2-bit + * Command used: fontconvert.py notosans_12_bolditalic 12 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_12_italic.h b/lib/EpdFont/builtinFonts/notosans_12_italic.h index 994dc40a..a599577f 100644 --- a/lib/EpdFont/builtinFonts/notosans_12_italic.h +++ b/lib/EpdFont/builtinFonts/notosans_12_italic.h @@ -3,6 +3,7 @@ * name: notosans_12_italic * size: 12 * mode: 2-bit + * Command used: fontconvert.py notosans_12_italic 12 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_12_regular.h b/lib/EpdFont/builtinFonts/notosans_12_regular.h index ff47f6fd..a89cb380 100644 --- a/lib/EpdFont/builtinFonts/notosans_12_regular.h +++ b/lib/EpdFont/builtinFonts/notosans_12_regular.h @@ -3,6 +3,7 @@ * name: notosans_12_regular * size: 12 * mode: 2-bit + * Command used: fontconvert.py notosans_12_regular 12 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_14_bold.h b/lib/EpdFont/builtinFonts/notosans_14_bold.h index 1f948b93..70403581 100644 --- a/lib/EpdFont/builtinFonts/notosans_14_bold.h +++ b/lib/EpdFont/builtinFonts/notosans_14_bold.h @@ -3,6 +3,7 @@ * name: notosans_14_bold * size: 14 * mode: 2-bit + * Command used: fontconvert.py notosans_14_bold 14 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_14_bolditalic.h b/lib/EpdFont/builtinFonts/notosans_14_bolditalic.h index f75fa527..f4168354 100644 --- a/lib/EpdFont/builtinFonts/notosans_14_bolditalic.h +++ b/lib/EpdFont/builtinFonts/notosans_14_bolditalic.h @@ -3,6 +3,7 @@ * name: notosans_14_bolditalic * size: 14 * mode: 2-bit + * Command used: fontconvert.py notosans_14_bolditalic 14 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_14_italic.h b/lib/EpdFont/builtinFonts/notosans_14_italic.h index d7d00a53..18cc49e0 100644 --- a/lib/EpdFont/builtinFonts/notosans_14_italic.h +++ b/lib/EpdFont/builtinFonts/notosans_14_italic.h @@ -3,6 +3,7 @@ * name: notosans_14_italic * size: 14 * mode: 2-bit + * Command used: fontconvert.py notosans_14_italic 14 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_14_regular.h b/lib/EpdFont/builtinFonts/notosans_14_regular.h index f93afddc..a8f7fbba 100644 --- a/lib/EpdFont/builtinFonts/notosans_14_regular.h +++ b/lib/EpdFont/builtinFonts/notosans_14_regular.h @@ -3,6 +3,7 @@ * name: notosans_14_regular * size: 14 * mode: 2-bit + * Command used: fontconvert.py notosans_14_regular 14 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_16_bold.h b/lib/EpdFont/builtinFonts/notosans_16_bold.h index b6a0414a..4e346852 100644 --- a/lib/EpdFont/builtinFonts/notosans_16_bold.h +++ b/lib/EpdFont/builtinFonts/notosans_16_bold.h @@ -3,6 +3,7 @@ * name: notosans_16_bold * size: 16 * mode: 2-bit + * Command used: fontconvert.py notosans_16_bold 16 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_16_bolditalic.h b/lib/EpdFont/builtinFonts/notosans_16_bolditalic.h index 8452a245..8c5bc3e5 100644 --- a/lib/EpdFont/builtinFonts/notosans_16_bolditalic.h +++ b/lib/EpdFont/builtinFonts/notosans_16_bolditalic.h @@ -3,6 +3,7 @@ * name: notosans_16_bolditalic * size: 16 * mode: 2-bit + * Command used: fontconvert.py notosans_16_bolditalic 16 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_16_italic.h b/lib/EpdFont/builtinFonts/notosans_16_italic.h index d1a0cac5..e129f3ed 100644 --- a/lib/EpdFont/builtinFonts/notosans_16_italic.h +++ b/lib/EpdFont/builtinFonts/notosans_16_italic.h @@ -3,6 +3,7 @@ * name: notosans_16_italic * size: 16 * mode: 2-bit + * Command used: fontconvert.py notosans_16_italic 16 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_16_regular.h b/lib/EpdFont/builtinFonts/notosans_16_regular.h index 24398196..f07dc566 100644 --- a/lib/EpdFont/builtinFonts/notosans_16_regular.h +++ b/lib/EpdFont/builtinFonts/notosans_16_regular.h @@ -3,6 +3,7 @@ * name: notosans_16_regular * size: 16 * mode: 2-bit + * Command used: fontconvert.py notosans_16_regular 16 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_18_bold.h b/lib/EpdFont/builtinFonts/notosans_18_bold.h index cb57a3bf..e2eb5799 100644 --- a/lib/EpdFont/builtinFonts/notosans_18_bold.h +++ b/lib/EpdFont/builtinFonts/notosans_18_bold.h @@ -3,6 +3,7 @@ * name: notosans_18_bold * size: 18 * mode: 2-bit + * Command used: fontconvert.py notosans_18_bold 18 ../builtinFonts/source/NotoSans/NotoSans-Bold.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_18_bolditalic.h b/lib/EpdFont/builtinFonts/notosans_18_bolditalic.h index bd09ce14..465d847f 100644 --- a/lib/EpdFont/builtinFonts/notosans_18_bolditalic.h +++ b/lib/EpdFont/builtinFonts/notosans_18_bolditalic.h @@ -3,6 +3,7 @@ * name: notosans_18_bolditalic * size: 18 * mode: 2-bit + * Command used: fontconvert.py notosans_18_bolditalic 18 ../builtinFonts/source/NotoSans/NotoSans-BoldItalic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_18_italic.h b/lib/EpdFont/builtinFonts/notosans_18_italic.h index 926bd32e..0e36e189 100644 --- a/lib/EpdFont/builtinFonts/notosans_18_italic.h +++ b/lib/EpdFont/builtinFonts/notosans_18_italic.h @@ -3,6 +3,7 @@ * name: notosans_18_italic * size: 18 * mode: 2-bit + * Command used: fontconvert.py notosans_18_italic 18 ../builtinFonts/source/NotoSans/NotoSans-Italic.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_18_regular.h b/lib/EpdFont/builtinFonts/notosans_18_regular.h index d8bbe9c7..029ff804 100644 --- a/lib/EpdFont/builtinFonts/notosans_18_regular.h +++ b/lib/EpdFont/builtinFonts/notosans_18_regular.h @@ -3,6 +3,7 @@ * name: notosans_18_regular * size: 18 * mode: 2-bit + * Command used: fontconvert.py notosans_18_regular 18 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/notosans_8_regular.h b/lib/EpdFont/builtinFonts/notosans_8_regular.h index 0c01edcc..7e339184 100644 --- a/lib/EpdFont/builtinFonts/notosans_8_regular.h +++ b/lib/EpdFont/builtinFonts/notosans_8_regular.h @@ -3,6 +3,7 @@ * name: notosans_8_regular * size: 8 * mode: 1-bit + * Command used: fontconvert.py notosans_8_regular 8 ../builtinFonts/source/NotoSans/NotoSans-Regular.ttf */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_10_bold.h b/lib/EpdFont/builtinFonts/opendyslexic_10_bold.h index eb900628..b3a16e6e 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_10_bold.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_10_bold.h @@ -3,6 +3,7 @@ * name: opendyslexic_10_bold * size: 10 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_10_bold 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_10_bolditalic.h b/lib/EpdFont/builtinFonts/opendyslexic_10_bolditalic.h index f2a45714..e939db2d 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_10_bolditalic.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_10_bolditalic.h @@ -3,6 +3,7 @@ * name: opendyslexic_10_bolditalic * size: 10 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_10_bolditalic 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_10_italic.h b/lib/EpdFont/builtinFonts/opendyslexic_10_italic.h index 2e9f4127..e0f43bb1 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_10_italic.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_10_italic.h @@ -3,6 +3,7 @@ * name: opendyslexic_10_italic * size: 10 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_10_italic 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_10_regular.h b/lib/EpdFont/builtinFonts/opendyslexic_10_regular.h index 928d7526..0fded271 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_10_regular.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_10_regular.h @@ -3,6 +3,7 @@ * name: opendyslexic_10_regular * size: 10 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_10_regular 10 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_12_bold.h b/lib/EpdFont/builtinFonts/opendyslexic_12_bold.h index 5f7c8ecc..115a737c 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_12_bold.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_12_bold.h @@ -3,6 +3,7 @@ * name: opendyslexic_12_bold * size: 12 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_12_bold 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_12_bolditalic.h b/lib/EpdFont/builtinFonts/opendyslexic_12_bolditalic.h index fdb3f63b..54732e1a 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_12_bolditalic.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_12_bolditalic.h @@ -3,6 +3,7 @@ * name: opendyslexic_12_bolditalic * size: 12 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_12_bolditalic 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_12_italic.h b/lib/EpdFont/builtinFonts/opendyslexic_12_italic.h index 4ce9eed8..d927f96c 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_12_italic.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_12_italic.h @@ -3,6 +3,7 @@ * name: opendyslexic_12_italic * size: 12 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_12_italic 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_12_regular.h b/lib/EpdFont/builtinFonts/opendyslexic_12_regular.h index 596ee1ec..61643c60 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_12_regular.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_12_regular.h @@ -3,6 +3,7 @@ * name: opendyslexic_12_regular * size: 12 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_12_regular 12 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_14_bold.h b/lib/EpdFont/builtinFonts/opendyslexic_14_bold.h index b5de40b6..e150dbd3 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_14_bold.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_14_bold.h @@ -3,6 +3,7 @@ * name: opendyslexic_14_bold * size: 14 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_14_bold 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_14_bolditalic.h b/lib/EpdFont/builtinFonts/opendyslexic_14_bolditalic.h index dae158ca..9aa5e19d 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_14_bolditalic.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_14_bolditalic.h @@ -3,6 +3,7 @@ * name: opendyslexic_14_bolditalic * size: 14 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_14_bolditalic 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_14_italic.h b/lib/EpdFont/builtinFonts/opendyslexic_14_italic.h index d69b842a..06fd04d4 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_14_italic.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_14_italic.h @@ -3,6 +3,7 @@ * name: opendyslexic_14_italic * size: 14 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_14_italic 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_14_regular.h b/lib/EpdFont/builtinFonts/opendyslexic_14_regular.h index f45e71ae..cda4f876 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_14_regular.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_14_regular.h @@ -3,6 +3,7 @@ * name: opendyslexic_14_regular * size: 14 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_14_regular 14 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_8_bold.h b/lib/EpdFont/builtinFonts/opendyslexic_8_bold.h index b0fc804c..72e131d8 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_8_bold.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_8_bold.h @@ -3,6 +3,7 @@ * name: opendyslexic_8_bold * size: 8 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_8_bold 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Bold.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_8_bolditalic.h b/lib/EpdFont/builtinFonts/opendyslexic_8_bolditalic.h index 77336edf..4858ad08 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_8_bolditalic.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_8_bolditalic.h @@ -3,6 +3,7 @@ * name: opendyslexic_8_bolditalic * size: 8 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_8_bolditalic 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-BoldItalic.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_8_italic.h b/lib/EpdFont/builtinFonts/opendyslexic_8_italic.h index 37dcfa99..62e37b32 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_8_italic.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_8_italic.h @@ -3,6 +3,7 @@ * name: opendyslexic_8_italic * size: 8 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_8_italic 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Italic.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/opendyslexic_8_regular.h b/lib/EpdFont/builtinFonts/opendyslexic_8_regular.h index f68c7438..fae287a5 100644 --- a/lib/EpdFont/builtinFonts/opendyslexic_8_regular.h +++ b/lib/EpdFont/builtinFonts/opendyslexic_8_regular.h @@ -3,6 +3,7 @@ * name: opendyslexic_8_regular * size: 8 * mode: 2-bit + * Command used: fontconvert.py opendyslexic_8_regular 8 ../builtinFonts/source/OpenDyslexic/OpenDyslexic-Regular.otf --2bit */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/ubuntu_10_bold.h b/lib/EpdFont/builtinFonts/ubuntu_10_bold.h index cab81b10..80032fd8 100644 --- a/lib/EpdFont/builtinFonts/ubuntu_10_bold.h +++ b/lib/EpdFont/builtinFonts/ubuntu_10_bold.h @@ -3,6 +3,7 @@ * name: ubuntu_10_bold * size: 10 * mode: 1-bit + * Command used: fontconvert.py ubuntu_10_bold 10 ../builtinFonts/source/Ubuntu/Ubuntu-Bold.ttf */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/ubuntu_10_regular.h b/lib/EpdFont/builtinFonts/ubuntu_10_regular.h index a7292c19..e76ab2c0 100644 --- a/lib/EpdFont/builtinFonts/ubuntu_10_regular.h +++ b/lib/EpdFont/builtinFonts/ubuntu_10_regular.h @@ -3,6 +3,7 @@ * name: ubuntu_10_regular * size: 10 * mode: 1-bit + * Command used: fontconvert.py ubuntu_10_regular 10 ../builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/ubuntu_12_bold.h b/lib/EpdFont/builtinFonts/ubuntu_12_bold.h index 9419ed4b..5b24d067 100644 --- a/lib/EpdFont/builtinFonts/ubuntu_12_bold.h +++ b/lib/EpdFont/builtinFonts/ubuntu_12_bold.h @@ -3,6 +3,7 @@ * name: ubuntu_12_bold * size: 12 * mode: 1-bit + * Command used: fontconvert.py ubuntu_12_bold 12 ../builtinFonts/source/Ubuntu/Ubuntu-Bold.ttf */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/builtinFonts/ubuntu_12_regular.h b/lib/EpdFont/builtinFonts/ubuntu_12_regular.h index f02de88c..23ddbe78 100644 --- a/lib/EpdFont/builtinFonts/ubuntu_12_regular.h +++ b/lib/EpdFont/builtinFonts/ubuntu_12_regular.h @@ -3,6 +3,7 @@ * name: ubuntu_12_regular * size: 12 * mode: 1-bit + * Command used: fontconvert.py ubuntu_12_regular 12 ../builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf */ #pragma once #include "EpdFontData.h" diff --git a/lib/EpdFont/scripts/fontconvert.py b/lib/EpdFont/scripts/fontconvert.py index d11f73b7..ba7a44af 100755 --- a/lib/EpdFont/scripts/fontconvert.py +++ b/lib/EpdFont/scripts/fontconvert.py @@ -270,9 +270,17 @@ for index, glyph in enumerate(all_glyphs): glyph_data.extend([b for b in packed]) glyph_props.append(props) -print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * mode: {'2-bit' if is2Bit else '1-bit'}\n */") -print("#pragma once") -print("#include \"EpdFontData.h\"\n") +print(f"""/** + * generated by fontconvert.py + * name: {font_name} + * size: {size} + * mode: {'2-bit' if is2Bit else '1-bit'} + * Command used: {' '.join(sys.argv)} + */ +#pragma once +#include "EpdFontData.h" +""") + print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{") for c in chunks(glyph_data, 16): print (" " + " ".join(f"0x{b:02X}," for b in c)) From e2ca0e94ca827a96cc0e88fad44cc7b3ab5b5864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=95=D0=B3=D0=BE=D1=80=20=D0=9C=D0=B0=D1=80=D1=82=D1=8B?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2?= Date: Tue, 27 Jan 2026 18:53:31 +0700 Subject: [PATCH 20/36] fix: add txt books to recent tab (#526) Fixes #512 --- ### AI Usage _**NO**_ --- src/activities/reader/TxtReaderActivity.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index db725320..b7de16d8 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -8,6 +8,7 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" #include "MappedInputManager.h" +#include "RecentBooksStore.h" #include "ScreenComponents.h" #include "fontIds.h" @@ -55,9 +56,10 @@ void TxtReaderActivity::onEnter() { txt->setupCacheDir(); - // Save current txt as last opened file + // Save current txt as last opened file and add to recent books APP_STATE.openEpubPath = txt->getPath(); APP_STATE.saveToFile(); + RECENT_BOOKS.addBook(txt->getPath()); // Trigger first update updateRequired = true; From 5e24895f6d0f6c4687f13f3c1067b511c5da6fee Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Tue, 27 Jan 2026 22:56:51 +1100 Subject: [PATCH 21/36] feat: Extract author from XTC/XTCH files (#563) ## Summary * Extract author from XTC/XTCH files ## Additional Context * Based on updated details in https://gist.github.com/CrazyCoder/b125f26d6987c0620058249f59f1327d --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? No --- lib/Xtc/Xtc.cpp | 10 ++++++- lib/Xtc/Xtc.h | 1 + lib/Xtc/Xtc/XtcParser.cpp | 43 +++++++++++++++++++++------- lib/Xtc/Xtc/XtcParser.h | 5 +++- lib/Xtc/Xtc/XtcTypes.h | 14 +++++---- src/activities/home/HomeActivity.cpp | 3 ++ 6 files changed, 58 insertions(+), 18 deletions(-) diff --git a/lib/Xtc/Xtc.cpp b/lib/Xtc/Xtc.cpp index c79421d7..7850d934 100644 --- a/lib/Xtc/Xtc.cpp +++ b/lib/Xtc/Xtc.cpp @@ -7,7 +7,6 @@ #include "Xtc.h" -#include #include #include @@ -87,6 +86,15 @@ std::string Xtc::getTitle() const { return filepath.substr(lastSlash, lastDot - lastSlash); } +std::string Xtc::getAuthor() const { + if (!loaded || !parser) { + return ""; + } + + // Try to get author from XTC metadata + return parser->getAuthor(); +} + bool Xtc::hasChapters() const { if (!loaded || !parser) { return false; diff --git a/lib/Xtc/Xtc.h b/lib/Xtc/Xtc.h index 7413ef47..c8d9a040 100644 --- a/lib/Xtc/Xtc.h +++ b/lib/Xtc/Xtc.h @@ -56,6 +56,7 @@ class Xtc { // Metadata std::string getTitle() const; + std::string getAuthor() const; bool hasChapters() const; const std::vector& getChapters() const; diff --git a/lib/Xtc/Xtc/XtcParser.cpp b/lib/Xtc/Xtc/XtcParser.cpp index c33e7193..8db3dead 100644 --- a/lib/Xtc/Xtc/XtcParser.cpp +++ b/lib/Xtc/Xtc/XtcParser.cpp @@ -47,8 +47,21 @@ XtcError XtcParser::open(const char* filepath) { return m_lastError; } - // Read title if available - readTitle(); + // Read title & author if available + if (m_header.hasMetadata) { + m_lastError = readTitle(); + if (m_lastError != XtcError::OK) { + Serial.printf("[%lu] [XTC] Failed to read title: %s\n", millis(), errorToString(m_lastError)); + m_file.close(); + return m_lastError; + } + m_lastError = readAuthor(); + if (m_lastError != XtcError::OK) { + Serial.printf("[%lu] [XTC] Failed to read author: %s\n", millis(), errorToString(m_lastError)); + m_file.close(); + return m_lastError; + } + } // Read page table m_lastError = readPageTable(); @@ -124,24 +137,34 @@ XtcError XtcParser::readHeader() { } XtcError XtcParser::readTitle() { - // Title is usually at offset 0x38 (56) for 88-byte headers - // Read title as null-terminated UTF-8 string - if (m_header.titleOffset == 0) { - m_header.titleOffset = 0x38; // Default offset - } - - if (!m_file.seek(m_header.titleOffset)) { + constexpr auto titleOffset = 0x38; + if (!m_file.seek(titleOffset)) { return XtcError::READ_ERROR; } char titleBuf[128] = {0}; - m_file.read(reinterpret_cast(titleBuf), sizeof(titleBuf) - 1); + m_file.read(titleBuf, sizeof(titleBuf) - 1); m_title = titleBuf; Serial.printf("[%lu] [XTC] Title: %s\n", millis(), m_title.c_str()); return XtcError::OK; } +XtcError XtcParser::readAuthor() { + // Read author as null-terminated UTF-8 string with max length 64, directly following title + constexpr auto authorOffset = 0xB8; + if (!m_file.seek(authorOffset)) { + return XtcError::READ_ERROR; + } + + char authorBuf[64] = {0}; + m_file.read(authorBuf, sizeof(authorBuf) - 1); + m_author = authorBuf; + + Serial.printf("[%lu] [XTC] Author: %s\n", millis(), m_author.c_str()); + return XtcError::OK; +} + XtcError XtcParser::readPageTable() { if (m_header.pageTableOffset == 0) { Serial.printf("[%lu] [XTC] Page table offset is 0, cannot read\n", millis()); diff --git a/lib/Xtc/Xtc/XtcParser.h b/lib/Xtc/Xtc/XtcParser.h index 2d2b780e..b0033542 100644 --- a/lib/Xtc/Xtc/XtcParser.h +++ b/lib/Xtc/Xtc/XtcParser.h @@ -67,8 +67,9 @@ class XtcParser { std::function callback, size_t chunkSize = 1024); - // Get title from metadata + // Get title/author from metadata std::string getTitle() const { return m_title; } + std::string getAuthor() const { return m_author; } bool hasChapters() const { return m_hasChapters; } const std::vector& getChapters() const { return m_chapters; } @@ -86,6 +87,7 @@ class XtcParser { std::vector m_pageTable; std::vector m_chapters; std::string m_title; + std::string m_author; uint16_t m_defaultWidth; uint16_t m_defaultHeight; uint8_t m_bitDepth; // 1 = XTC/XTG (1-bit), 2 = XTCH/XTH (2-bit) @@ -96,6 +98,7 @@ class XtcParser { XtcError readHeader(); XtcError readPageTable(); XtcError readTitle(); + XtcError readAuthor(); XtcError readChapters(); }; diff --git a/lib/Xtc/Xtc/XtcTypes.h b/lib/Xtc/Xtc/XtcTypes.h index 08f9c00b..773c7ad5 100644 --- a/lib/Xtc/Xtc/XtcTypes.h +++ b/lib/Xtc/Xtc/XtcTypes.h @@ -38,14 +38,16 @@ struct XtcHeader { uint8_t versionMajor; // 0x04: Format version major (typically 1) (together with minor = 1.0) uint8_t versionMinor; // 0x05: Format version minor (typically 0) uint16_t pageCount; // 0x06: Total page count - uint32_t flags; // 0x08: Flags/reserved - uint32_t headerSize; // 0x0C: Size of header section (typically 88) - uint32_t reserved1; // 0x10: Reserved - uint32_t tocOffset; // 0x14: TOC offset (0 if unused) - 4 bytes, not 8! + uint8_t readDirection; // 0x08: Reading direction (0-2) + uint8_t hasMetadata; // 0x09: Has metadata (0-1) + uint8_t hasThumbnails; // 0x0A: Has thumbnails (0-1) + uint8_t hasChapters; // 0x0B: Has chapters (0-1) + uint32_t currentPage; // 0x0C: Current page (1-based) (0-65535) + uint64_t metadataOffset; // 0x10: Metadata offset (0 if unused) uint64_t pageTableOffset; // 0x18: Page table offset uint64_t dataOffset; // 0x20: First page data offset - uint64_t reserved2; // 0x28: Reserved - uint32_t titleOffset; // 0x30: Title string offset + uint64_t thumbOffset; // 0x28: Thumbnail offset + uint32_t chapterOffset; // 0x30: Chapter data offset uint32_t padding; // 0x34: Padding to 56 bytes }; #pragma pack(pop) diff --git a/src/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index 3389e80d..58b29505 100644 --- a/src/activities/home/HomeActivity.cpp +++ b/src/activities/home/HomeActivity.cpp @@ -71,6 +71,9 @@ void HomeActivity::onEnter() { if (!xtc.getTitle().empty()) { lastBookTitle = std::string(xtc.getTitle()); } + if (!xtc.getAuthor().empty()) { + lastBookAuthor = std::string(xtc.getAuthor()); + } // Try to generate thumbnail image for Continue Reading card if (xtc.generateThumbBmp()) { coverBmpPath = xtc.getThumbBmpPath(); From 51c5c3c0aa665f0852bd8bda42a5d4aa44ae4add Mon Sep 17 00:00:00 2001 From: Maeve Andrews <37351465+maeveynot@users.noreply.github.com> Date: Tue, 27 Jan 2026 05:59:41 -0600 Subject: [PATCH 22/36] fix: rotate origin in drawImage (#557) ## Summary This was originally a comment in #499, but I'm making it its own PR, because it doesn't depend on anything there and then I can base that PR on this one. Currently, `drawBitmap` is used for covers and sleep wallpaper, and `drawImage` is used for the boot logo. `drawBitmap` goes row by row and pixel by pixel, so it respects the renderer orientation. `drawImage` just calls the `EInkDisplay`'s `drawImage`, which works in the eink panel's native display orientation. `drawImage` rotates the x,y coordinates where it's going to draw the image, but doesn't account for the fact that the northwest corner in portrait orientation becomes, the southwest corner of the image rectangle in the native orientation. The boot and sleep activities currently work around this by calculating the north*east* corner of where the image should go, which becomes the northwest corner after `rotateCoordinates`. I think this wasn't really apparent because the CrossPoint logo is rotationally symmetrical. The `EInkDisplay` `drawImage` always draws the image in native orientation, but that looks the same for the "X" image. If we rotate the origin coordinate in `GfxRenderer`'s `drawImage`, we can use a much clearer northwest corner coordinate in the boot and sleep activities. (And then, in #499, we can actually rotate the boot screen to the user's preferred orientation). This does *not* yet rotate the actual bits in the image; it's still displayed in native orientation. This doesn't affect the rotationally-symmetric logo, but if it's ever changed, we will probably want to allocate a new `u8int[]` and transpose rows and columns if necessary. ## Additional Context I've created an additional branch on top of this to demonstrate by replacing the logo with a non-rotationally-symmetrical image: Cat-in-a-pan-128-bw https://github.com/crosspoint-reader/crosspoint-reader/compare/master...maeveynot:rotated-cat (many thanks to https://notisrac.github.io/FileToCArray/) As you can see, it is always drawn in native orientation, which makes it sideways (turned clockwise) in portrait. --- ### AI Usage No Co-authored-by: Maeve Andrews --- lib/GfxRenderer/GfxRenderer.cpp | 17 ++++++++++++++++- src/activities/boot_sleep/BootActivity.cpp | 2 +- src/activities/boot_sleep/SleepActivity.cpp | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 08420bf9..1dbe8ee6 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -145,10 +145,25 @@ void GfxRenderer::fillRect(const int x, const int y, const int width, const int } void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, const int width, const int height) const { - // TODO: Rotate bits int rotatedX = 0; int rotatedY = 0; rotateCoordinates(x, y, &rotatedX, &rotatedY); + // Rotate origin corner + switch (orientation) { + case Portrait: + rotatedY = rotatedY - height; + break; + case PortraitInverted: + rotatedX = rotatedX - width; + break; + case LandscapeClockwise: + rotatedY = rotatedY - height; + rotatedX = rotatedX - width; + break; + case LandscapeCounterClockwise: + break; + } + // TODO: Rotate bits einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); } diff --git a/src/activities/boot_sleep/BootActivity.cpp b/src/activities/boot_sleep/BootActivity.cpp index 65eb6a07..b741c3e3 100644 --- a/src/activities/boot_sleep/BootActivity.cpp +++ b/src/activities/boot_sleep/BootActivity.cpp @@ -12,7 +12,7 @@ void BootActivity::onEnter() { const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128); + renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION); diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 40341e5f..c4b98968 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -124,7 +124,7 @@ void SleepActivity::renderDefaultSleepScreen() const { const auto pageHeight = renderer.getScreenHeight(); renderer.clearScreen(); - renderer.drawImage(CrossLarge, (pageWidth + 128) / 2, (pageHeight - 128) / 2, 128, 128); + renderer.drawImage(CrossLarge, (pageWidth - 128) / 2, (pageHeight - 128) / 2, 128, 128); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 70, "CrossPoint", true, EpdFontFamily::BOLD); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING"); From dd1741bf0b53ce7abf9fc8c50106bf2c7819fd0c Mon Sep 17 00:00:00 2001 From: Jonas Diemer Date: Tue, 27 Jan 2026 13:08:58 +0100 Subject: [PATCH 23/36] fix: Validate settings on read. (#492) ## Summary Fixes #487 --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _** YES **_ Co-authored-by: Dave Allie --- src/CrossPointSettings.cpp | 36 ++++++++++++++---------- src/CrossPointSettings.h | 57 ++++++++++++++++++++++++++------------ 2 files changed, 61 insertions(+), 32 deletions(-) diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index ea26ad91..f3a7a524 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -11,6 +11,14 @@ // Initialize the static instance CrossPointSettings CrossPointSettings::instance; +void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { + uint8_t tempValue; + serialization::readPod(file, tempValue); + if (tempValue < maxValue) { + member = tempValue; + } +} + namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields @@ -78,35 +86,35 @@ bool CrossPointSettings::loadFromFile() { // load settings that exist (support older files with fewer fields) uint8_t settingsRead = 0; do { - serialization::readPod(inputFile, sleepScreen); + readAndValidate(inputFile, sleepScreen, SLEEP_SCREEN_MODE_COUNT); if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, extraParagraphSpacing); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, shortPwrBtn); + readAndValidate(inputFile, shortPwrBtn, SHORT_PWRBTN_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, statusBar); + readAndValidate(inputFile, statusBar, STATUS_BAR_MODE_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, orientation); + readAndValidate(inputFile, orientation, ORIENTATION_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, frontButtonLayout); + readAndValidate(inputFile, frontButtonLayout, FRONT_BUTTON_LAYOUT_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, sideButtonLayout); + readAndValidate(inputFile, sideButtonLayout, SIDE_BUTTON_LAYOUT_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, fontFamily); + readAndValidate(inputFile, fontFamily, FONT_FAMILY_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, fontSize); + readAndValidate(inputFile, fontSize, FONT_SIZE_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, lineSpacing); + readAndValidate(inputFile, lineSpacing, LINE_COMPRESSION_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, paragraphAlignment); + readAndValidate(inputFile, paragraphAlignment, PARAGRAPH_ALIGNMENT_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, sleepTimeout); + readAndValidate(inputFile, sleepTimeout, SLEEP_TIMEOUT_COUNT); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, refreshFrequency); + readAndValidate(inputFile, refreshFrequency, REFRESH_FREQUENCY_COUNT); if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, screenMargin); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, sleepScreenCoverMode); + readAndValidate(inputFile, sleepScreenCoverMode, SLEEP_SCREEN_COVER_MODE_COUNT); if (++settingsRead >= fileSettingsCount) break; { std::string urlStr; @@ -117,7 +125,7 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, textAntiAliasing); if (++settingsRead >= fileSettingsCount) break; - serialization::readPod(inputFile, hideBatteryPercentage); + readAndValidate(inputFile, hideBatteryPercentage, HIDE_BATTERY_PERCENTAGE_COUNT); if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, longPressChapterSkip); if (++settingsRead >= fileSettingsCount) break; diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index f8892bef..e2883425 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -15,18 +15,18 @@ class CrossPointSettings { CrossPointSettings(const CrossPointSettings&) = delete; CrossPointSettings& operator=(const CrossPointSettings&) = delete; - // Should match with SettingsActivity text - enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4 }; - enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1 }; + enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4, SLEEP_SCREEN_MODE_COUNT }; + enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; // Status bar display type enum - enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2 }; + enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2, STATUS_BAR_MODE_COUNT }; enum ORIENTATION { - PORTRAIT = 0, // 480x800 logical coordinates (current default) - LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom) - INVERTED = 2, // 480x800 logical coordinates, inverted - LANDSCAPE_CCW = 3 // 800x480 logical coordinates, native panel orientation + PORTRAIT = 0, // 480x800 logical coordinates (current default) + LANDSCAPE_CW = 1, // 800x480 logical coordinates, rotated 180° (swap top/bottom) + INVERTED = 2, // 480x800 logical coordinates, inverted + LANDSCAPE_CCW = 3, // 800x480 logical coordinates, native panel orientation + ORIENTATION_COUNT }; // Front button layout options @@ -36,32 +36,53 @@ class CrossPointSettings { BACK_CONFIRM_LEFT_RIGHT = 0, LEFT_RIGHT_BACK_CONFIRM = 1, LEFT_BACK_CONFIRM_RIGHT = 2, - BACK_CONFIRM_RIGHT_LEFT = 3 + BACK_CONFIRM_RIGHT_LEFT = 3, + FRONT_BUTTON_LAYOUT_COUNT }; // Side button layout options // Default: Previous, Next // Swapped: Next, Previous - enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1 }; + enum SIDE_BUTTON_LAYOUT { PREV_NEXT = 0, NEXT_PREV = 1, SIDE_BUTTON_LAYOUT_COUNT }; // Font family options - enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2 }; + enum FONT_FAMILY { BOOKERLY = 0, NOTOSANS = 1, OPENDYSLEXIC = 2, FONT_FAMILY_COUNT }; // Font size options - enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3 }; - enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2 }; - enum PARAGRAPH_ALIGNMENT { JUSTIFIED = 0, LEFT_ALIGN = 1, CENTER_ALIGN = 2, RIGHT_ALIGN = 3 }; + enum FONT_SIZE { SMALL = 0, MEDIUM = 1, LARGE = 2, EXTRA_LARGE = 3, FONT_SIZE_COUNT }; + enum LINE_COMPRESSION { TIGHT = 0, NORMAL = 1, WIDE = 2, LINE_COMPRESSION_COUNT }; + enum PARAGRAPH_ALIGNMENT { + JUSTIFIED = 0, + LEFT_ALIGN = 1, + CENTER_ALIGN = 2, + RIGHT_ALIGN = 3, + PARAGRAPH_ALIGNMENT_COUNT + }; // Auto-sleep timeout options (in minutes) - enum SLEEP_TIMEOUT { SLEEP_1_MIN = 0, SLEEP_5_MIN = 1, SLEEP_10_MIN = 2, SLEEP_15_MIN = 3, SLEEP_30_MIN = 4 }; + enum SLEEP_TIMEOUT { + SLEEP_1_MIN = 0, + SLEEP_5_MIN = 1, + SLEEP_10_MIN = 2, + SLEEP_15_MIN = 3, + SLEEP_30_MIN = 4, + SLEEP_TIMEOUT_COUNT + }; // E-ink refresh frequency (pages between full refreshes) - enum REFRESH_FREQUENCY { REFRESH_1 = 0, REFRESH_5 = 1, REFRESH_10 = 2, REFRESH_15 = 3, REFRESH_30 = 4 }; + enum REFRESH_FREQUENCY { + REFRESH_1 = 0, + REFRESH_5 = 1, + REFRESH_10 = 2, + REFRESH_15 = 3, + REFRESH_30 = 4, + REFRESH_FREQUENCY_COUNT + }; // Short power button press actions - enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2 }; + enum SHORT_PWRBTN { IGNORE = 0, SLEEP = 1, PAGE_TURN = 2, SHORT_PWRBTN_COUNT }; // Hide battery percentage - enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2 }; + enum HIDE_BATTERY_PERCENTAGE { HIDE_NEVER = 0, HIDE_READER = 1, HIDE_ALWAYS = 2, HIDE_BATTERY_PERCENTAGE_COUNT }; // Sleep screen settings uint8_t sleepScreen = DARK; From e9c2fe1c8780c68af9bc0a7aca8801a3ddf58313 Mon Sep 17 00:00:00 2001 From: Alex Faria <3195321+alexfaria@users.noreply.github.com> Date: Tue, 27 Jan 2026 12:25:44 +0000 Subject: [PATCH 24/36] feat: Add status bar option "Full w/ Progress Bar" (#438) ## Summary * **What is the goal of this PR?** This PR introduces a new "Status Bar" mode that displays a visual progress bar at the bottom of the screen, providing readers with a graphical indication of their position within the book. * **What changes are included?** * **Settings**: Updated SettingsActivity to expand the "Status Bar" configuration with a new option: Full w/ Progress Bar. * **EPUB Reader**: Modified EpubReaderActivity to calculate the global book progress and render a progress bar at the bottom of the viewable area when the new setting is active. * **TXT Reader**: Modified TxtReaderActivity to implement similar progress bar rendering logic based on the current page and total page count. ## Additional Context * The progress bar is rendered with a height of 4 pixels at the very bottom of the screen (adjusted for margins). * The feature reuses the existing renderStatusBar logic but conditionally draws the bar instead of (or in addition to) other elements depending on the specific implementation details in each reader. * Renamed existing 'Full' mode to 'Full w/ Percentage' * Added new 'Full w/ Progress Bar' option --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- src/CrossPointSettings.h | 9 +++- src/ScreenComponents.cpp | 11 ++++ src/ScreenComponents.h | 3 ++ src/activities/reader/EpubReaderActivity.cpp | 54 +++++++++++++++----- src/activities/reader/TxtReaderActivity.cpp | 51 ++++++++++++++---- src/activities/settings/SettingsActivity.cpp | 3 +- 6 files changed, 105 insertions(+), 26 deletions(-) diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index e2883425..6385f4f1 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -19,7 +19,14 @@ class CrossPointSettings { enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; // Status bar display type enum - enum STATUS_BAR_MODE { NONE = 0, NO_PROGRESS = 1, FULL = 2, STATUS_BAR_MODE_COUNT }; + enum STATUS_BAR_MODE { + NONE = 0, + NO_PROGRESS = 1, + FULL = 2, + FULL_WITH_PROGRESS_BAR = 3, + ONLY_PROGRESS_BAR = 4, + STATUS_BAR_MODE_COUNT + }; enum ORIENTATION { PORTRAIT = 0, // 480x800 logical coordinates (current default) diff --git a/src/ScreenComponents.cpp b/src/ScreenComponents.cpp index 2e8d9e7c..ef47dfc5 100644 --- a/src/ScreenComponents.cpp +++ b/src/ScreenComponents.cpp @@ -42,6 +42,17 @@ void ScreenComponents::drawBattery(const GfxRenderer& renderer, const int left, renderer.fillRect(x + 2, y + 2, filledWidth, batteryHeight - 4); } +void ScreenComponents::drawBookProgressBar(const GfxRenderer& renderer, const size_t bookProgress) { + int vieweableMarginTop, vieweableMarginRight, vieweableMarginBottom, vieweableMarginLeft; + renderer.getOrientedViewableTRBL(&vieweableMarginTop, &vieweableMarginRight, &vieweableMarginBottom, + &vieweableMarginLeft); + + const int progressBarMaxWidth = renderer.getScreenWidth() - vieweableMarginLeft - vieweableMarginRight; + const int progressBarY = renderer.getScreenHeight() - vieweableMarginBottom - BOOK_PROGRESS_BAR_HEIGHT; + const int barWidth = progressBarMaxWidth * bookProgress / 100; + renderer.fillRect(vieweableMarginLeft, progressBarY, barWidth, BOOK_PROGRESS_BAR_HEIGHT, true); +} + int ScreenComponents::drawTabBar(const GfxRenderer& renderer, const int y, const std::vector& tabs) { constexpr int tabPadding = 20; // Horizontal padding between tabs constexpr int leftMargin = 20; // Left margin for first tab diff --git a/src/ScreenComponents.h b/src/ScreenComponents.h index 48c40f42..15403f60 100644 --- a/src/ScreenComponents.h +++ b/src/ScreenComponents.h @@ -13,7 +13,10 @@ struct TabInfo { class ScreenComponents { public: + static const int BOOK_PROGRESS_BAR_HEIGHT = 4; + static void drawBattery(const GfxRenderer& renderer, int left, int top, bool showPercentage = true); + static void drawBookProgressBar(const GfxRenderer& renderer, size_t bookProgress); // Draw a horizontal tab bar with underline indicator for selected tab // Returns the height of the tab bar (for positioning content below) diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index a6d27d34..bd9c1b1d 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -18,6 +18,8 @@ namespace { constexpr unsigned long skipChapterMs = 700; constexpr unsigned long goHomeMs = 1000; constexpr int statusBarMargin = 19; +constexpr int progressBarMarginTop = 1; + } // namespace void EpubReaderActivity::taskTrampoline(void* param) { @@ -275,7 +277,16 @@ void EpubReaderActivity::renderScreen() { orientedMarginTop += SETTINGS.screenMargin; orientedMarginLeft += SETTINGS.screenMargin; orientedMarginRight += SETTINGS.screenMargin; - orientedMarginBottom += statusBarMargin; + orientedMarginBottom += SETTINGS.screenMargin; + + // Add status bar margin + if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) { + // Add additional margin for status bar if progress bar is shown + const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; + orientedMarginBottom += statusBarMargin - SETTINGS.screenMargin + + (showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0); + } if (!section) { const auto filepath = epub->getSpineItem(currentSpineIndex).href; @@ -446,11 +457,17 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) const { // determine visible status bar elements - const bool showProgress = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; + const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; + const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; + const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; const bool showChapterTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; const bool showBatteryPercentage = SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; @@ -459,19 +476,30 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in const auto textY = screenHeight - orientedMarginBottom - 4; int progressTextWidth = 0; - if (showProgress) { - // Calculate progress in book - const float sectionChapterProg = static_cast(section->currentPage) / section->pageCount; - const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100; + // Calculate progress in book + const float sectionChapterProg = static_cast(section->currentPage) / section->pageCount; + const float bookProgress = epub->calculateProgress(currentSpineIndex, sectionChapterProg) * 100; + if (showProgressText || showProgressPercentage) { // Right aligned text for progress counter char progressStr[32]; - snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount, - bookProgress); - const std::string progress = progressStr; - progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progress.c_str()); + + // Hide percentage when progress bar is shown to reduce clutter + if (showProgressPercentage) { + snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", section->currentPage + 1, section->pageCount, + bookProgress); + } else { + snprintf(progressStr, sizeof(progressStr), "%d/%d", section->currentPage + 1, section->pageCount); + } + + progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr); renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY, - progress.c_str()); + progressStr); + } + + if (showProgressBar) { + // Draw progress bar at the very bottom of the screen, from edge to edge of viewable area + ScreenComponents::drawBookProgressBar(renderer, static_cast(bookProgress)); } if (showBattery) { diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index b7de16d8..cc2036b9 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -15,6 +15,7 @@ namespace { constexpr unsigned long goHomeMs = 1000; constexpr int statusBarMargin = 25; +constexpr int progressBarMarginTop = 1; constexpr size_t CHUNK_SIZE = 8 * 1024; // 8KB chunk for reading // Cache file magic and version @@ -158,7 +159,16 @@ void TxtReaderActivity::initializeReader() { orientedMarginTop += cachedScreenMargin; orientedMarginLeft += cachedScreenMargin; orientedMarginRight += cachedScreenMargin; - orientedMarginBottom += statusBarMargin; + orientedMarginBottom += cachedScreenMargin; + + // Add status bar margin + if (SETTINGS.statusBar != CrossPointSettings::STATUS_BAR_MODE::NONE) { + // Add additional margin for status bar if progress bar is shown + const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; + orientedMarginBottom += statusBarMargin - cachedScreenMargin + + (showProgressBar ? (ScreenComponents::BOOK_PROGRESS_BAR_HEIGHT + progressBarMarginTop) : 0); + } viewportWidth = renderer.getScreenWidth() - orientedMarginLeft - orientedMarginRight; const int viewportHeight = renderer.getScreenHeight() - orientedMarginTop - orientedMarginBottom; @@ -499,27 +509,46 @@ void TxtReaderActivity::renderPage() { void TxtReaderActivity::renderStatusBar(const int orientedMarginRight, const int orientedMarginBottom, const int orientedMarginLeft) const { - const bool showProgress = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; + const bool showProgressPercentage = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; + const bool showProgressBar = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::ONLY_PROGRESS_BAR; + const bool showProgressText = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; const bool showBattery = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; const bool showTitle = SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::NO_PROGRESS || - SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL; + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL || + SETTINGS.statusBar == CrossPointSettings::STATUS_BAR_MODE::FULL_WITH_PROGRESS_BAR; + const bool showBatteryPercentage = + SETTINGS.hideBatteryPercentage == CrossPointSettings::HIDE_BATTERY_PERCENTAGE::HIDE_NEVER; const auto screenHeight = renderer.getScreenHeight(); const auto textY = screenHeight - orientedMarginBottom - 4; int progressTextWidth = 0; - if (showProgress) { - const int progress = totalPages > 0 ? (currentPage + 1) * 100 / totalPages : 0; - const std::string progressStr = - std::to_string(currentPage + 1) + "/" + std::to_string(totalPages) + " " + std::to_string(progress) + "%"; - progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr.c_str()); + const float progress = totalPages > 0 ? (currentPage + 1) * 100.0f / totalPages : 0; + + if (showProgressText || showProgressPercentage) { + char progressStr[32]; + if (showProgressPercentage) { + snprintf(progressStr, sizeof(progressStr), "%d/%d %.0f%%", currentPage + 1, totalPages, progress); + } else { + snprintf(progressStr, sizeof(progressStr), "%d/%d", currentPage + 1, totalPages); + } + + progressTextWidth = renderer.getTextWidth(SMALL_FONT_ID, progressStr); renderer.drawText(SMALL_FONT_ID, renderer.getScreenWidth() - orientedMarginRight - progressTextWidth, textY, - progressStr.c_str()); + progressStr); + } + + if (showProgressBar) { + // Draw progress bar at the very bottom of the screen, from edge to edge of viewable area + ScreenComponents::drawBookProgressBar(renderer, static_cast(progress)); } if (showBattery) { - ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY); + ScreenComponents::drawBattery(renderer, orientedMarginLeft, textY, showBatteryPercentage); } if (showTitle) { diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 819115a5..a211e033 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -16,7 +16,8 @@ const SettingInfo displaySettings[displaySettingsCount] = { // Should match with SLEEP_SCREEN_MODE SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), - SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), + SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, + {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}), SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, {"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; From 49190cca6d405d7f4e8fd2fdb20363fdd92d8441 Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 27 Jan 2026 17:53:13 +0500 Subject: [PATCH 25/36] feat(ux): page turning on button pressed if long-press chapter skip is disabled (#451) ## Summary * If long-press chapter skip is disabled, turn pages on button pressed, not released * Makes page turning snappier * Refactors MappedInputManager for readability --- ### AI Usage Did you use AI tools to help write this code? _**< PARTIALLY>**_ --------- Co-authored-by: Dave Allie --- src/MappedInputManager.cpp | 120 ++++++++----------- src/MappedInputManager.h | 3 +- src/activities/reader/EpubReaderActivity.cpp | 25 ++-- src/activities/reader/TxtReaderActivity.cpp | 25 ++-- src/activities/reader/XtcReaderActivity.cpp | 25 ++-- 5 files changed, 98 insertions(+), 100 deletions(-) diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 994dda5f..14c45deb 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -2,97 +2,73 @@ #include "CrossPointSettings.h" -decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button button) const { +namespace { +using ButtonIndex = uint8_t; + +struct FrontLayoutMap { + ButtonIndex back; + ButtonIndex confirm; + ButtonIndex left; + ButtonIndex right; +}; + +struct SideLayoutMap { + ButtonIndex pageBack; + ButtonIndex pageForward; +}; + +// Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT. +constexpr FrontLayoutMap kFrontLayouts[] = { + {InputManager::BTN_BACK, InputManager::BTN_CONFIRM, InputManager::BTN_LEFT, InputManager::BTN_RIGHT}, + {InputManager::BTN_LEFT, InputManager::BTN_RIGHT, InputManager::BTN_BACK, InputManager::BTN_CONFIRM}, + {InputManager::BTN_CONFIRM, InputManager::BTN_LEFT, InputManager::BTN_BACK, InputManager::BTN_RIGHT}, + {InputManager::BTN_BACK, InputManager::BTN_CONFIRM, InputManager::BTN_RIGHT, InputManager::BTN_LEFT}, +}; + +// Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. +constexpr SideLayoutMap kSideLayouts[] = { + {InputManager::BTN_UP, InputManager::BTN_DOWN}, + {InputManager::BTN_DOWN, InputManager::BTN_UP}, +}; +} // namespace + +bool MappedInputManager::mapButton(const Button button, bool (InputManager::*fn)(uint8_t) const) const { const auto frontLayout = static_cast(SETTINGS.frontButtonLayout); const auto sideLayout = static_cast(SETTINGS.sideButtonLayout); + const auto& front = kFrontLayouts[frontLayout]; + const auto& side = kSideLayouts[sideLayout]; switch (button) { case Button::Back: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - return InputManager::BTN_LEFT; - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - return InputManager::BTN_CONFIRM; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - /* fall through */ - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: - /* fall through */ - default: - return InputManager::BTN_BACK; - } + return (inputManager.*fn)(front.back); case Button::Confirm: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - return InputManager::BTN_RIGHT; - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - return InputManager::BTN_LEFT; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - /* fall through */ - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: - /* fall through */ - default: - return InputManager::BTN_CONFIRM; - } + return (inputManager.*fn)(front.confirm); case Button::Left: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - /* fall through */ - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - return InputManager::BTN_BACK; - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: - return InputManager::BTN_RIGHT; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - /* fall through */ - default: - return InputManager::BTN_LEFT; - } + return (inputManager.*fn)(front.left); case Button::Right: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - return InputManager::BTN_CONFIRM; - case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: - return InputManager::BTN_LEFT; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - /* fall through */ - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - /* fall through */ - default: - return InputManager::BTN_RIGHT; - } + return (inputManager.*fn)(front.right); case Button::Up: - return InputManager::BTN_UP; + return (inputManager.*fn)(InputManager::BTN_UP); case Button::Down: - return InputManager::BTN_DOWN; + return (inputManager.*fn)(InputManager::BTN_DOWN); case Button::Power: - return InputManager::BTN_POWER; + return (inputManager.*fn)(InputManager::BTN_POWER); case Button::PageBack: - switch (sideLayout) { - case CrossPointSettings::NEXT_PREV: - return InputManager::BTN_DOWN; - case CrossPointSettings::PREV_NEXT: - /* fall through */ - default: - return InputManager::BTN_UP; - } + return (inputManager.*fn)(side.pageBack); case Button::PageForward: - switch (sideLayout) { - case CrossPointSettings::NEXT_PREV: - return InputManager::BTN_UP; - case CrossPointSettings::PREV_NEXT: - /* fall through */ - default: - return InputManager::BTN_DOWN; - } + return (inputManager.*fn)(side.pageForward); } - return InputManager::BTN_BACK; + return false; } -bool MappedInputManager::wasPressed(const Button button) const { return inputManager.wasPressed(mapButton(button)); } +bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &InputManager::wasPressed); } -bool MappedInputManager::wasReleased(const Button button) const { return inputManager.wasReleased(mapButton(button)); } +bool MappedInputManager::wasReleased(const Button button) const { + return mapButton(button, &InputManager::wasReleased); +} -bool MappedInputManager::isPressed(const Button button) const { return inputManager.isPressed(mapButton(button)); } +bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &InputManager::isPressed); } bool MappedInputManager::wasAnyPressed() const { return inputManager.wasAnyPressed(); } diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index 62065fe9..bee7cd4b 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -25,5 +25,6 @@ class MappedInputManager { private: InputManager& inputManager; - decltype(InputManager::BTN_BACK) mapButton(Button button) const; + + bool mapButton(Button button, bool (InputManager::*fn)(uint8_t) const) const; }; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index bd9c1b1d..89be3bc7 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -170,14 +170,21 @@ void EpubReaderActivity::loop() { return; } - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || - (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && - mappedInput.wasReleased(MappedInputManager::Button::Power)) || - mappedInput.wasReleased(MappedInputManager::Button::Right); + // When long-press chapter skip is disabled, turn pages on press instead of release. + const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip; + const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) || + mappedInput.wasPressed(MappedInputManager::Button::Left)) + : (mappedInput.wasReleased(MappedInputManager::Button::PageBack) || + mappedInput.wasReleased(MappedInputManager::Button::Left)); + const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && + mappedInput.wasReleased(MappedInputManager::Button::Power); + const bool nextTriggered = usePressForPageTurn + ? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn || + mappedInput.wasPressed(MappedInputManager::Button::Right)) + : (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn || + mappedInput.wasReleased(MappedInputManager::Button::Right)); - if (!prevReleased && !nextReleased) { + if (!prevTriggered && !nextTriggered) { return; } @@ -195,7 +202,7 @@ void EpubReaderActivity::loop() { // We don't want to delete the section mid-render, so grab the semaphore xSemaphoreTake(renderingMutex, portMAX_DELAY); nextPageNumber = 0; - currentSpineIndex = nextReleased ? currentSpineIndex + 1 : currentSpineIndex - 1; + currentSpineIndex = nextTriggered ? currentSpineIndex + 1 : currentSpineIndex - 1; section.reset(); xSemaphoreGive(renderingMutex); updateRequired = true; @@ -208,7 +215,7 @@ void EpubReaderActivity::loop() { return; } - if (prevReleased) { + if (prevTriggered) { if (section->currentPage > 0) { section->currentPage--; } else { diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index cc2036b9..7df083a6 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -110,21 +110,28 @@ void TxtReaderActivity::loop() { return; } - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || - (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && - mappedInput.wasReleased(MappedInputManager::Button::Power)) || - mappedInput.wasReleased(MappedInputManager::Button::Right); + // When long-press chapter skip is disabled, turn pages on press instead of release. + const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip; + const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) || + mappedInput.wasPressed(MappedInputManager::Button::Left)) + : (mappedInput.wasReleased(MappedInputManager::Button::PageBack) || + mappedInput.wasReleased(MappedInputManager::Button::Left)); + const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && + mappedInput.wasReleased(MappedInputManager::Button::Power); + const bool nextTriggered = usePressForPageTurn + ? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn || + mappedInput.wasPressed(MappedInputManager::Button::Right)) + : (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn || + mappedInput.wasReleased(MappedInputManager::Button::Right)); - if (!prevReleased && !nextReleased) { + if (!prevTriggered && !nextTriggered) { return; } - if (prevReleased && currentPage > 0) { + if (prevTriggered && currentPage > 0) { currentPage--; updateRequired = true; - } else if (nextReleased && currentPage < totalPages - 1) { + } else if (nextTriggered && currentPage < totalPages - 1) { currentPage++; updateRequired = true; } diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 0a58d7b3..9761e27d 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -111,14 +111,21 @@ void XtcReaderActivity::loop() { return; } - const bool prevReleased = mappedInput.wasReleased(MappedInputManager::Button::PageBack) || - mappedInput.wasReleased(MappedInputManager::Button::Left); - const bool nextReleased = mappedInput.wasReleased(MappedInputManager::Button::PageForward) || - (SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && - mappedInput.wasReleased(MappedInputManager::Button::Power)) || - mappedInput.wasReleased(MappedInputManager::Button::Right); + // When long-press chapter skip is disabled, turn pages on press instead of release. + const bool usePressForPageTurn = !SETTINGS.longPressChapterSkip; + const bool prevTriggered = usePressForPageTurn ? (mappedInput.wasPressed(MappedInputManager::Button::PageBack) || + mappedInput.wasPressed(MappedInputManager::Button::Left)) + : (mappedInput.wasReleased(MappedInputManager::Button::PageBack) || + mappedInput.wasReleased(MappedInputManager::Button::Left)); + const bool powerPageTurn = SETTINGS.shortPwrBtn == CrossPointSettings::SHORT_PWRBTN::PAGE_TURN && + mappedInput.wasReleased(MappedInputManager::Button::Power); + const bool nextTriggered = usePressForPageTurn + ? (mappedInput.wasPressed(MappedInputManager::Button::PageForward) || powerPageTurn || + mappedInput.wasPressed(MappedInputManager::Button::Right)) + : (mappedInput.wasReleased(MappedInputManager::Button::PageForward) || powerPageTurn || + mappedInput.wasReleased(MappedInputManager::Button::Right)); - if (!prevReleased && !nextReleased) { + if (!prevTriggered && !nextTriggered) { return; } @@ -132,14 +139,14 @@ void XtcReaderActivity::loop() { const bool skipPages = SETTINGS.longPressChapterSkip && mappedInput.getHeldTime() > skipPageMs; const int skipAmount = skipPages ? 10 : 1; - if (prevReleased) { + if (prevTriggered) { if (currentPage >= static_cast(skipAmount)) { currentPage -= skipAmount; } else { currentPage = 0; } updateRequired = true; - } else if (nextReleased) { + } else if (nextTriggered) { currentPage += skipAmount; if (currentPage >= xtc->getPageCount()) { currentPage = xtc->getPageCount(); // Allow showing "End of book" From 4848a77e1ba4b0eb02533b91388e5b70bf4b2c82 Mon Sep 17 00:00:00 2001 From: Eliz Date: Tue, 27 Jan 2026 13:21:59 +0000 Subject: [PATCH 26/36] feat: Add support to B&W filters to image covers (#476) ## Summary * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Implementation of a new feature in Display options as Image Filter * **What changes are included?** Black & White and Inverted Black & White options are added. ## Additional Context Here are some examples: | None | Contrast | Inverted | | --- | --- | --- | | image | image | image | | image | image | image | * Add any other information that might be helpful for the reviewer (e.g., performance implications, potential risks, specific areas to focus on). I have also tried adding Color inversion, but could not see much difference with that. It might be because my implementation was wrong. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _** PARTIALLY **_ --------- Co-authored-by: Dave Allie --- USER_GUIDE.md | 4 ++++ src/CrossPointSettings.cpp | 9 ++++++--- src/CrossPointSettings.h | 8 ++++++++ src/activities/boot_sleep/SleepActivity.cpp | 11 ++++++++++- src/activities/settings/SettingsActivity.cpp | 4 +++- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index bdc0f036..06973c92 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -105,6 +105,10 @@ The Settings screen allows you to configure the device's behavior. There are a f - **Sleep Screen Cover Mode**: How to display the book cover when "Cover" sleep screen is selected: - "Fit" (default) - Scale the image down to fit centered on the screen, padding with white borders as necessary - "Crop" - Scale the image down and crop as necessary to try to to fill the screen (Note: this is experimental and may not work as expected) +- **Sleep Screen Cover Filter**: What filter will be applied to the book cover when "Cover" sleep screen is selected + - "None" (default) - The cover image will be converted to a grayscale image and displayed as it is + - "Contrast" - The image will be displayed as a black & white image without grayscale conversion + - "Inverted" - The image will be inverted as in white&black and will be displayed without grayscale conversion - **Status Bar**: Configure the status bar displayed while reading: - "None" - No status bar - "No Progress" - Show status bar without reading progress diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index f3a7a524..232c7c57 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -22,7 +22,7 @@ void readAndValidate(FsFile& file, uint8_t& member, const uint8_t maxValue) { namespace { constexpr uint8_t SETTINGS_FILE_VERSION = 1; // Increment this when adding new persisted settings fields -constexpr uint8_t SETTINGS_COUNT = 22; +constexpr uint8_t SETTINGS_COUNT = 23; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -57,9 +57,10 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, hideBatteryPercentage); serialization::writePod(outputFile, longPressChapterSkip); serialization::writePod(outputFile, hyphenationEnabled); - // New fields added at end for backward compatibility serialization::writeString(outputFile, std::string(opdsUsername)); serialization::writeString(outputFile, std::string(opdsPassword)); + serialization::writePod(outputFile, sleepScreenCoverFilter); + // New fields added at end for backward compatibility outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -131,7 +132,6 @@ bool CrossPointSettings::loadFromFile() { if (++settingsRead >= fileSettingsCount) break; serialization::readPod(inputFile, hyphenationEnabled); if (++settingsRead >= fileSettingsCount) break; - // New fields added at end for backward compatibility { std::string usernameStr; serialization::readString(inputFile, usernameStr); @@ -146,6 +146,9 @@ bool CrossPointSettings::loadFromFile() { opdsPassword[sizeof(opdsPassword) - 1] = '\0'; } if (++settingsRead >= fileSettingsCount) break; + readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); + if (++settingsRead >= fileSettingsCount) break; + // New fields added at end for backward compatibility } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 6385f4f1..c450d348 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -17,6 +17,12 @@ class CrossPointSettings { enum SLEEP_SCREEN_MODE { DARK = 0, LIGHT = 1, CUSTOM = 2, COVER = 3, BLANK = 4, SLEEP_SCREEN_MODE_COUNT }; enum SLEEP_SCREEN_COVER_MODE { FIT = 0, CROP = 1, SLEEP_SCREEN_COVER_MODE_COUNT }; + enum SLEEP_SCREEN_COVER_FILTER { + NO_FILTER = 0, + BLACK_AND_WHITE = 1, + INVERTED_BLACK_AND_WHITE = 2, + SLEEP_SCREEN_COVER_FILTER_COUNT + }; // Status bar display type enum enum STATUS_BAR_MODE { @@ -95,6 +101,8 @@ class CrossPointSettings { uint8_t sleepScreen = DARK; // Sleep screen cover mode settings uint8_t sleepScreenCoverMode = FIT; + // Sleep screen cover filter + uint8_t sleepScreenCoverFilter = NO_FILTER; // Status bar settings uint8_t statusBar = FULL; // Text rendering settings diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index c4b98968..95fe742f 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -179,10 +179,19 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y); renderer.clearScreen(); + + const bool hasGreyscale = bitmap.hasGreyscale() && + SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::NO_FILTER; + renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); + + if (SETTINGS.sleepScreenCoverFilter == CrossPointSettings::SLEEP_SCREEN_COVER_FILTER::INVERTED_BLACK_AND_WHITE) { + renderer.invertScreen(); + } + renderer.displayBuffer(EInkDisplay::HALF_REFRESH); - if (bitmap.hasGreyscale()) { + if (hasGreyscale) { bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); diff --git a/src/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index a211e033..7316db05 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -11,11 +11,13 @@ const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; namespace { -constexpr int displaySettingsCount = 5; +constexpr int displaySettingsCount = 6; const SettingInfo displaySettings[displaySettingsCount] = { // Should match with SLEEP_SCREEN_MODE SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), + SettingInfo::Enum("Sleep Screen Cover Filter", &CrossPointSettings::sleepScreenCoverFilter, + {"None", "Contrast", "Inverted"}), SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full w/ Percentage", "Full w/ Progress Bar", "Progress Bar"}), SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), From 8e0d2bece232f54c7cfbf01e58ad9643c37a38da Mon Sep 17 00:00:00 2001 From: Lalo <86909609+la-lo-go@users.noreply.github.com> Date: Tue, 27 Jan 2026 15:17:48 +0100 Subject: [PATCH 27/36] feat: Add Spanish hyphenation support (#558) ## Summary * **What is the goal of this PR?** Add Spanish language hyphenation support to improve text rendering for Spanish books. * **What changes are included?** - Added Spanish hyphenation trie (`hyph-es.trie.h`) generated from Typst's hypher patterns - Registered `spanishHyphenator` in `LanguageRegistry.cpp` for language tag `es` - Added Spanish to the hyphenation evaluation test suite - Added Spanish test data file with 5000 test cases ## Additional Context * **Test Results:** Spanish hyphenation achieves 99.02% F1 Score (97.72% perfect matches out of 5000 test cases) * **Compatibility:** Works automatically for EPUBs with `es` (or es-ES, es-MX, etc.) imagen | Metric | Value | |--------|-------| | Perfect matches | 97.72% | | Overall Precision | 99.33% | | Overall Recall | 99.42% | | Overall F1 Score | 99.38% | --- ### AI Usage Did you use AI tools to help write this code? _**PARTIALLY**_ AI assisted with: - Guiding and compile - Preparing the PR description --- .../Epub/hyphenation/LanguageRegistry.cpp | 7 +- .../Epub/hyphenation/generated/hyph-es.trie.h | 734 +++ .../HyphenationEvaluationTest.cpp | 1 + .../resources/spanish_hyphenation_tests.txt | 5012 +++++++++++++++++ 4 files changed, 5752 insertions(+), 2 deletions(-) create mode 100644 lib/Epub/Epub/hyphenation/generated/hyph-es.trie.h create mode 100644 test/hyphenation_eval/resources/spanish_hyphenation_tests.txt diff --git a/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp b/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp index 0643a9fa..5efd76bb 100644 --- a/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp +++ b/lib/Epub/Epub/hyphenation/LanguageRegistry.cpp @@ -6,6 +6,7 @@ #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-ru.trie.h" @@ -16,14 +17,16 @@ LanguageHyphenator englishHyphenator(en_us_patterns, isLatinLetter, toLowerLatin LanguageHyphenator frenchHyphenator(fr_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator germanHyphenator(de_patterns, isLatinLetter, toLowerLatin); LanguageHyphenator russianHyphenator(ru_ru_patterns, isCyrillicLetter, toLowerCyrillic); +LanguageHyphenator spanishHyphenator(es_patterns, isLatinLetter, toLowerLatin); -using EntryArray = std::array; +using EntryArray = std::array; const EntryArray& entries() { static const EntryArray kEntries = {{{"english", "en", &englishHyphenator}, {"french", "fr", &frenchHyphenator}, {"german", "de", &germanHyphenator}, - {"russian", "ru", &russianHyphenator}}}; + {"russian", "ru", &russianHyphenator}, + {"spanish", "es", &spanishHyphenator}}}; return kEntries; } diff --git a/lib/Epub/Epub/hyphenation/generated/hyph-es.trie.h b/lib/Epub/Epub/hyphenation/generated/hyph-es.trie.h new file mode 100644 index 00000000..0df8819a --- /dev/null +++ b/lib/Epub/Epub/hyphenation/generated/hyph-es.trie.h @@ -0,0 +1,734 @@ +#pragma once + +#include +#include + +#include "../SerializedHyphenationTrie.h" + +// Auto-generated by generate_hyphenation_trie.py. Do not edit manually. +alignas(4) constexpr uint8_t es_trie_data[] = { + 0x00, 0x00, 0x34, 0xFC, 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 = { + es_trie_data, + sizeof(es_trie_data), +}; diff --git a/test/hyphenation_eval/HyphenationEvaluationTest.cpp b/test/hyphenation_eval/HyphenationEvaluationTest.cpp index 90d17101..e01b647f 100644 --- a/test/hyphenation_eval/HyphenationEvaluationTest.cpp +++ b/test/hyphenation_eval/HyphenationEvaluationTest.cpp @@ -42,6 +42,7 @@ const std::vector kSupportedLanguages = { {"french", "test/hyphenation_eval/resources/french_hyphenation_tests.txt", "fr"}, {"german", "test/hyphenation_eval/resources/german_hyphenation_tests.txt", "de"}, {"russian", "test/hyphenation_eval/resources/russian_hyphenation_tests.txt", "ru"}, + {"spanish", "test/hyphenation_eval/resources/spanish_hyphenation_tests.txt", "es"}, }; std::vector expectedPositionsFromAnnotatedWord(const std::string& annotated) { diff --git a/test/hyphenation_eval/resources/spanish_hyphenation_tests.txt b/test/hyphenation_eval/resources/spanish_hyphenation_tests.txt new file mode 100644 index 00000000..f50fb062 --- /dev/null +++ b/test/hyphenation_eval/resources/spanish_hyphenation_tests.txt @@ -0,0 +1,5012 @@ +# Hyphenation Test Data +# Source: quijote.epub +# Language: es_ES +# Min prefix: 2 +# Min suffix: 2 +# Total words: 5000 +# Format: word | hyphenated_form | frequency_in_source +# +# Hyphenation points are marked with '=' +# Example: Silbentrennung -> Sil=ben=tren=nung +# + +Quijote|Qui=jo=te|2264 +Sancho|San=cho|2171 +porque|por=que|1333 +respondió|res=pon=dió|1053 +merced|mer=ced|900 +vuestra|vues=tra|813 +cuando|cuan=do|712 +caballero|ca=ba=lle=ro|584 +aunque|aun=que|525 +señora|se=ño=ra|504 +estaba|es=ta=ba|462 +verdad|ver=dad|418 +alguna|al=gu=na|384 +manera|ma=ne=ra|329 +aquella|aque=lla|328 +tiempo|tiem=po|327 +puesto|pues=to|305 +caballeros|ca=ba=lle=ros|292 +Dulcinea|Dul=ci=nea|284 +tierra|tie=rra|273 +historia|his=to=ria|259 +hombre|hom=bre|258 +quiero|quie=ro|253 +habían|ha=bían|251 +camino|ca=mino|246 +escudero|es=cu=de=ro|246 +parece|pa=re=ce|239 +muchas|mu=chas|236 +cuenta|cuen=ta|222 +cuanto|cuan=to|219 +cabeza|ca=be=za|216 +replicó|re=pli=có|207 +Rocinante|Ro=ci=nan=te|204 +nuestro|nues=tro|202 +parecer|pa=re=cer|202 +razones|ra=zo=nes|202 +también|tam=bién|199 +diciendo|di=cien=do|198 +grande|gran=de|198 +andante|an=dan=te|197 +muchos|mu=chos|197 +caballo|ca=ba=llo|196 +duquesa|du=que=sa|190 +después|des=pués|186 +primero|pri=me=ro|186 +nombre|nom=bre|181 +Mancha|Man=cha|178 +estaban|es=ta=ban|174 +barbero|bar=be=ro|171 +gobernador|go=ber=na=dor|171 +adelante|ade=lan=te|170 +Toboso|To=bo=so|163 +andantes|an=dan=tes|162 +aventura|aven=tu=ra|160 +voluntad|vo=lun=tad|160 +vuestro|vues=tro|158 +aquellos|aque=llos|156 +ventura|ven=tu=ra|155 +cuatro|cua=tro|153 +rostro|ros=tro|153 +entender|en=ten=der|151 +Camila|Ca=mi=la|148 +doncella|don=ce=lla|148 +libros|li=bros|148 +menester|me=nes=ter|147 +palabra|pa=la=bra|147 +tienen|tie=nen|147 +fueron|fue=ron|146 +siempre|siem=pre|145 +señores|se=ño=res|144 +caballería|ca=ba=lle=ría|143 +castillo|cas=ti=llo|143 +cuales|cua=les|143 +alguno|al=guno|142 +cuerpo|cuer=po|142 +hermosa|her=mo=sa|142 +Lotario|Lo=ta=rio|142 +corazón|co=ra=zón|141 +quiere|quie=re|141 +suerte|suer=te|141 +nuestra|nues=tra|140 +Anselmo|An=sel=mo|138 +muerte|muer=te|138 +ninguna|nin=gu=na|137 +persona|per=so=na|136 +Fernando|Fer=nan=do|135 +entonces|en=ton=ces|133 +comenzó|co=men=zó|132 +fuerza|fuer=za|128 +memoria|me=mo=ria|128 +Capítulo|Ca=pí=tu=lo|126 +preguntó|pre=gun=tó|126 +grandes|gran=des|125 +palabras|pa=la=bras|125 +delante|de=lan=te|124 +contra|contra|123 +hermosura|her=mo=su=ra|123 +posible|po=si=ble|123 +cierto|cier=to|121 +contento|con=ten=to|119 +ventero|ven=te=ro|118 +ínsula|ín=su=la|118 +algunos|al=gu=nos|117 +gobierno|go=bierno|117 +Gutenberg|Gu=ten=berg|117 +Project|Pro=ject|117 +viendo|vien=do|117 +Dorotea|Do=ro=tea|112 +bachiller|ba=chi=ller|111 +tantas|tan=tas|111 +habéis|ha=béis|110 +espada|es=pa=da|109 +volvió|vol=vió|109 +nosotros|no=so=tros|108 +intención|in=ten=ción|107 +muerto|muer=to|107 +volver|vol=ver|107 +buscar|bus=car|105 +aquello|aque=llo|104 +ocasión|oca=sión|104 +pueblo|pue=blo|104 +buenos|bue=nos|102 +hombres|hom=bres|102 +lengua|len=gua|102 +pensamientos|pen=sa=mien=tos|102 +siendo|sien=do|102 +Cardenio|Car=de=nio|101 +partes|par=tes|101 +ciudad|ciu=dad|100 +estado|es=ta=do|100 +llevar|lle=var|100 +pareció|pa=re=ció|100 +Luscinda|Lus=cin=da|99 +lágrimas|lá=gri=mas|99 +tantos|tan=tos|99 +aventuras|aven=tu=ras|98 +dellos|de=llos|98 +hermano|her=ma=no|98 +adonde|adon=de|97 +hablar|ha=blar|97 +aquellas|aque=llas|96 +pienso|pien=so|96 +quería|que=ría|96 +buenas|bue=nas|95 +famoso|fa=mo=so|95 +ninguno|nin=guno|94 +parecía|pa=re=cía|94 +aposento|apo=sen=to|93 +primera|pri=me=ra|93 +Caballero|Ca=ba=lle=ro|92 +brazos|bra=zos|91 +conmigo|con=mi=go|90 +entrar|en=trar|90 +podría|po=dría|90 +Teresa|Te=re=sa|90 +cuento|cuen=to|89 +pudiera|pu=die=ra|89 +pensar|pen=sar|88 +algunas|al=gu=nas|86 +dieron|die=ron|86 +trabajo|tra=ba=jo|86 +habiendo|ha=bien=do|85 +mañana|ma=ña=na|85 +puerta|puer=ta|85 +batalla|ba=ta=lla|84 +diablo|dia=blo|84 +pueden|pue=den|84 +juicio|jui=cio|83 +nuevas|nue=vas|83 +cuantos|cuan=tos|82 +entendimiento|en=ten=di=mien=to|82 +libertad|li=ber=tad|82 +sucesos|su=ce=sos|82 +vuestras|vues=tras|82 +apenas|ape=nas|80 +caballerías|ca=ba=lle=rías|80 +desgracia|des=gra=cia|80 +llaman|lla=man|80 +natural|na=tu=ral|80 +peligro|pe=li=gro|80 +quisiera|qui=sie=ra|80 +marido|ma=ri=do|78 +pensamiento|pen=sa=mien=to|78 +Zoraida|Zo=rai=da|78 +locura|lo=cu=ra|77 +Sansón|San=són|77 +debajo|de=ba=jo|76 +discreto|dis=cre=to|76 +mercedes|mer=ce=des|76 +España|Es=pa=ña|75 +Finalmente|Fi=nal=men=te|75 +hubiera|hu=bie=ra|75 +nuestros|nues=tros|75 +remedio|re=me=dio|75 +silencio|si=len=cio|75 +suceso|su=ce=so|75 +valeroso|va=le=ro=so|75 +criado|cria=do|74 +jumento|ju=men=to|74 +licencia|li=cen=cia|74 +presto|pres=to|74 +semejantes|se=me=jan=tes|74 +sucedió|su=ce=dió|74 +deseos|de=seos|73 +priesa|prie=sa|73 +condición|con=di=ción|72 +cristiano|cris=tia=no|72 +espacio|es=pa=cio|72 +hidalgo|hi=dal=go|72 +doncellas|don=ce=llas|71 +llegar|lle=gar|71 +estando|es=tan=do|70 +hicieron|hi=cie=ron|70 +labrador|la=bra=dor|70 +padres|pa=dres|70 +cristianos|cris=tia=nos|69 +quisiere|qui=sie=re|69 +venían|ve=nían|69 +vestido|ves=ti=do|69 +noticia|no=ti=cia|68 +pensaba|pen=sa=ba|68 +discurso|dis=cur=so|67 +hazañas|ha=za=ñas|67 +mayores|ma=yo=res|67 +pasado|pa=sa=do|67 +amigos|ami=gos|66 +espaldas|es=pal=das|66 +gracias|gra=cias|66 +tenido|te=ni=do|66 +tenían|te=nían|66 +Antonio|An=to=nio|65 +barbas|bar=bas|65 +escuderos|es=cu=de=ros|65 +esperar|es=pe=rar|65 +figura|fi=gu=ra|65 +grandeza|gran=de=za|65 +muestras|mues=tras|65 +sangre|san=gre|65 +virtud|vir=tud|65 +Altisidora|Alti=si=do=ra|64 +criados|cria=dos|64 +encantadores|en=can=ta=do=res|64 +llamaba|lla=ma=ba|64 +principio|prin=ci=pio|64 +querer|que=rer|64 +enamorado|ena=mo=ra=do|63 +enemigo|ene=mi=go|63 +Carrasco|Ca=rras=co|62 +dormir|dor=mir|62 +fortuna|for=tu=na|62 +Porque|Por=que|62 +valiente|va=lien=te|62 +azotes|azo=tes|61 +compañía|com=pa=ñía|61 +consigo|con=si=go|61 +querría|que=rría|61 +resolución|re=so=lu=ción|61 +haciendo|ha=cien=do|60 +ningún|nin=gún|60 +provecho|pro=ve=cho|60 +renegado|re=ne=ga=do|60 +sobrina|so=bri=na|60 +tuviese|tu=vie=se|60 +hambre|ham=bre|59 +hubiese|hu=bie=se|59 +triste|tris=te|59 +encantado|en=can=ta=do|58 +finalmente|fi=nal=men=te|58 +hallar|ha=llar|58 +necesidad|ne=ce=si=dad|58 +pasaba|pa=sa=ba|58 +pastor|pas=tor|58 +presente|pre=sen=te|58 +principal|prin=ci=pal|58 +suelen|sue=len|58 +vieron|vie=ron|58 +dijese|di=je=se|57 +dineros|di=ne=ros|57 +esposo|es=po=so|57 +estuvo|es=tu=vo|57 +hallado|ha=lla=do|57 +letras|le=tras|57 +licenciado|li=cen=cia=do|57 +destos|des=tos|56 +discreción|dis=cre=ción|56 +gigante|gi=gan=te|56 +plática|plá=ti=ca|56 +quitar|qui=tar|56 +tienes|tie=nes|56 +ejercicio|ejer=ci=cio|55 +encima|en=ci=ma|55 +llegaron|lle=ga=ron|55 +princesa|prin=ce=sa|55 +reales|rea=les|55 +veinte|vein=te|55 +dueñas|due=ñas|54 +esperando|es=pe=ran=do|54 +llamar|lla=mar|54 +mirando|mi=ran=do|54 +quedaron|que=da=ron|54 +vencido|ven=ci=do|54 +Basilio|Ba=si=lio|53 +cuidado|cui=da=do|53 +dejando|de=jan=do|53 +hacienda|ha=cien=da|53 +Montesinos|Mon=te=si=nos|53 +mujeres|mu=je=res|53 +oficio|ofi=cio|53 +servir|ser=vir|53 +verdadera|ver=da=de=ra|53 +versos|ver=sos|53 +acabar|aca=bar|52 +acuerdo|acuer=do|52 +asimismo|asi=mis=mo|52 +contar|con=tar|52 +dentro|den=tro|52 +diciéndole|di=cién=do=le|52 +Miguel|Mi=guel|52 +prosiguió|pro=si=guió|52 +quieres|quie=res|52 +responder|res=pon=der|52 +sucedido|su=ce=di=do|52 +verdadero|ver=da=de=ro|52 +destas|des=tas|51 +esposa|es=po=sa|51 +historias|his=to=rias|51 +negocio|ne=go=cio|51 +presencia|pre=sen=cia|51 +servido|ser=vi=do|51 +efecto|efec=to|50 +escrito|es=cri=to|50 +fuerzas|fuer=zas|50 +presentes|pre=sen=tes|50 +puesta|pues=ta|50 +bondad|bon=dad|49 +cabrero|ca=bre=ro|49 +dejado|de=ja=do|49 +disparates|dis=pa=ra=tes|49 +enemigos|ene=mi=gos|49 +huésped|huésped|49 +personas|per=so=nas|49 +respuesta|res=pues=ta|49 +señoras|se=ño=ras|49 +Cervantes|Cer=van=tes|48 +conocido|co=no=ci=do|48 +Figura|Fi=gu=ra|48 +género|gé=ne=ro|48 +nuestras|nues=tras|48 +pesadumbre|pe=sa=dum=bre|48 +Triste|Tris=te|48 +término|tér=mino|48 +vestidos|ves=ti=dos|48 +consejo|con=se=jo|47 +cólera|có=le=ra|47 +ingenio|in=ge=nio|47 +justicia|jus=ti=cia|47 +llegando|lle=gan=do|47 +muestra|mues=tra|47 +opinión|opi=nión|47 +perder|per=der|47 +propósito|pro=pó=si=to|47 +pusieron|pu=sie=ron|47 +vuelto|vuel=to|47 +albarda|al=bar=da|46 +amores|amo=res|46 +andaba|an=da=ba|46 +cabellos|ca=be=llos|46 +cautivo|cau=ti=vo|46 +conocer|co=no=cer|46 +cortesía|cor=tesía|46 +Cuando|Cuan=do|46 +entiendo|en=tien=do|46 +especialmente|es=pe=cial=men=te|46 +estremo|es=tre=mo|46 +fueran|fue=ran|46 +imaginación|ima=gi=na=ción|46 +venido|ve=ni=do|46 +viento|vien=to|46 +escudos|es=cu=dos|45 +esperanza|es=pe=ran=za|45 +instante|ins=tan=te|45 +pudiese|pu=die=se|45 +quieren|quie=ren|45 +Rodríguez|Ro=drí=guez|45 +todavía|to=da=vía|45 +tuvieron|tu=vie=ron|45 +Amadís|Ama=dís|44 +arriba|arri=ba|44 +belleza|be=lle=za|44 +blanco|blan=co|44 +emperador|em=pe=ra=dor|44 +fuesen|fuesen|44 +general|ge=ne=ral|44 +gloria|glo=ria|44 +guerra|gue=rra|44 +honestidad|ho=nes=ti=dad|44 +Leonela|Leo=ne=la|44 +pasaron|pa=sa=ron|44 +árboles|ár=bo=les|44 +Bosque|Bos=que|43 +buscando|bus=can=do|43 +contado|con=ta=do|43 +hechos|he=chos|43 +llegado|lle=ga=do|43 +llevaba|lle=va=ba|43 +primer|pri=mer|43 +quedar|que=dar|43 +Saavedra|Saave=dra|43 +fuerte|fuer=te|42 +imposible|im=po=si=ble|42 +linaje|li=na=je|42 +llamado|lla=ma=do|42 +mejores|me=jo=res|42 +tiempos|tiem=pos|42 +venganza|ven=gan=za|42 +verdaderamente|ver=da=de=ra=men=te|42 +vuestros|vues=tros|42 +agravio|agra=vio|41 +amistad|amis=tad|41 +armado|ar=ma=do|41 +duques|du=ques|41 +escribir|es=cri=bir|41 +jardín|jar=dín|41 +oyeron|oye=ron|41 +segunda|se=gun=da|41 +seguro|se=gu=ro|41 +admiración|ad=mi=ra=ción|40 +alcanzar|al=can=zar|40 +alegre|ale=gre|40 +canónigo|ca=nó=ni=go|40 +contrario|con=tra=rio|40 +costumbre|cos=tum=bre|40 +desdichado|des=di=cha=do|40 +hiciese|hi=cie=se|40 +pastores|pas=to=res|40 +Quiteria|Qui=te=ria|40 +siglos|si=glos|40 +conciencia|con=cien=cia|39 +dientes|dien=tes|39 +discreta|dis=cre=ta|39 +leguas|le=guas|39 +mentira|men=ti=ra|39 +podían|po=dían|39 +rodillas|ro=di=llas|39 +señales|se=ña=les|39 +vienen|vie=nen|39 +Vuestra|Vues=tra|39 +caminos|ca=mi=nos|38 +conozco|co=noz=co|38 +diligencia|di=li=gen=cia|38 +dinero|di=ne=ro|38 +donaire|do=nai=re|38 +hallaron|ha=lla=ron|38 +mayordomo|ma=yor=do=mo|38 +perlas|per=las|38 +salido|sali=do|38 +capitán|ca=pi=tán|37 +correr|co=rrer|37 +entrambos|en=tram=bos|37 +galeras|ga=le=ras|37 +gentil|gen=til|37 +hacían|ha=cían|37 +Hamete|Ha=me=te|37 +juntos|jun=tos|37 +levantó|le=van=tó|37 +molido|mo=li=do|37 +mostraba|mos=tra=ba|37 +paciencia|pa=cien=cia|37 +tuviera|tu=vie=ra|37 +Camacho|Ca=ma=cho|36 +experiencia|ex=pe=rien=cia|36 +gentes|gen=tes|36 +grandísimo|gran=dí=si=mo|36 +labradora|la=bra=do=ra|36 +obligado|obli=ga=do|36 +trabajos|tra=ba=jos|36 +título|tí=tu=lo|36 +viniese|vi=nie=se|36 +volvieron|vol=vie=ron|36 +celada|ce=la=da|35 +creyendo|cre=yen=do|35 +cristiana|cris=tia=na|35 +cumplir|cum=plir|35 +decían|de=cían|35 +Dolorida|Do=lo=ri=da|35 +encantador|en=can=ta=dor|35 +engaño|en=ga=ño|35 +entrañas|en=tra=ñas|35 +guardar|guar=dar|35 +imagino|ima=gino|35 +industria|in=dus=tria|35 +llevado|lle=va=do|35 +locuras|lo=cu=ras|35 +pasados|pa=sa=dos|35 +pedazos|pe=da=zos|35 +presteza|pres=te=za|35 +profesión|pro=fe=sión|35 +prosigue|pro=si=gue|35 +quedaba|que=da=ba|35 +querido|que=ri=do|35 +Ricote|Ri=co=te|35 +También|Tam=bién|35 +tenemos|te=ne=mos|35 +asimesmo|asi=mes=mo|34 +ausencia|au=sen=cia|34 +cualquiera|cual=quie=ra|34 +dellas|de=llas|34 +facilidad|fa=ci=li=dad|34 +hacerse|ha=cer=se|34 +llevaban|lle=va=ban|34 +oyendo|oyen=do|34 +parecen|pa=re=cen|34 +soldado|sol=da=do|34 +sombra|som=bra|34 +Viendo|Vien=do|34 +alforjas|al=for=jas|33 +andado|an=da=do|33 +blanca|blan=ca|33 +cualquier|cual=quier|33 +cuello|cue=llo|33 +decirse|de=cir=se|33 +dejaba|de=ja=ba|33 +encantamento|en=can=ta=men=to|33 +estamos|es=ta=mos|33 +naturaleza|na=tu=ra=le=za|33 +número|nú=me=ro|33 +ponerse|po=ner=se|33 +recibió|re=ci=bió|33 +refranes|re=fra=nes|33 +seguir|se=guir|33 +segundo|se=gun=do|33 +sepultura|se=pul=tu=ra|33 +temeroso|te=me=ro=so|33 +burlas|bur=las|32 +condesa|con=de=sa|32 +deseaba|de=sea=ba|32 +esperaba|es=pe=ra=ba|32 +hablando|ha=blan=do|32 +mancebo|man=ce=bo|32 +mandado|man=da=do|32 +merece|me=re=ce|32 +pláticas|plá=ti=cas|32 +principales|prin=ci=pa=les|32 +puedes|pue=des|32 +queréis|que=réis|32 +recebir|re=ce=bir|32 +riendas|rien=das|32 +siquiera|si=quie=ra|32 +adónde|adón=de|31 +caminar|ca=mi=nar|31 +crédito|cré=di=to|31 +dejaron|de=ja=ron|31 +dejase|de=ja=se|31 +derecho|de=re=cho|31 +famosos|fa=mo=sos|31 +Francia|Fran=cia|31 +ganado|ga=na=do|31 +gracia|gra=cia|31 +haberle|ha=ber=le|31 +Maritornes|Ma=ri=tor=nes|31 +momento|mo=men=to|31 +particular|par=ti=cu=lar|31 +príncipes|prín=ci=pes|31 +quisiese|qui=sie=se|31 +respeto|res=pe=to|31 +salieron|salie=ron|31 +sosiego|so=sie=go|31 +suspiros|sus=pi=ros|31 +valentía|va=len=tía|31 +ventana|ven=ta=na|31 +volviéndose|vol=vién=do=se|31 +vuelta|vuel=ta|31 +acometer|aco=me=ter|30 +acudir|acu=dir|30 +conoció|co=no=ció|30 +cuentan|cuen=tan|30 +descubierto|des=cu=bier=to|30 +determinado|de=ter=mi=na=do|30 +diferentes|di=fe=ren=tes|30 +doctor|doc=tor|30 +encantada|en=can=ta=da|30 +estraña|es=tra=ña|30 +forzoso|for=zo=so|30 +gigantes|gi=gan=tes|30 +guarda|guar=da|30 +hacerle|ha=cer=le|30 +hábito|há=bi=to|30 +llegaba|lle=ga=ba|30 +llorar|llo=rar|30 +Lorenzo|Lo=ren=zo|30 +prometido|pro=me=ti=do|30 +salida|sali=da|30 +servicio|ser=vi=cio|30 +sobresalto|so=bre=sal=to|30 +soldados|sol=da=dos|30 +Trifaldi|Tri=fal=di|30 +ventera|ven=te=ra|30 +verdaderas|ver=da=de=ras|30 +vizcaíno|viz=caí=no|30 +volviese|vol=vie=se|30 +artificio|ar=ti=fi=cio|29 +decirle|de=cir=le|29 +determinación|de=ter=mi=na=ción|29 +entraron|en=tra=ron|29 +Grisóstomo|Gri=sós=to=mo|29 +hombros|hom=bros|29 +levantar|le=van=tar|29 +mesmos|mes=mos|29 +parecido|pa=re=ci=do|29 +pendencia|pen=den=cia|29 +poetas|poe=tas|29 +puertas|puer=tas|29 +tomado|to=ma=do|29 +términos|tér=mi=nos|29 +villano|vi=lla=no|29 +volviendo|vol=vien=do|29 +acabado|aca=ba=do|28 +agradable|agra=da=ble|28 +calidad|ca=li=dad|28 +compadre|com=pa=dre|28 +confuso|con=fu=so|28 +derecha|de=re=cha|28 +Espejos|Es=pe=jos|28 +famosa|fa=mo=sa|28 +gobernar|go=ber=nar|28 +hablado|ha=bla=do|28 +infanta|in=fan=ta|28 +infierno|in=fierno|28 +Marcela|Mar=ce=la|28 +mostró|mos=tró|28 +muchacho|mu=cha=cho|28 +nacido|na=ci=do|28 +narices|na=ri=ces|28 +nombres|nom=bres|28 +nuevos|nue=vos|28 +Oyendo|Oyen=do|28 +pareciéndole|pa=re=cién=do=le|28 +partida|par=ti=da|28 +pequeño|pe=que=ño|28 +promesas|pro=me=sas|28 +quiera|quie=ra|28 +quisieres|qui=sie=res|28 +retablo|re=ta=blo|28 +rostros|ros=tros|28 +secreto|se=cre=to|28 +solamente|so=la=men=te|28 +suspenso|sus=pen=so|28 +Tosilos|To=si=los|28 +vosotros|vo=so=tros|28 +ínsulas|ín=su=las|28 +último|úl=ti=mo|28 +acerca|acer=ca|27 +aliento|alien=to|27 +andaban|an=da=ban|27 +capítulo|ca=pí=tu=lo|27 +carnes|car=nes|27 +cartas|car=tas|27 +comenzaron|co=men=za=ron|27 +conviene|con=vie=ne|27 +cuerdo|cuer=do|27 +despecho|des=pe=cho|27 +determinó|de=ter=mi=nó|27 +electronic|elec=tro=nic|27 +empresa|em=pre=sa|27 +escudo|es=cu=do|27 +esperanzas|es=pe=ran=zas|27 +estuviese|es=tu=vie=se|27 +estómago|es=tó=ma=go|27 +gracioso|gra=cio=so|27 +hallaba|ha=lla=ba|27 +hiciera|hi=cie=ra|27 +imaginar|ima=gi=nar|27 +lugares|lu=ga=res|27 +ofreció|ofre=ció|27 +pasada|pa=sa=da|27 +reinos|rei=nos|27 +respondía|res=pon=día|27 +semejante|se=me=jan=te|27 +simple|sim=ple|27 +tampoco|tam=po=co|27 +tratar|tra=tar|27 +traído|traí=do|27 +treinta|trein=ta|27 +admirados|ad=mi=ra=dos|26 +atrevido|atre=vi=do|26 +comodidad|co=mo=di=dad|26 +cuantas|cuan=tas|26 +desnudo|des=nu=do|26 +diablos|dia=blos|26 +estraño|es=tra=ño|26 +estudiante|es=tu=dian=te|26 +hacerme|ha=cer=me|26 +hermoso|her=mo=so|26 +honesta|ho=nes=ta|26 +impertinente|im=per=ti=nen=te|26 +lacayo|la=ca=yo|26 +ladrón|la=drón|26 +llevan|lle=van|26 +malicia|ma=li=cia|26 +Merlín|Mer=lín|26 +médico|mé=di=co|26 +penitencia|pe=ni=ten=cia|26 +pensativo|pen=sa=ti=vo|26 +pequeña|pe=que=ña|26 +piernas|pier=nas|26 +puestos|pues=tos|26 +quedan|que=dan|26 +suplico|su=pli=co|26 +valientes|va=lien=tes|26 +viéndose|vién=do=se|26 +bosque|bos=que|25 +caballos|ca=ba=llos|25 +castellano|cas=te=llano|25 +cierta|cier=ta|25 +conforme|con=for=me|25 +consejos|con=se=jos|25 +desventura|des=ven=tu=ra|25 +díjole|dí=jo=le|25 +entrada|en=tra=da|25 +entrado|en=tra=do|25 +Gregorio|Gre=go=rio|25 +Iglesia|Igle=sia|25 +ingenioso|in=ge=nio=so|25 +juramento|ju=ra=men=to|25 +limpio|lim=pio|25 +maestresala|ma=es=tre=sa=la|25 +Majestad|Ma=jes=tad|25 +milagro|mi=la=gro|25 +mostrar|mos=trar|25 +necesario|ne=ce=sa=rio|25 +pecado|pe=ca=do|25 +piedra|pie=dra|25 +pobres|po=bres|25 +quitado|qui=ta=do|25 +recebido|re=ce=bi=do|25 +sentido|sen=ti=do|25 +tendido|ten=di=do|25 +tendré|ten=dré|25 +tercera|ter=ce=ra|25 +turcos|tur=cos|25 +vuelva|vuel=va|25 +vuelve|vuel=ve|25 +acudió|acu=dió|24 +admirado|ad=mi=ra=do|24 +alabanzas|ala=ban=zas|24 +alcanza|al=can=za|24 +aparte|apar=te|24 +Apenas|Ape=nas|24 +apriesa|aprie=sa|24 +aquélla|aqué=lla|24 +atención|aten=ción|24 +bastante|bas=tan=te|24 +breves|bre=ves|24 +cielos|cie=los|24 +compañeros|com=pa=ñe=ros|24 +cuadrilleros|cua=dri=lle=ros|24 +desencanto|des=en=can=to|24 +desgracias|des=gra=cias|24 +dígame|dí=ga=me|24 +echado|echa=do|24 +encantados|en=can=ta=dos|24 +espero|es=pe=ro|24 +estancia|es=tan=cia|24 +felice|fe=li=ce|24 +Foundation|Foun=da=tion|24 +gobernadores|go=ber=na=do=res|24 +honrada|hon=ra=da|24 +honrado|hon=ra=do|24 +importancia|im=por=tan=cia|24 +infinitos|in=fi=ni=tos|24 +invención|in=ven=ción|24 +ligereza|li=ge=re=za|24 +llegándose|lle=gán=do=se|24 +mentecato|men=te=ca=to|24 +ordenó|or=de=nó|24 +patria|pa=tria|24 +pechos|pe=chos|24 +perdido|per=di=do|24 +ponerme|po=ner=me|24 +propio|pro=pio|24 +satisfecho|sa=tis=fe=cho|24 +sentado|sen=ta=do|24 +talante|ta=lan=te|24 +tenéis|te=néis|24 +tomando|to=man=do|24 +traigo|trai=go|24 +traían|traían|24 +trecho|tre=cho|24 +vecino|ve=cino|24 +acertado|acer=ta=do|23 +además|ade=más|23 +arremetió|arre=me=tió|23 +blancas|blan=cas|23 +cantidad|canti=dad|23 +conocimiento|co=no=ci=mien=to|23 +convenía|con=ve=nía|23 +corral|co=rral|23 +dijeron|di=je=ron|23 +discretos|dis=cre=tos|23 +entendió|en=ten=dió|23 +golpes|gol=pes|23 +habido|ha=bi=do|23 +importa|im=por=ta|23 +intento|in=ten=to|23 +juntamente|jun=ta=men=te|23 +liberal|li=be=ral|23 +parecían|pa=re=cían|23 +piensa|pien=sa|23 +pobreza|po=bre=za|23 +preguntar|pre=gun=tar|23 +romance|ro=man=ce|23 +saliese|salie=se|23 +señoría|se=ño=ría|23 +teniendo|te=nien=do|23 +venida|ve=ni=da|23 +vergüenza|ver=güen=za|23 +visorrey|vi=so=rrey|23 +viéndole|vién=do=le|23 +volvía|vol=vía|23 +alegría|ale=g=ría|22 +añadió|aña=dió|22 +cabras|ca=bras|22 +corona|co=ro=na|22 +desdichada|des=di=cha=da|22 +desenvoltura|des=en=vol=tu=ra|22 +diversas|di=ver=sas|22 +ejecución|eje=cu=ción|22 +ejemplo|ejem=plo|22 +escuchando|es=cu=chan=do|22 +Estando|Es=tan=do|22 +estáis|es=táis|22 +Excelencia|Ex=ce=len=cia|22 +faltar|fal=tar|22 +haberse|ha=ber=se|22 +humilde|hu=mil=de|22 +Hízolo|Hí=zo=lo|22 +lenguas|len=guas|22 +mostrado|mos=tra=do|22 +muerta|muer=ta|22 +ordinario|or=di=na=rio|22 +papeles|pa=pe=les|22 +pecador|pe=ca=dor|22 +pintado|pin=ta=do|22 +podéis|po=déis|22 +ponerle|po=ner=le|22 +pusiese|pu=sie=se|22 +quince|quin=ce|22 +refrán|re=frán|22 +riquezas|ri=que=zas|22 +Roldán|Rol=dán|22 +sacado|saca=do|22 +sintió|sin=tió|22 +vencimiento|ven=ci=mien=to|22 +vestida|ves=ti=da|22 +vinieron|vi=nie=ron|22 +yerbas|yer=bas|22 +atento|aten=to|21 +atrevimiento|atre=vi=mien=to|21 +bellaco|be=lla=co|21 +Blanca|Blan=ca|21 +calles|ca=lles|21 +confusión|con=fu=sión|21 +debían|de=bían|21 +defensa|de=fen=sa|21 +descuido|des=cui=do|21 +diferencia|di=fe=ren=cia|21 +dijera|di=je=ra|21 +discretas|dis=cre=tas|21 +dándole|dán=do=le|21 +entiende|en=tien=de|21 +escribió|es=cri=bió|21 +escritos|es=cri=tos|21 +estilo|es=ti=lo|21 +figuras|fi=gu=ras|21 +galeotes|ga=leo=tes|21 +Hermandad|Her=man=dad|21 +justas|jus=tas|21 +labios|la=bios|21 +leones|leo=nes|21 +llegue|lle=gue|21 +mandar|man=dar|21 +misericordia|mi=se=ri=cor=dia|21 +máquina|má=qui=na|21 +ofrecimientos|ofre=ci=mien=tos|21 +pagado|pa=ga=do|21 +pastora|pas=to=ra|21 +perjuicio|per=jui=cio|21 +prudente|pru=den=te|21 +prueba|prue=ba|21 +puestas|pues=tas|21 +Sanchica|San=chi=ca|21 +sentencias|sen=ten=cias|21 +sentimiento|sen=ti=mien=to|21 +Sierra|Sie=rra|21 +soledad|so=le=dad|21 +suspensos|sus=pen=sos|21 +temerosa|te=me=ro=sa|21 +temiendo|te=mien=do|21 +traidor|trai=dor|21 +vendrá|ven=drá|21 +Zaragoza|Za=ra=go=za|21 +arriero|arrie=ro|20 +arrojó|arro=jó|20 +batallas|ba=ta=llas|20 +brevedad|bre=ve=dad|20 +bronce|bron=ce|20 +caballeriza|ca=ba=lle=ri=za|20 +campos|cam=pos|20 +ciencia|cien=cia|20 +comedia|co=me=dia|20 +comido|co=mi=do|20 +contigo|con=ti=go|20 +costillas|cos=ti=llas|20 +cubierto|cu=bier=to|20 +descubrió|des=cu=brió|20 +detrás|de=trás|20 +diesen|die=sen|20 +enamorados|ena=mo=ra=dos|20 +encierra|en=cie=rra|20 +entendido|en=ten=di=do|20 +espanto|es=pan=to|20 +estuviera|es=tu=vie=ra|20 +faltaba|fal=ta=ba|20 +gallardo|ga=llar=do|20 +hierro|hie=rro|20 +ignorante|ig=no=ran=te|20 +lienzo|lien=zo|20 +lástima|lás=ti=ma|20 +maravilla|ma=ra=vi=lla|20 +maravillas|ma=ra=vi=llas|20 +miraba|mi=ra=ba|20 +miraban|mi=ra=ban|20 +montaña|mon=ta=ña|20 +movido|mo=vi=do|20 +música|mú=si=ca|20 +poderoso|po=de=ro=so|20 +precio|pre=cio|20 +pregunta|pre=gun=ta|20 +prudencia|pru=den=cia|20 +quedado|que=da=do|20 +recato|re=ca=to|20 +sabían|sa=bían|20 +segura|se=gu=ra|20 +suceder|su=ce=der|20 +Sucedió|Su=ce=dió|20 +sujeto|su=je=to|20 +trecientos|tre=cien=tos|20 +vencedor|ven=ce=dor|20 +verdaderos|ver=da=de=ros|20 +viniere|vi=nie=re|20 +Álvaro|Ál=va=ro|20 +acciones|ac=cio=nes|19 +adarga|adar=ga|19 +agravios|agra=vios|19 +Andrés|An=drés|19 +autores|au=to=res|19 +bendición|ben=di=ción|19 +Benengeli|Be=nen=ge=li|19 +bestias|bes=tias|19 +blanda|blan=da|19 +camisa|ca=mi=sa|19 +carrera|ca=rre=ra|19 +carreta|ca=rre=ta|19 +casado|ca=sa=do|19 +cautivos|cau=ti=vos|19 +ceremonias|ce=re=mo=nias|19 +circunstantes|cir=cuns=tan=tes|19 +Claudia|Clau=dia|19 +Clavileño|Cla=vi=le=ño|19 +comida|co=mi=da|19 +compañero|com=pa=ñe=ro|19 +copyright|co=p=y=ri=ght|19 +Cuanto|Cuan=to|19 +cueros|cue=ros|19 +defender|de=fen=der|19 +demonio|de=mo=nio|19 +desmayada|des=ma=ya=da|19 +desventuras|des=ven=tu=ras|19 +docientos|do=cien=tos|19 +durmiendo|dur=mien=do|19 +espadas|es=pa=das|19 +espíritu|es=píri=tu|19 +estrecheza|es=tre=che=za|19 +estrellas|es=tre=llas|19 +fortaleza|for=ta=le=za|19 +hechas|he=chas|19 +hubieran|hu=bie=ran|19 +humana|hu=ma=na|19 +humano|hu=ma=no|19 +imaginaba|ima=gi=na=ba|19 +improviso|im=pro=vi=so|19 +liberalidad|li=be=ra=li=dad|19 +llenas|lle=nas|19 +Melisendra|Me=li=sen=dra|19 +menudo|me=nu=do|19 +mientras|mien=tras|19 +mirado|mi=ra=do|19 +miserable|mi=se=ra=ble|19 +Morena|More=na|19 +muelas|mue=las|19 +nación|na=ción|19 +olvido|ol=vi=do|19 +peligros|pe=li=gros|19 +pensado|pen=sa=do|19 +persiguen|per=si=guen|19 +piedras|pie=dras|19 +poniendo|po=nien=do|19 +posesión|po=se=sión|19 +primeros|pri=me=ros|19 +pudieran|pu=die=ran|19 +pudiere|pu=die=re|19 +quedará|que=da=rá|19 +secretario|se=cre=ta=rio|19 +Sevilla|Se=vi=lla|19 +sufrir|su=frir|19 +tengan|ten=gan|19 +testamento|tes=ta=men=to|19 +tocaba|to=ca=ba|19 +trance|tran=ce|19 +veréis|ve=réis|19 +virtudes|vir=tu=des|19 +acabada|aca=ba=da|18 +alegres|ale=gres|18 +aquéllos|aqué=llos|18 +bienes|bienes|18 +bálsamo|bál=samo|18 +cabreros|ca=bre=ros|18 +campaña|cam=pa=ña|18 +contenta|con=ten=ta|18 +cortés|cor=tés|18 +cuerpos|cuer=pos|18 +desdichas|des=di=chas|18 +desear|de=sear|18 +despojos|des=po=jos|18 +ducados|du=ca=dos|18 +engañado|en=ga=ña=do|18 +escondido|es=con=di=do|18 +escuadrón|es=cua=drón|18 +herida|he=ri=da|18 +hermanos|her=ma=nos|18 +honesto|ho=nes=to|18 +hubiere|hu=bie=re|18 +huesos|hue=sos|18 +lanzón|lan=zón|18 +Leandra|Lean=dra|18 +levantándose|le=van=tán=do=se|18 +llamada|lla=ma=da|18 +Llegóse|Lle=gó=se|18 +llenos|lle=nos|18 +Malambruno|Ma=lam=bruno|18 +manteles|man=te=les|18 +montañas|mon=ta=ñas|18 +necedades|ne=ce=da=des|18 +Nicolás|Ni=co=lás|18 +Nuestro|Nues=tro|18 +parientes|pa=rien=tes|18 +Paréceme|Pa=ré=ce=me|18 +pecados|pe=ca=dos|18 +perdón|per=dón|18 +persuadir|per=sua=dir|18 +premio|pre=mio|18 +probar|pro=bar|18 +profundo|pro=fun=do|18 +promesa|pro=me=sa|18 +prometida|pro=me=ti=da|18 +prometió|pro=me=tió|18 +proseguir|pro=se=guir|18 +querían|que=rían|18 +reposo|re=po=so|18 +ruegos|rue=gos|18 +sabido|sa=bi=do|18 +salario|sa=la=rio|18 +servicios|ser=vi=cios|18 +Verdad|Ver=dad|18 +Válame|Vá=la=me|18 +abundancia|abun=dan=cia|17 +acordó|acor=dó|17 +agreement|agree=ment|17 +amantes|aman=tes|17 +antigua|an=ti=gua|17 +arzobispo|ar=zo=bis=po|17 +ausente|au=sen=te|17 +Barcelona|Bar=ce=lo=na|17 +Berbería|Ber=be=ría|17 +bueyes|bue=yes|17 +cadena|ca=de=na|17 +castigo|cas=ti=go|17 +comenzado|co=men=za=do|17 +compasión|com=pa=sión|17 +compuesto|com=pues=to|17 +consideración|con=si=de=ra=ción|17 +continente|con=ti=nen=te|17 +corría|co=rría|17 +curiosidad|cu=rio=si=dad|17 +cuándo|cuán=do|17 +cuántas|cuán=tas|17 +descubrieron|des=cu=brie=ron|17 +deshora|des=ho=ra|17 +desmayo|des=ma=yo|17 +despertó|des=per=tó|17 +discursos|dis=cur=sos|17 +enamorada|ena=mo=ra=da|17 +encantamentos|en=can=ta=men=tos|17 +encina|en=ci=na|17 +enfermedad|en=fer=me=dad|17 +entera|en=te=ra|17 +escribano|es=cri=bano|17 +espejo|es=pe=jo|17 +estruendo|es=truen=do|17 +estuvieron|es=tu=vie=ron|17 +favorecer|fa=vo=re=cer|17 +flores|flo=res|17 +fuente|fuen=te|17 +garganta|gar=gan=ta|17 +hallarse|ha=llar=se|17 +heridas|he=ri=das|17 +huyendo|hu=yen=do|17 +iguales|igua=les|17 +llegase|lle=ga=se|17 +llevaron|lle=va=ron|17 +malandrines|ma=lan=dri=nes|17 +Mambrino|Mam=brino|17 +mandamiento|man=da=mien=to|17 +maravedís|ma=ra=ve=dís|17 +menesterosos|me=nes=te=ro=sos|17 +Micomicona|Mi=co=mi=co=na|17 +mortal|mor=tal|17 +mármol|már=mol|17 +ofrecía|ofre=cía|17 +parecerle|pa=re=cer=le|17 +pasase|pa=sa=se|17 +podido|po=di=do|17 +poesía|poesía|17 +prometo|pro=me=to|17 +puerto|puer=to|17 +puntualidad|pun=tua=li=dad|17 +quedase|que=da=se|17 +recado|re=ca=do|17 +regalo|re=ga=lo|17 +responde|res=pon=de|17 +santos|san=tos|17 +seguridad|se=gu=ri=dad|17 +seiscientos|seis=cien=tos|17 +sentidos|sen=ti=dos|17 +siguiente|si=guien=te|17 +Soneto|So=ne=to|17 +sospecha|sos=pe=cha|17 +supiese|su=pie=se|17 +tendría|ten=dría|17 +terrible|te=rri=ble|17 +usanza|usan=za|17 +Vicente|Vi=cen=te|17 +vuelvo|vuel=vo|17 +adorno|adorno|16 +Ambrosio|Am=bro=sio|16 +ansimesmo|an=si=mes=mo|16 +antiguos|an=ti=guos|16 +atentamente|aten=ta=men=te|16 +cantar|can=tar|16 +ciento|cien=to|16 +cincuenta|cin=cuen=ta|16 +colores|co=lo=res|16 +comedias|co=me=dias|16 +concierto|con=cier=to|16 +conocía|co=no=cía|16 +contando|con=tan=do|16 +contentos|con=ten=tos|16 +costumbres|cos=tum=bres|16 +cuesta|cues=ta|16 +dejaré|de=ja=ré|16 +demasiadamente|de=ma=sia=da=men=te|16 +detuvo|de=tu=vo|16 +dijere|di=je=re|16 +disparate|dis=pa=ra=te|16 +dondequiera|don=de=quie=ra|16 +ejercicios|ejer=ci=cios|16 +entero|en=te=ro|16 +entrambas|en=tram=bas|16 +envidia|en=vi=dia|16 +escribe|es=cri=be|16 +espuelas|es=pue=las|16 +Estaba|Es=ta=ba|16 +estacas|es=ta=cas|16 +Gaiferos|Gai=fe=ros|16 +habilidad|ha=bi=li=dad|16 +hablaba|ha=bla=ba|16 +habrán|ha=brán|16 +imprimir|im=pri=mir|16 +labradores|la=bra=do=res|16 +llevando|lle=van=do|16 +límites|lí=mi=tes|16 +maleta|ma=le=ta|16 +mandaba|man=da=ba|16 +Marién|Ma=rién|16 +marqués|mar=qués|16 +muchachos|mu=cha=chos|16 +naturales|na=tu=ra=les|16 +novela|no=ve=la|16 +parezca|pa=rez=ca|16 +pasaban|pa=sa=ban|16 +placer|pla=cer|16 +pondré|pon=dré|16 +príncipe|prín=ci=pe|16 +pudieron|pu=die=ron|16 +quejas|que=jas|16 +quisieren|qui=sie=ren|16 +quisiéredes|qui=sié=re=des|16 +respondido|res=pon=di=do|16 +respondiese|res=pon=die=se|16 +sabéis|sa=béis|16 +sosegado|so=se=ga=do|16 +tesoro|te=so=ro|16 +tiento|tien=to|16 +tristeza|tris=te=za|16 +ventaja|ven=ta=ja|16 +verano|ve=rano|16 +verdades|ver=da=des|16 +volverse|vol=ver=se|16 +abrazó|abra=zó|15 +ajenas|aje=nas|15 +alabanza|ala=ban=za|15 +alteza|al=te=za|15 +amante|aman=te|15 +amparo|am=pa=ro|15 +asiento|asien=to|15 +asiéndole|asién=do=le|15 +autoridad|au=to=ri=dad|15 +averiguar|ave=ri=guar|15 +bellotas|be=llo=tas|15 +buscarle|bus=car=le|15 +cabestro|ca=bes=tro|15 +castigar|cas=ti=gar|15 +cerrada|ce=rra=da|15 +cerrar|ce=rrar|15 +claridad|cla=ri=dad|15 +cobrar|co=brar|15 +condado|con=da=do|15 +confesar|con=fe=sar|15 +consintió|con=sin=tió|15 +consuelo|con=sue=lo|15 +contornos|con=tor=nos|15 +cuadrillero|cua=dri=lle=ro|15 +cuchilladas|cu=chi=lla=das|15 +decoro|de=co=ro|15 +dejará|de=ja=rá|15 +dejasen|de=ja=sen|15 +descubrir|des=cu=brir|15 +deshonra|des=hon=ra|15 +dignas|dig=nas|15 +duerme|duer=me|15 +dádivas|dádi=vas|15 +entrando|en=tran=do|15 +entretenimiento|en=tre=te=ni=mien=to|15 +escrúpulo|es=crú=pu=lo|15 +escuchaba|es=cu=cha=ba|15 +estima|es=ti=ma|15 +fermosa|fer=mo=sa|15 +fermosura|fer=mo=su=ra|15 +frente|fren=te|15 +gentileza|gen=ti=le=za|15 +grandezas|gran=de=zas|15 +hermosas|her=mo=sas|15 +humildad|hu=mil=dad|15 +huéspedes|huéspe=des|15 +imagen|ima=gen|15 +infinitas|in=fi=ni=tas|15 +ingrata|in=gra=ta|15 +jornada|jor=na=da|15 +leonero|leo=ne=ro|15 +libres|li=bres|15 +llegan|lle=gan|15 +Llegaron|Lle=ga=ron|15 +mentir|men=tir|15 +mentiras|men=ti=ras|15 +mirase|mi=ra=se|15 +montes|mon=tes|15 +negros|ne=gros|15 +niñerías|ni=ñe=rías|15 +noches|no=ches|15 +original|ori=gi=nal|15 +palacios|pa=la=cios|15 +pedido|pe=di=do|15 +pensando|pen=san=do|15 +perdición|per=di=ción|15 +prendas|pren=das|15 +procura|pro=cu=ra|15 +propuso|pro=pu=so|15 +puntas|pun=tas|15 +puntos|pun=tos|15 +redonda|re=don=da|15 +remediar|re=me=diar|15 +rescate|res=ca=te|15 +sabiendo|sa=bien=do|15 +sacaron|sa=ca=ron|15 +Salamanca|Sa=la=man=ca|15 +satisfación|sa=tis=fa=ción|15 +serían|se=rían|15 +sierra|sie=rra|15 +simplicidad|sim=pli=ci=dad|15 +sirven|sir=ven|15 +States|Sta=tes|15 +sustentar|sus=ten=tar|15 +tuertos|tuer=tos|15 +United|United|15 +vasallos|va=sa=llos|15 +viesen|vie=sen|15 +virrey|vi=rrey|15 +vitoria|vi=to=ria|15 +vuelven|vuel=ven|15 +acomodó|aco=mo=dó|14 +acudieron|acu=die=ron|14 +afrenta|afren=ta|14 +ahínco|ahín=co|14 +alcanzó|al=can=zó|14 +alzando|al=zan=do|14 +animal|ani=mal|14 +antiguo|an=ti=guo|14 +apartó|apar=tó|14 +apearse|apear=se|14 +arrogante|arro=gan=te|14 +arroyo|arro=yo|14 +añadidura|aña=di=du=ra|14 +báculo|bá=cu=lo|14 +caminante|ca=mi=nan=te|14 +castellana|cas=te=lla=na|14 +Cecial|Ce=cial|14 +ciertos|cier=tos|14 +cobarde|co=bar=de|14 +coloquio|co=lo=quio|14 +comedido|co=me=di=do|14 +confieso|con=fie=so|14 +conocidos|co=no=ci=dos|14 +Consejo|Con=se=jo|14 +contiene|con=tie=ne|14 +corrido|co=rri=do|14 +cuánto|cuán=to|14 +cuántos|cuán=tos|14 +cárcel|cár=cel|14 +decirte|de=cir=te|14 +dejaban|de=ja=ban|14 +docena|do=ce=na|14 +encanto|en=can=to|14 +enfermo|en=fer=mo|14 +entretener|en=tre=te=ner|14 +estrella|es=tre=lla|14 +faltan|fal=tan|14 +fiesta|fies=ta|14 +fiestas|fies=tas|14 +fuentes|fuen=tes|14 +graves|gra=ves|14 +guarde|guar=de|14 +hallará|ha=lla=rá|14 +izquierdo|iz=quier=do|14 +juzgar|juz=gar|14 +levantado|le=van=ta=do|14 +levantaron|le=van=ta=ron|14 +ligero|li=ge=ro|14 +Madrid|Ma=drid|14 +maestro|ma=es=tro|14 +medias|me=dias|14 +milagros|mi=la=gros|14 +mismas|mis=mas|14 +mismos|mis=mos|14 +muchacha|mu=cha=cha|14 +músico|mú=si=co|14 +olvidado|ol=vi=da=do|14 +ovejas|ove=jas|14 +palacio|pa=la=cio|14 +paraba|pa=ra=ba|14 +pasadas|pa=sa=das|14 +Pasamonte|Pa=samon=te|14 +pelear|pe=lear|14 +pendencias|pen=den=cias|14 +pidiese|pi=die=se|14 +pierna|pier=na|14 +principalmente|prin=ci=pal=men=te|14 +prisión|pri=sión|14 +procesión|pro=ce=sión|14 +pudiesen|pu=die=sen|14 +puñadas|pu=ña=das|14 +quedaré|que=da=ré|14 +quienquiera|quien=quie=ra|14 +razonable|ra=zo=na=ble|14 +referido|re=fe=ri=do|14 +república|re=pú=bli=ca|14 +rienda|rien=da|14 +riqueza|ri=que=za|14 +sentencia|sen=ten=cia|14 +sentir|sen=tir|14 +sesenta|se=s=en=ta|14 +siguieron|si=guie=ron|14 +siguió|si=guió|14 +testigo|tes=ti=go|14 +testigos|tes=ti=gos|14 +tomase|to=ma=se|14 +tristes|tris=tes|14 +tuviere|tu=vie=re|14 +vender|ven=der|14 +viniesen|vi=nie=sen|14 +vuesas|vue=sas|14 +zapatos|za=pa=tos|14 +abierto|abier=to|13 +abismo|abis=mo|13 +acabando|aca=ban=do|13 +acomodado|aco=mo=da=do|13 +agradecido|agra=de=ci=do|13 +alcornoque|al=cor=no=que|13 +amenazas|ame=na=zas|13 +anduvo|an=du=vo|13 +apeándose|apeán=do=se|13 +Archive|Ar=chi=ve|13 +atónito|ató=ni=to|13 +añadir|aña=dir|13 +blando|blan=do|13 +buscaba|bus=ca=ba|13 +canalla|ca=na=lla|13 +cansado|can=sa=do|13 +cantando|can=tan=do|13 +capellán|ca=pe=llán|13 +católico|ca=tó=li=co|13 +cebada|ce=ba=da|13 +claras|cla=ras|13 +compuso|com=pu=so|13 +condiciones|con=di=cio=nes|13 +conocida|co=no=ci=da|13 +considerando|con=si=de=ran=do|13 +conversación|con=ver=sación|13 +corales|co=ra=les|13 +corriendo|co=rrien=do|13 +criadas|cria=das|13 +cumplido|cum=pli=do|13 +curioso|cu=rio=so|13 +darles|dar=les|13 +decirme|de=cir=me|13 +denuedo|de=nue=do|13 +descubre|des=cu=bre|13 +desesperado|des=es=pe=ra=do|13 +dichoso|di=cho=so|13 +dignos|dig=nos|13 +diligencias|di=li=gen=cias|13 +dormía|dor=mía|13 +Díjole|Dí=jo=le|13 +echando|echan=do|13 +emperadores|em=pe=ra=do=res|13 +encerrado|en=ce=rra=do|13 +entereza|en=te=re=za|13 +escritas|es=cri=tas|13 +escuchar|es=cu=char|13 +escusar|es=cu=sar|13 +estará|es=ta=rá|13 +estimar|es=ti=mar|13 +estraños|es=tra=ños|13 +fantasmas|fan=tas=mas|13 +fatiga|fa=ti=ga|13 +Fortuna|For=tu=na|13 +frailes|frai=les|13 +fuertemente|fuer=te=men=te|13 +grandísima|gran=dí=si=ma|13 +gravedad|gra=ve=dad|13 +gritos|gri=tos|13 +habemos|ha=be=mos|13 +habría|ha=bría|13 +herido|he=ri=do|13 +historiador|his=to=ria=dor|13 +imitación|imi=ta=ción|13 +imitar|imi=tar|13 +instrumentos|ins=tru=men=tos|13 +juntas|jun=tas|13 +lanzas|lan=zas|13 +Leones|Leo=nes|13 +leyendo|le=yen=do|13 +leyese|le=ye=se|13 +ligera|li=ge=ra|13 +limpia|lim=pia|13 +Literary|Li=te=ra=ry|13 +llamando|lla=man=do|13 +llevase|lle=va=se|13 +maldiciones|mal=di=cio=nes|13 +manifiesto|ma=ni=fies=to|13 +miembros|miem=bros|13 +miente|mien=te|13 +molinos|mo=li=nos|13 +Muerte|Muer=te|13 +nombrar|nom=brar|13 +partió|par=tió|13 +piense|pien=se|13 +pierda|pier=da|13 +pliego|plie=go|13 +podrán|po=drán|13 +prados|pra=dos|13 +preguntado|pre=gun=ta=do|13 +Preguntó|Pre=gun=tó|13 +principios|prin=ci=pios|13 +procuraba|pro=cu=ra=ba|13 +propia|pro=pia|13 +puedan|pue=dan|13 +público|pú=bli=co|13 +quedara|que=da=ra|13 +QUIJOTE|QUI=JO=TE|13 +quisieron|qui=sie=ron|13 +rebuzno|re=buzno|13 +región|re=gión|13 +relación|re=la=ción|13 +respondí|res=pon=dí|13 +reverencia|re=ve=ren=cia|13 +romper|rom=per|13 +sacando|sacan=do|13 +sandeces|san=de=ces|13 +Segunda|Se=gun=da|13 +sentía|sen=tía|13 +Señora|Se=ño=ra|13 +siento|sien=to|13 +simples|sim=ples|13 +subieron|su=bie=ron|13 +suspiro|sus=pi=ro|13 +tardanza|tar=dan=za|13 +trademark|tra=de=ma=rk|13 +venideros|ve=ni=de=ros|13 +verdes|ver=des|13 +visera|vi=se=ra|13 +abierta|abier=ta|12 +abrazar|abra=zar|12 +acabase|aca=ba=se|12 +acertó|acer=tó|12 +acullá|acu=llá|12 +advertir|ad=ver=tir|12 +Adónde|Adón=de|12 +agradezco|agra=dez=co|12 +aguardar|aguar=dar|12 +agujero|agu=je=ro|12 +alcalde|al=cal=de|12 +almohada|al=moha=da|12 +amorosa|amo=ro=sa|12 +andando|an=dan=do|12 +apacible|apa=ci=ble|12 +apartado|apar=ta=do|12 +apostaré|apos=ta=ré|12 +armada|ar=ma=da|12 +armados|ar=ma=dos|12 +ayudar|ayu=dar|12 +Barataria|Ba=ra=ta=ria|12 +bardas|bar=das|12 +callando|ca=llan=do|12 +callar|ca=llar|12 +casamiento|ca=sa=mien=to|12 +casarse|ca=sar=se|12 +ciudades|ciu=da=des|12 +comenzar|co=men=zar|12 +concertado|con=cer=ta=do|12 +conocían|co=no=cían|12 +corteses|cor=te=ses|12 +costal|cos=tal|12 +cubierta|cu=bier=ta|12 +cuentos|cuen=tos|12 +cuáles|cuá=les|12 +cédula|cé=du=la|12 +dejarse|de=jar=se|12 +dejemos|de=je=mos|12 +demanda|de=man=da|12 +descubriese|des=cu=brie=se|12 +desdeñado|des=de=ña=do|12 +desdicha|des=di=cha|12 +desengaño|des=en=ga=ño|12 +despacio|des=pa=cio|12 +donations|do=na=tions|12 +Dígolo|Dí=go=lo|12 +encantos|en=can=tos|12 +encubrir|en=cu=brir|12 +enemiga|ene=mi=ga|12 +entienda|en=tien=da|12 +Entonces|En=ton=ces|12 +enviado|en=via=do|12 +escura|es=cu=ra|12 +español|es=pa=ñol|12 +españoles|es=pa=ño=les|12 +estrañas|es=tra=ñas|12 +estrecha|es=tre=cha|12 +faldas|fal=das|12 +faltas|fal=tas|12 +flaqueza|fla=que=za|12 +gobiernos|go=bier=nos|12 +Goleta|Go=le=ta|12 +graciosa|gra=cio=sa|12 +guardado|guar=da=do|12 +haberme|ha=ber=me|12 +habiéndose|ha=bién=do=se|12 +hiciere|hi=cie=re|12 +hidalgos|hi=dal=gos|12 +hombro|hom=bro|12 +honrados|hon=ra=dos|12 +iglesia|igle=sia|12 +ignorancia|ig=no=ran=cia|12 +imaginaciones|ima=gi=na=cio=nes|12 +imaginarse|ima=gi=nar=se|12 +indicio|in=di=cio|12 +instrumento|ins=tru=men=to|12 +jueces|jue=ces|12 +largas|lar=gas|12 +lastimada|las=ti=ma=da|12 +linajes|li=na=jes|12 +llanto|llan=to|12 +luengos|luen=gos|12 +materia|ma=te=ria|12 +mezcla|mez=cla|12 +mostrarse|mos=trar=se|12 +muriendo|mu=rien=do|12 +obligados|obli=ga=dos|12 +paredes|pa=re=des|12 +pasara|pa=sa=ra|12 +pedían|pe=dían|12 +perdió|per=dió|12 +perros|pe=rros|12 +pintada|pin=ta=da|12 +posada|po=sa=da|12 +preguntas|pre=gun=tas|12 +procurar|pro=cu=rar|12 +quedos|que=dos|12 +quédese|qué=de=se|12 +recibieron|re=ci=bie=ron|12 +referidas|re=fe=ri=das|12 +religión|re=li=gión|12 +repuesto|re=pues=to|12 +respondieron|res=pon=die=ron|12 +saliendo|salien=do|12 +salían|salían|12 +selvas|se=l=vas|12 +serviros|ser=vi=ros|12 +sierras|sie=rras|12 +sirvió|sir=vió|12 +socarrón|so=ca=rrón|12 +socorro|so=co=rro|12 +soneto|so=ne=to|12 +suceden|su=ce=den|12 +sucediese|su=ce=die=se|12 +sujetos|su=je=tos|12 +supiera|su=pie=ra|12 +tablas|ta=blas|12 +testimonio|tes=ti=mo=nio|12 +tratan|tra=tan|12 +valerosos|va=le=ro=sos|12 +vecinos|ve=ci=nos|12 +venturoso|ven=tu=ro=so|12 +Volvió|Vol=vió|12 +abiertos|abier=tos|11 +acababa|aca=ba=ba|11 +acogimiento|aco=gi=mien=to|11 +acompañar|acom=pa=ñar|11 +acontecido|acon=te=ci=do|11 +ademán|ade=mán|11 +admirar|ad=mi=rar|11 +admiró|ad=mi=ró|11 +advertido|ad=ver=ti=do|11 +agraviado|agra=via=do|11 +alcázar|al=cá=zar|11 +Alejandro|Ale=jan=dro|11 +alivio|ali=vio|11 +animales|ani=ma=les|11 +artillería|ar=ti=lle=ría|11 +arábigo|ará=bi=go|11 +aventurero|aven=tu=re=ro|11 +batanes|ba=ta=nes|11 +bestia|bes=tia|11 +cabecera|ca=be=ce=ra|11 +cabezas|ca=be=zas|11 +camaradas|ca=ma=ra=das|11 +camisas|ca=mi=sas|11 +cansancio|can=s=an=cio|11 +capitanes|ca=pi=ta=nes|11 +caridad|ca=ri=dad|11 +cascos|cas=cos|11 +caterva|ca=ter=va|11 +chusma|chus=ma|11 +ciertas|cier=tas|11 +claramente|cla=ra=men=te|11 +clavija|cla=vi=ja|11 +colgado|col=ga=do|11 +cometido|co=me=ti=do|11 +comisario|co=mi=sa=rio|11 +comparación|com=pa=ra=ción|11 +componer|com=po=ner|11 +compuesta|com=pues=ta|11 +conoce|co=no=ce|11 +conocieron|co=no=cie=ron|11 +consiste|con=sis=te|11 +contraria|con=tra=ria|11 +correo|co=rreo|11 +corriente|co=rrien=te|11 +cortesanos|cor=te=sanos|11 +criatura|cria=tu=ra|11 +crueldad|cruel=dad|11 +cuestas|cues=tas|11 +dejara|de=ja=ra|11 +dejarle|de=jar=le|11 +derribado|de=rri=ba=do|11 +desafío|de=sa=fío|11 +desaguisado|des=agui=sa=do|11 +desdenes|des=de=nes|11 +deshacer|des=ha=cer|11 +despidió|des=pi=dió|11 +detener|de=te=ner|11 +determinaron|de=ter=mi=na=ron|11 +dieren|die=ren|11 +dificultades|di=fi=cul=ta=des|11 +dormido|dor=mi=do|11 +Durandarte|Du=ran=dar=te|11 +Dígame|Dí=ga=me|11 +ejemplos|ejem=plos|11 +ejército|ejérci=to|11 +encuentro|en=cuen=tro|11 +escoger|es=co=ger|11 +escuridad|es=cu=ri=dad|11 +espera|es=pe=ra|11 +esperaban|es=pe=ra=ban|11 +estimación|es=ti=ma=ción|11 +estrecho|es=tre=cho|11 +gallarda|ga=llar=da|11 +guardas|guar=das|11 +Guinart|Gui=nart|11 +habilidades|ha=bi=li=da=des|11 +hablase|ha=bla=se|11 +hacerla|ha=cer=la|11 +hideputa|hi=de=pu=ta|11 +humanas|hu=ma=nas|11 +imaginó|ima=gi=nó|11 +imposibles|im=po=si=bles|11 +inconveniente|in=con=ve=nien=te|11 +ingenios|in=ge=nios|11 +invidia|in=vi=dia|11 +invierno|in=vierno|11 +laberinto|la=be=rin=to|11 +ladrones|la=dro=nes|11 +levantarse|le=van=tar=se|11 +limpieza|lim=pie=za|11 +llamarse|lla=mar=se|11 +llegaban|lle=ga=ban|11 +llegasen|lle=ga=sen|11 +llover|llo=ver|11 +matrimonio|ma=tri=mo=nio|11 +merecen|me=re=cen|11 +merecía|me=re=cía|11 +mesmas|mes=mas|11 +metido|me=ti=do|11 +molino|mo=lino|11 +moneda|mo=ne=da|11 +monesterio|mo=nes=te=rio|11 +morisca|mo=ris=ca|11 +muertos|muer=tos|11 +nacidos|na=ci=dos|11 +necedad|ne=ce=dad|11 +negocios|ne=go=cios|11 +obligación|obli=ga=ción|11 +ocasiones|oca=sio=nes|11 +oficios|ofi=cios|11 +ordenado|or=de=na=do|11 +parado|pa=ra=do|11 +pareciese|pa=re=cie=se|11 +pasatiempo|pa=sa=tiem=po|11 +peligrosa|pe=li=gro=sa|11 +pesada|pe=sa=da|11 +podemos|po=de=mos|11 +preguntóle|pre=gun=tó=le|11 +promete|pro=me=te|11 +prometer|pro=me=ter|11 +propias|pro=pias|11 +pudiendo|pu=dien=do|11 +puente|puen=te|11 +puercos|puer=cos|11 +pusiera|pu=sie=ra|11 +pérdida|pér=di=da|11 +quedamos|que=da=mos|11 +quejarse|que=jar=se|11 +querida|que=ri=da|11 +queriendo|que=rien=do|11 +quieras|quie=ras|11 +quitarme|qui=tar=me|11 +razonamiento|ra=zo=na=mien=to|11 +recibe|re=ci=be|11 +rendido|ren=di=do|11 +reposar|re=po=sar|11 +respuestas|res=pues=tas|11 +rodela|ro=de=la|11 +saldrá|sal=drá|11 +sentaron|sen=ta=ron|11 +servida|ser=vi=da|11 +servían|ser=vían|11 +solicitud|so=li=ci=tud|11 +sustento|sus=ten=to|11 +tendrá|ten=drá|11 +terciopelo|ter=cio=pe=lo|11 +tierna|tier=na|11 +tierras|tie=rras|11 +tocantes|to=can=tes|11 +Toledo|To=le=do|11 +traición|trai=ción|11 +través|tra=vés|11 +trueco|true=co|11 +trujeron|tru=je=ron|11 +vendría|ven=dría|11 +viejos|vie=jos|11 +vistas|vis=tas|11 +Vivaldo|Vi=val=do|11 +volverme|vol=ver=me|11 +órdenes|ór=de=nes|11 +última|úl=ti=ma|11 +access|ac=ce=ss|10 +accidente|ac=ci=den=te|10 +aceite|acei=te|10 +acertar|acer=tar|10 +Acudió|Acu=dió|10 +advierta|ad=vier=ta|10 +aficionado|afi=cio=na=do|10 +ajenos|aje=nos|10 +alabado|ala=ba=do|10 +alcaide|al=cai=de|10 +Alonso|Alon=so|10 +amorosos|amo=ro=sos|10 +Andalucía|An=da=lu=cía|10 +anoche|ano=che|10 +Antonomasia|An=to=no=ma=sia|10 +apartar|apar=tar|10 +Aragón|Ara=gón|10 +arrobas|arro=bas|10 +asperezas|as=pe=re=zas|10 +bastantes|bas=tan=tes|10 +Belerma|Be=ler=ma|10 +beneficio|be=ne=fi=cio|10 +caminando|ca=mi=nan=do|10 +caminantes|ca=mi=nan=tes|10 +Candaya|Can=da=ya|10 +carretero|ca=rre=te=ro|10 +catorce|ca=tor=ce|10 +causado|cau=sa=do|10 +causas|cau=sas|10 +cautiva|cau=ti=va|10 +cayeron|ca=ye=ron|10 +comiendo|co=mien=do|10 +componen|com=po=nen|10 +comúnmente|co=mún=men=te|10 +consentir|con=sen=tir|10 +considerar|con=si=de=rar|10 +continua|con=ti=nua|10 +Corchuelo|Cor=chue=lo|10 +corredores|co=rre=do=res|10 +corren|co=rren|10 +coyuntura|co=yun=tu=ra|10 +creído|creí=do|10 +criada|cria=da|10 +cuarta|cuar=ta|10 +cuarto|cuar=to|10 +cubrir|cu=brir|10 +cuentas|cuen=tas|10 +cuerno|cuerno|10 +cumplimiento|cum=pli=mien=to|10 +cántaro|cán=ta=ro|10 +debida|de=bi=da|10 +dejarme|de=jar=me|10 +descomunal|des=co=mu=nal|10 +deseosos|de=seo=sos|10 +desgraciado|des=gra=cia=do|10 +despedirse|des=pe=dir=se|10 +despierto|des=pier=to|10 +Después|Des=pués|10 +difunto|di=fun=to|10 +digáis|di=gáis|10 +disculpa|dis=cul=pa|10 +disposición|dis=po=si=ción|10 +donaires|do=nai=res|10 +echaron|echa=ron|10 +ejércitos|ejérci=tos|10 +embajada|em=ba=ja=da|10 +encomendándose|en=co=men=dán=do=se|10 +enviar|en=viar|10 +escritura|es=cri=tu=ra|10 +escusado|es=cu=sa=do|10 +espíritus|es=píri=tus|10 +estribos|es=tri=bos|10 +eterna|eter=na|10 +faltado|fal=ta=do|10 +felicísimo|fe=li=cí=si=mo|10 +ferido|fe=ri=do|10 +galera|ga=le=ra|10 +gallardía|ga=llar=día|10 +ganancia|ga=nan=cia|10 +gentilhombre|gen=tilhom=bre|10 +habiéndole|ha=bién=do=le|10 +hachas|ha=chas|10 +hallase|ha=lla=se|10 +hallazgo|ha=llaz=go|10 +hermosos|her=mo=sos|10 +ilustre|ilus=tre|10 +imágines|imá=gi=nes|10 +infinita|in=fi=ni=ta|10 +lealtad|leal=tad|10 +lenguaje|len=gua=je|10 +Levantóse|Le=van=tó=se|10 +License|Li=cen=se|10 +llevarle|lle=var=le|10 +llevas|lle=vas|10 +manchego|man=che=go|10 +menoscabo|me=nos=ca=bo|10 +mientes|mien=tes|10 +miserables|mi=se=ra=bles|10 +miseria|mi=se=ria|10 +mohíno|mohí=no|10 +morisco|mo=ris=co|10 +movimientos|mo=vi=mien=tos|10 +muestre|mues=tre|10 +nobleza|no=ble=za|10 +nombrado|nom=bra=do|10 +nosotras|no=so=tras|10 +ofrece|ofre=ce|10 +ofrezco|ofrez=co|10 +paragraph|pa=ra=gra=ph|10 +pedazo|pe=da=zo|10 +pelota|pe=lo=ta|10 +pendiente|pen=dien=te|10 +peregrino|pe=re=grino|10 +perfeción|per=fe=ción|10 +pierde|pier=de|10 +Pierres|Pie=rres|10 +pintor|pin=tor|10 +poderosa|po=de=ro=sa|10 +podrás|po=drás|10 +pondría|pon=dría|10 +poniéndose|po=nién=do=se|10 +primeras|pri=me=ras|10 +profeso|pro=fe=so|10 +profunda|pro=fun=da|10 +prosiguiendo|pro=si=guien=do|10 +quietud|quie=tud|10 +quitan|qui=tan|10 +quitaron|qui=ta=ron|10 +realmente|real=men=te|10 +referida|re=fe=ri=da|10 +reinas|rei=nas|10 +reliquias|re=li=quias|10 +requesones|re=que=so=nes|10 +responderé|res=pon=de=ré|10 +Respondió|Res=pon=dió|10 +Rodrigo|Ro=dri=go|10 +sandez|san=dez|10 +Satanás|Sata=nás|10 +satisfacer|sa=tis=fa=cer|10 +sentar|sen=tar|10 +singular|sin=gu=lar|10 +soberbia|so=ber=bia|10 +soledades|so=le=da=des|10 +soltar|sol=tar|10 +sonetos|so=ne=tos|10 +sospechas|sos=pe=chas|10 +temeridad|te=me=ri=dad|10 +tendrás|ten=drás|10 +tengas|ten=gas|10 +tormento|tor=men=to|10 +torres|to=rres|10 +tronco|tron=co|10 +tropel|tro=pel|10 +turbada|tur=ba=da|10 +vencer|ven=cer|10 +viernes|vier=nes|10 +viniendo|vi=nien=do|10 +volveré|vol=ve=ré|10 +éramos|éra=mos|10 +abriendo|abrien=do|9 +acabaron|aca=ba=ron|9 +achaque|acha=que|9 +acompañaban|acom=pa=ña=ban|9 +acompañe|acom=pa=ñe|9 +acostumbrada|acos=tum=bra=da|9 +acostumbrado|acos=tum=bra=do|9 +admiraba|ad=mi=ra=ba|9 +admiraron|ad=mi=ra=ron|9 +afición|afi=ción|9 +agradecimiento|agra=de=ci=mien=to|9 +alcanzado|al=can=za=do|9 +alcanzan|al=can=zan|9 +alrededor|al=re=de=dor|9 +amigas|ami=gas|9 +amorosas|amo=ro=sas|9 +Angélica|An=gé=li=ca|9 +ardite|ar=di=te|9 +atrevidos|atre=vi=dos|9 +bendito|ben=di=to|9 +bergantín|ber=gan=tín|9 +bocado|bo=ca=do|9 +cadenas|ca=de=nas|9 +callaba|ca=lla=ba|9 +calzas|cal=zas|9 +camina|ca=mi=na|9 +capitana|ca=pi=ta=na|9 +caritativo|ca=ri=ta=ti=vo|9 +Carlomagno|Car=lo=mag=no|9 +casarme|ca=sar=me|9 +Casildea|Ca=sil=dea|9 +castillos|cas=ti=llos|9 +cazadores|ca=za=do=res|9 +cerrado|ce=rra=do|9 +chirimías|chi=ri=mías|9 +cintura|cin=tu=ra|9 +circunstancias|cir=cuns=tan=cias|9 +comedimiento|co=me=di=mien=to|9 +comenzaba|co=men=za=ba|9 +comprar|com=prar|9 +compás|com=pás|9 +confirmación|con=fir=ma=ción|9 +conocemos|co=no=ce=mos|9 +conocen|co=no=cen|9 +contenía|con=te=nía|9 +contino|con=tino|9 +cordel|cor=del|9 +cortésmente|cor=tés=men=te|9 +Cuenta|Cuen=ta|9 +cumbre|cum=bre|9 +Córdoba|Cór=do=ba|9 +deciros|de=ci=ros|9 +dejaremos|de=ja=re=mos|9 +demasiado|de=ma=sia=do|9 +demonios|de=mo=nios|9 +desatino|des=a=tino|9 +descanso|des=can=so|9 +desventurado|des=ven=tu=ra=do|9 +Diablo|Dia=blo|9 +dichas|di=chas|9 +dificultoso|di=fi=cul=to=so|9 +docenas|do=ce=nas|9 +doliente|do=lien=te|9 +duelen|due=len|9 +déjame|dé=ja=me|9 +déjeme|dé=je=me|9 +déstos|dés=tos|9 +enamoró|ena=mo=ró|9 +encontrar|en=con=trar|9 +entendía|en=ten=día|9 +entraba|en=tra=ba|9 +entretenía|en=tre=te=nía|9 +ermita|er=mi=ta|9 +escrita|es=cri=ta|9 +esfuerzo|es=fuer=zo|9 +espacioso|es=pa=cio=so|9 +espuma|es=pu=ma|9 +estampa|es=tam=pa|9 +estremos|es=tre=mos|9 +firmeza|fir=me=za|9 +franceses|fran=ce=ses|9 +fresca|fres=ca|9 +frontero|fron=te=ro|9 +fácilmente|fá=cil=men=te|9 +gallinas|ga=lli=nas|9 +Grecia|Gre=cia|9 +guardaba|guar=da=ba|9 +guardando|guar=dan=do|9 +gutenberg|gu=ten=berg|9 +géneros|gé=ne=ros|9 +haberla|ha=ber=la|9 +hacerlo|ha=cer=lo|9 +hallara|ha=lla=ra|9 +hermana|her=ma=na|9 +hermosísima|her=mo=sí=si=ma|9 +hubieron|hu=bie=ron|9 +hubiesen|hu=bie=sen|9 +humildes|hu=mil=des|9 +iguala|igua=la|9 +imperio|im=pe=rio|9 +imposibilidad|im=po=si=bi=li=dad|9 +impresión|im=pre=sión|9 +impreso|im=pre=so|9 +incomparable|in=com=pa=ra=ble|9 +Indias|In=dias|9 +interés|in=te=rés|9 +Interés|In=te=rés|9 +intrépido|in=trépi=do|9 +inútil|inú=til|9 +juramentos|ju=ra=men=tos|9 +Lanzarote|Lan=za=ro=te|9 +lastimado|las=ti=ma=do|9 +lector|lec=tor|9 +letrados|le=tra=dos|9 +levantando|le=van=tan=do|9 +limpias|lim=pias|9 +lleven|lle=ven|9 +malezas|ma=le=zas|9 +maltrecho|mal=tre=cho|9 +MANCHA|MAN=CHA|9 +maneras|ma=ne=ras|9 +Mantua|Man=tua|9 +medios|me=dios|9 +medroso|me=dro=so|9 +melancolía|me=lan=co=lía|9 +merecido|me=re=ci=do|9 +moverse|mo=ver=se|9 +mudado|mu=da=do|9 +nacida|na=ci=da|9 +necesarias|ne=ce=sa=rias|9 +notable|no=ta=ble|9 +notado|no=ta=do|9 +obligaciones|obli=ga=cio=nes|9 +ojeriza|oje=ri=za|9 +olvide|ol=vi=de|9 +ordinaria|or=di=na=ria|9 +padece|pa=de=ce|9 +palafrén|pa=la=frén|9 +parecerme|pa=re=cer=me|9 +partido|par=ti=do|9 +partir|par=tir|9 +partirse|par=tir=se|9 +pastoral|pas=to=ral|9 +pedirle|pe=dir=le|9 +peores|peo=res|9 +peregrinos|pe=re=gri=nos|9 +pintar|pin=tar|9 +plazas|pla=zas|9 +podrían|po=drían|9 +pongan|pon=gan|9 +poniéndole|po=nién=do=le|9 +ponían|po=nían|9 +porfía|por=fía|9 +preguntase|pre=gun=ta=se|9 +prenda|pren=da|9 +quedaban|que=da=ban|9 +quedándose|que=dán=do=se|9 +Quisiera|Qui=sie=ra|9 +Reinaldos|Rei=nal=dos|9 +reprehensión|re=prehen=sión|9 +resucitar|re=su=ci=tar|9 +riberas|ri=be=ras|9 +riguroso|ri=gu=ro=so|9 +rincón|rin=cón|9 +Roncesvalles|Ron=ces=va=lles|9 +sabemos|sa=be=mos|9 +sabios|sa=bios|9 +sacarle|sa=car=le|9 +saltando|sal=tan=do|9 +Sancha|San=cha|9 +sentimientos|sen=ti=mien=tos|9 +sepáis|se=páis|9 +siguiendo|si=guien=do|9 +siéndolo|sién=do=lo|9 +sollozos|so=llo=zos|9 +subido|su=bi=do|9 +suceda|su=ce=da|9 +suelta|suel=ta|9 +tendió|ten=dió|9 +tocado|to=ca=do|9 +trasluce|tras=lu=ce|9 +trataba|tra=ta=ba|9 +tratado|tra=ta=do|9 +trompetas|trom=pe=tas|9 +turbado|tur=ba=do|9 +tuviesen|tu=vie=sen|9 +venerable|ve=ne=ra=ble|9 +venimos|ve=ni=mos|9 +veremos|ve=re=mos|9 +vestir|ves=tir|9 +villana|vi=lla=na|9 +Virgilio|Vir=gi=lio|9 +vituperios|vi=tu=pe=rios|9 +viudas|viu=das|9 +volverá|vol=ve=rá|9 +volviesen|vol=vie=sen|9 +aceñas|ace=ñas|8 +acompañamiento|acom=pa=ña=mien=to|8 +aconteció|acon=te=ció|8 +Admirado|Ad=mi=ra=do|8 +advierte|ad=vier=te|8 +alboroto|al=bo=ro=to|8 +alcuza|al=cu=za|8 +alfanje|al=fan=je|8 +amarillo|ama=ri=llo|8 +amenaza|ame=na=za|8 +anochecer|ano=che=cer|8 +apartaron|apar=ta=ron|8 +apartándose|apar=tán=do=se|8 +aprieta|aprie=ta|8 +aprovecharse|apro=ve=char=se|8 +arrieros|arrie=ros|8 +arráez|arráez|8 +asiendo|asien=do|8 +atadas|ata=das|8 +atentos|aten=tos|8 +barruntos|ba=rrun=tos|8 +bastaba|bas=ta=ba|8 +blancos|blan=cos|8 +bonitamente|bo=ni=ta=men=te|8 +brevemente|bre=ve=men=te|8 +caldero|cal=de=ro|8 +caliente|ca=lien=te|8 +caminaban|ca=mi=na=ban|8 +cantaba|can=ta=ba|8 +carneros|car=ne=ros|8 +Castilla|Cas=ti=lla|8 +ciencias|cien=cias|8 +cinchas|cin=chas|8 +comoquiera|co=mo=quie=ra|8 +concertadas|con=cer=ta=das|8 +confianza|con=fian=za|8 +conocerle|co=no=cer=le|8 +conoces|co=no=ces|8 +conocí|co=no=cí|8 +conozca|co=noz=ca|8 +consiente|con=sien=te|8 +contaba|con=ta=ba|8 +contentó|con=ten=tó|8 +cordura|cor=du=ra|8 +cortesano|cor=te=sano|8 +cortesías|cor=tesías|8 +cristal|cris=tal|8 +cuarenta|cua=ren=ta|8 +cuatrocientos|cua=tro=cien=tos|8 +cuchillada|cu=chi=lla=da|8 +Cuerpo|Cuer=po|8 +cumpla|cum=pla|8 +Curioso|Cu=rio=so|8 +debieron|de=bie=ron|8 +debéis|de=béis|8 +declaraba|de=cla=ra=ba|8 +dejamos|de=ja=mos|8 +dejaría|de=ja=ría|8 +demasía|de=ma=sía|8 +derribó|de=rri=bó|8 +derrota|de=rro=ta|8 +desafíos|de=sa=fíos|8 +descubriendo|des=cu=brien=do|8 +descubría|des=cu=bría|8 +desdén|des=dén|8 +desigual|de=si=gual|8 +despoblados|des=po=bla=dos|8 +diciplinantes|di=ci=pli=nan=tes|8 +diciéndoles|di=cién=do=les|8 +diferente|di=fe=ren=te|8 +dificultad|di=fi=cul=tad|8 +dijesen|di=je=sen|8 +discreciones|dis=cre=cio=nes|8 +distintamente|dis=tin=ta=men=te|8 +diversos|di=ver=sos|8 +divino|di=vino|8 +duermen|duer=men|8 +dándome|dán=do=me|8 +edificio|edi=fi=cio|8 +embestir|em=bes=tir|8 +embuste|em=bus=te|8 +embustes|em=bus=tes|8 +encaje|en=ca=je|8 +encerramiento|en=ce=rra=mien=to|8 +encomendarse|en=co=men=dar=se|8 +engañar|en=ga=ñar|8 +entierro|en=tie=rro|8 +entretanto|en=tre=tan=to|8 +escogido|es=co=gi=do|8 +escondida|es=con=di=da|8 +escuchado|es=cu=cha=do|8 +espantable|es=pan=ta=ble|8 +espantado|es=pan=ta=do|8 +esperiencia|es=pe=rien=cia|8 +estarse|es=tar=se|8 +estaría|es=ta=ría|8 +estuviere|es=tu=vie=re|8 +estábamos|es=tá=ba=mos|8 +eterno|eterno|8 +excelencia|ex=ce=len=cia|8 +falsas|fal=sas|8 +faltarán|fal=ta=rán|8 +famosas|fa=mo=sas|8 +fantasma|fan=tas=ma|8 +fatigaba|fa=ti=ga=ba|8 +feridas|fe=ri=das|8 +fidelidad|fi=de=li=dad|8 +finísimo|fi=ní=si=mo|8 +firmes|fir=mes|8 +follones|fo=llo=nes|8 +fueren|fue=ren|8 +fuertes|fuer=tes|8 +furioso|fu=rio=so|8 +galeote|ga=leo=te|8 +ganadero|ga=na=de=ro|8 +gemidos|ge=mi=dos|8 +generoso|ge=ne=ro=so|8 +graciosos|gra=cio=sos|8 +greguescos|gre=gues=cos|8 +guardada|guar=da=da|8 +hablador|ha=bla=dor|8 +habrás|ha=brás|8 +habréis|ha=bréis|8 +habíamos|ha=bía=mos|8 +hacerte|ha=cer=te|8 +hallan|ha=llan|8 +Hermano|Her=ma=no|8 +historiadores|his=to=ria=do=res|8 +holgara|hol=ga=ra|8 +honroso|hon=ro=so|8 +huevos|hue=vos|8 +ignorantes|ig=no=ran=tes|8 +impresa|im=pre=sa|8 +including|in=clu=ding|8 +infame|in=fa=me|8 +infinito|in=fi=ni=to|8 +intencionado|in=ten=cio=na=do|8 +intenciones|in=ten=cio=nes|8 +invencible|in=ven=ci=ble|8 +jurado|ju=ra=do|8 +juzgado|juz=ga=do|8 +labradoras|la=bra=do=ras|8 +legítima|le=gí=ti=ma|8 +levantada|le=van=ta=da|8 +leyenda|le=yen=da|8 +libreas|li=breas|8 +liebre|lie=bre|8 +llamaban|lla=ma=ban|8 +llegamos|lle=ga=mos|8 +llegué|lle=gué|8 +lloraba|llo=ra=ba|8 +lícito|lí=ci=to|8 +mandase|man=da=se|8 +manjares|man=ja=res|8 +margen|mar=gen|8 +medicina|me=di=ci=na|8 +memorias|me=mo=rias|8 +mercader|mer=ca=der|8 +Micomicón|Mi=co=mi=cón|8 +Miranda|Mi=ran=da|8 +molineros|mo=li=ne=ros|8 +momentos|mo=men=tos|8 +Montalbán|Mon=tal=bán|8 +Moreno|Mo=reno|8 +mostraron|mos=tra=ron|8 +Muchas|Mu=chas|8 +médicos|mé=di=cos|8 +nacimiento|na=ci=mien=to|8 +naciones|na=cio=nes|8 +narración|na=rra=ción|8 +necesaria|ne=ce=sa=ria|8 +Nápoles|Ná=po=les|8 +obligada|obli=ga=da|8 +ociosidad|ocio=si=dad|8 +ofrecido|ofre=ci=do|8 +ordena|or=de=na|8 +ordenanzas|or=de=nan=zas|8 +orejas|ore=jas|8 +paradero|pa=ra=de=ro|8 +particulares|par=ti=cu=la=res|8 +pasamos|pa=sa=mos|8 +pensaban|pen=sa=ban|8 +pensase|pen=sa=se|8 +perdonar|per=do=nar|8 +perdone|per=do=ne|8 +perezoso|pe=re=zo=so|8 +personaje|per=so=na=je|8 +pesaba|pe=sa=ba|8 +piadoso|pia=do=so|8 +pidiendo|pi=dien=do|8 +piensas|pien=sas|8 +poblado|po=bla=do|8 +pollinos|po=lli=nos|8 +pregunté|pre=gun=té|8 +Preguntóle|Pre=gun=tó=le|8 +proceder|pro=ce=der|8 +procurando|pro=cu=ran=do|8 +prosiguen|pro=si=guen|8 +prosupuesto|pro=su=pues=to|8 +pusiesen|pu=sie=sen|8 +Púsose|Pú=so=se|8 +quebrada|que=bra=da|8 +quebrantado|que=bran=ta=do|8 +quedarse|que=dar=se|8 +quedóse|que=dó=se|8 +quemar|que=mar|8 +Quiero|Quie=ro|8 +quitarle|qui=tar=le|8 +rebuznar|re=buz=nar|8 +recebida|re=ce=bi=da|8 +recebirle|re=ce=bir=le|8 +recién|re=cién|8 +refund|re=fund|8 +regocijo|re=go=ci=jo|8 +reposada|re=po=sa=da|8 +repúblicas|re=pú=bli=cas|8 +riesgo|ries=go|8 +riscos|ris=cos|8 +rogando|ro=gan=do|8 +ruedas|rue=das|8 +Ruidera|Rui=de=ra|8 +rústico|rús=ti=co|8 +saberse|sa=ber=se|8 +sacaba|sa=ca=ba|8 +sacaré|sa=ca=ré|8 +sagacidad|saga=ci=dad|8 +salimos|sali=mos|8 +sastre|sas=tre|8 +servidos|ser=vi=dos|8 +servirle|ser=vir=le|8 +servía|ser=vía|8 +Siendo|Sien=do|8 +siente|sien=te|8 +soberbio|so=ber=bio|8 +socorrer|so=co=rrer|8 +sucedidas|su=ce=di=das|8 +sábanas|sá=ba=nas|8 +tendida|ten=di=da|8 +tenerla|te=ner=la|8 +tenerle|te=ner=le|8 +tercero|ter=ce=ro|8 +tienda|tien=da|8 +tinieblas|ti=nie=blas|8 +Tirteafuera|Tir=tea=fue=ra|8 +tocadores|to=ca=do=res|8 +tomaba|to=ma=ba|8 +tomaron|to=ma=ron|8 +traerle|traer=le|8 +trajes|tra=jes|8 +trataban|tra=ta=ban|8 +Trifaldín|Tri=fal=dín|8 +tripas|tri=pas|8 +tuerto|tuer=to|8 +títulos|tí=tu=los|8 +túmulo|tú=mu=lo|8 +veamos|vea=mos|8 +vencida|ven=ci=da|8 +vencidos|ven=ci=dos|8 +vengan|ven=gan|8 +vengar|ven=gar|8 +vengarse|ven=gar=se|8 +ventanas|ven=ta=nas|8 +visión|vi=sión|8 +viéndola|vién=do=la|8 +váyase|vá=ya=se|8 +without|wi=thout|8 +yeguas|ye=guas|8 +zapato|za=pa=to|8 +áspero|ás=pe=ro|8 +acomodados|aco=mo=da=dos|7 +acomodarse|aco=mo=dar=se|7 +acontecimientos|acon=te=ci=mien=tos|7 +acordaba|acor=da=ba|7 +acuerda|acuer=da|7 +acémila|acé=mi=la|7 +aderezada|ade=re=za=da|7 +adivino|adi=vino|7 +admirables|ad=mi=ra=bles|7 +adornada|ador=na=da|7 +advertid|ad=ver=tid|7 +afirmar|afir=mar|7 +agradecida|agra=de=ci=da|7 +aguarda|aguar=da|7 +agüero|agüe=ro|7 +albedrío|al=be=drío|7 +alborotado|al=bo=ro=ta=do|7 +aldeana|al=dea=na|7 +Aldonza|Al=don=za|7 +alegrar|ale=grar|7 +alerta|aler=ta|7 +alzaron|al=za=ron|7 +amoroso|amo=ro=so|7 +antojadiza|an=to=ja=di=za|7 +aparejos|apa=re=jos|7 +aplauso|aplau=so|7 +aprobación|apro=ba=ción|7 +aprovechar|apro=ve=char|7 +arcabuces|ar=ca=bu=ces|7 +arrimado|arri=ma=do|7 +asalto|asal=to|7 +aspereza|as=pe=re=za|7 +associated|as=so=ciated|7 +asturiana|as=tu=ria=na|7 +asumpto|asump=to|7 +atados|ata=dos|7 +atrevió|atre=vió|7 +aurora|au=ro=ra|7 +aventaja|aven=ta=ja|7 +averiguada|ave=ri=gua=da|7 +ballesta|ba=lles=ta|7 +banderas|ban=de=ras|7 +bastan|bas=tan|7 +bellaquería|be=lla=que=ría|7 +Bendito|Ben=di=to|7 +beneplácito|be=ne=plá=ci=to|7 +Bernardo|Ber=nar=do|7 +billete|bi=lle=te|7 +bizarría|bi=za=rría|7 +blandas|blan=das|7 +borrasca|bo=rras=ca|7 +calzones|cal=zo=nes|7 +campanas|cam=pa=nas|7 +candil|can=dil|7 +cansados|can=sa=dos|7 +caperuzas|ca=pe=ru=zas|7 +Capitán|Ca=pi=tán|7 +carreras|ca=rre=ras|7 +causar|cau=sar|7 +celebro|ce=le=bro|7 +celoso|ce=lo=so|7 +cenaron|ce=na=ron|7 +cerrados|ce=rra=dos|7 +codicioso|co=di=cio=so|7 +cogido|co=gi=do|7 +colérico|co=lé=ri=co|7 +comenzando|co=men=zan=do|7 +comienzan|co=mien=zan|7 +compasivo|com=pa=si=vo|7 +compostura|com=pos=tu=ra|7 +compuestas|com=pues=tas|7 +confesó|con=fe=só|7 +confirmar|con=fir=mar|7 +confirmó|con=fir=mó|7 +confusa|con=fu=sa|7 +considera|con=si=de=ra|7 +consolar|con=so=lar|7 +Constantinopla|Cons=tan=ti=no=pla|7 +contarse|con=tar=se|7 +contase|con=ta=se|7 +contrarios|con=tra=rios|7 +contravenir|con=tra=ve=nir|7 +conveniente|con=ve=nien=te|7 +copies|co=pies|7 +coplas|co=plas|7 +corazones|co=ra=zo=nes|7 +coronas|co=ro=nas|7 +creció|cre=ció|7 +creyese|cre=ye=se|7 +crianza|crian=za|7 +cruces|cru=ces|7 +créame|créa=me|7 +Cuatro|Cua=tro|7 +cubiertos|cu=bier=tos|7 +cubrió|cu=brió|7 +cuitas|cui=tas|7 +curado|cu=ra=do|7 +cuánta|cuán=ta|7 +Cámara|Cá=ma=ra|7 +daremos|da=re=mos|7 +darían|da=rían|7 +decencia|de=cen=cia|7 +Decidme|De=cid=me|7 +declara|de=cla=ra|7 +declarado|de=cla=ra=do|7 +defenderse|de=fen=der=se|7 +dejándole|de=ján=do=le|7 +demostraciones|de=mos=tra=cio=nes|7 +desdichados|des=di=cha=dos|7 +deseaban|de=sea=ban|7 +deseado|de=sea=do|7 +desean|de=sean|7 +desencantada|des=en=can=ta=da|7 +desiertos|de=sier=tos|7 +desmán|des=mán|7 +despechado|des=pe=cha=do|7 +destreza|des=tre=za|7 +detenía|de=te=nía|7 +diamante|dia=man=te|7 +diamantes|dia=man=tes|7 +diciplina|di=ci=pli=na|7 +diente|dien=te|7 +dijésemos|di=jé=se=mos|7 +disparatados|dis=pa=ra=ta=dos|7 +distributing|dis=tri=bu=ting|7 +doloroso|do=lo=ro=so|7 +dueños|due=ños|7 +durmió|dur=mió|7 +dádiva|dádi=va|7 +eBooks|eBooks|7 +eclesiástico|ecle=siás=ti=co|7 +empero|em=pe=ro|7 +encaminaban|en=ca=mi=na=ban|7 +encaminó|en=ca=mi=nó|7 +encarecidamente|en=ca=re=ci=da=men=te|7 +encendidas|en=cen=di=das|7 +encierran|en=cie=rran|7 +encontrado|en=contra=do|7 +engendró|en=gen=dró|7 +entrase|en=tra=se|7 +entregó|en=tre=gó|7 +enviase|en=via=se|7 +envuelto|en=vuel=to|7 +ermitaño|er=mi=ta=ño|7 +escopetas|es=co=pe=tas|7 +Escritura|Es=cri=tu=ra|7 +escuela|es=cue=la|7 +espantosa|es=pan=to=sa|7 +esperan|es=pe=ran|7 +estados|es=ta=dos|7 +estaré|es=ta=ré|7 +estimaba|es=ti=ma=ba|7 +estranjeros|es=tran=je=ros|7 +estrechamente|es=tre=cha=men=te|7 +estudiar|es=tu=diar|7 +estudio|es=tu=dio|7 +estuve|es=tu=ve|7 +estuviesen|es=tu=vie=sen|7 +faltaban|fal=ta=ban|7 +fantasía|fan=ta=sía|7 +favorable|fa=vo=ra=ble|7 +favorezca|fa=vo=rez=ca|7 +fazañas|fa=za=ñas|7 +Felixmarte|Fe=lix=mar=te|7 +fingidas|fin=gi=das|7 +galope|ga=lo=pe|7 +garras|ga=rras|7 +gastar|gas=tar|7 +gentiles|gen=ti=les|7 +Guadiana|Gua=dia=na|7 +guardan|guar=dan|7 +guardarse|guar=dar=se|7 +guiaba|guia=ba|7 +hacerlas|ha=cer=las|7 +haciéndole|ha=cién=do=le|7 +hagáis|ha=gáis|7 +hallarle|ha=llar=le|7 +haremos|ha=re=mos|7 +hazaña|ha=za=ña|7 +hembra|hem=bra|7 +heredera|he=re=de=ra|7 +herreruelo|he=rre=rue=lo|7 +hiciesen|hi=cie=sen|7 +hincar|hin=car|7 +Hircania|Hir=ca=nia|7 +Homero|Ho=me=ro|7 +huérfanos|huér=fa=nos|7 +Hízose|Hí=zo=se|7 +igualar|igua=lar|7 +imaginando|ima=gi=nan=do|7 +imposibilitado|im=po=si=bi=li=ta=do|7 +imprimiere|im=pri=mie=re|7 +inclinado|in=cli=na=do|7 +inconvenientes|in=con=ve=nien=tes|7 +indicios|in=di=cios|7 +innumerables|in=nu=me=ra=bles|7 +insulanos|in=su=la=nos|7 +invenciones|in=ven=cio=nes|7 +jornadas|jor=na=das|7 +juridición|ju=ri=di=ción|7 +justos|jus=tos|7 +lagunas|la=gu=nas|7 +levanta|le=van=ta|7 +levantase|le=van=ta=se|7 +leyere|le=ye=re|7 +libremente|li=bre=men=te|7 +librillo|li=bri=llo|7 +limosna|li=mos=na|7 +llamas|lla=mas|7 +llegará|lle=ga=rá|7 +llevasen|lle=va=sen|7 +lleváis|lle=váis|7 +llorando|llo=ran=do|7 +located|lo=ca=ted|7 +malicias|ma=li=cias|7 +mamonas|ma=mo=nas|7 +manadas|ma=na=das|7 +manjar|man=jar|7 +maravillo|ma=ra=vi=llo|7 +maravilloso|ma=ra=vi=llo=so|7 +Marsilio|Mar=si=lio|7 +mediano|me=diano|7 +medicinas|me=di=ci=nas|7 +medida|me=di=da|7 +memorable|me=mo=ra=ble|7 +menear|me=near|7 +menesterosa|me=nes=te=ro=sa|7 +menguado|men=gua=do|7 +mentirosos|men=ti=ro=sos|7 +menudencias|me=nu=den=cias|7 +merecían|me=re=cían|7 +ministros|mi=nis=tros|7 +mirara|mi=ra=ra|7 +mirándole|mi=rán=do=le|7 +mochachos|mo=cha=chos|7 +montera|mon=te=ra|7 +Morato|Mo=ra=to|7 +movimiento|mo=vi=mien=to|7 +mudanza|mu=dan=za|7 +mínima|mí=ni=ma|7 +naturalmente|na=tu=ral=men=te|7 +ningunas|nin=gu=nas|7 +ocupado|ocu=pa=do|7 +ofender|ofen=der|7 +ofrecen|ofre=cen|7 +ofrecer|ofre=cer|7 +ofrecimiento|ofre=ci=mien=to|7 +oración|ora=ción|7 +Pardiez|Par=diez|7 +parecieron|pa=re=cie=ron|7 +parezcan|pa=rez=can|7 +pasando|pa=san=do|7 +pastoras|pas=to=ras|7 +pensáis|pen=sáis|7 +perdiese|per=die=se|7 +pereza|pe=re=za|7 +perpetuo|per=pe=tuo|7 +pleito|plei=to|7 +plumas|plu=mas|7 +podremos|po=dre=mos|7 +podréis|po=dréis|7 +predicar|pre=di=car|7 +pregunto|pre=gun=to|7 +preguntándole|pre=gun=tán=do=le|7 +presos|pre=sos|7 +pretende|pre=ten=de|7 +prevenciones|pre=ven=cio=nes|7 +procuraré|pro=cu=ra=ré|7 +profesa|pro=fe=sa|7 +profundos|pro=fun=dos|7 +prometen|pro=me=ten|7 +proprio|pro=prio|7 +provide|pro=vi=de|7 +prudentes|pru=den=tes|7 +purgatorio|pur=ga=to=rio|7 +pública|pú=bli=ca|7 +púsose|pú=so=se|7 +quejar|que=jar|7 +quinto|quin=to|7 +quisiesen|qui=sie=sen|7 +quitaba|qui=ta=ba|7 +razonamientos|ra=zo=na=mien=tos|7 +reciben|re=ci=ben|7 +recibo|re=ci=bo|7 +recogiendo|re=co=gien=do|7 +recogimiento|re=co=gi=mien=to|7 +recogió|re=co=gió|7 +reducir|re=du=cir|7 +referidos|re=fe=ri=dos|7 +regalos|re=ga=los|7 +regidor|re=gi=dor|7 +religioso|re=li=gio=so|7 +representan|re=pre=sen=tan|7 +representó|re=pre=sen=tó|7 +requiebros|re=quie=bros|7 +requieren|re=quie=ren|7 +responda|res=pon=da|7 +revuelto|re=vuel=to|7 +rigurosa|ri=gu=ro=sa|7 +romano|ro=ma=no|7 +rompió|rom=pió|7 +rosario|ro=sa=rio|7 +rubios|ru=bios|7 +rústica|rús=ti=ca|7 +sabrosa|sa=bro=sa|7 +sacase|saca=se|7 +secretos|se=cre=tos|7 +Section|Sec=tion|7 +Segovia|Se=go=via|7 +seguramente|se=gu=ra=men=te|7 +seguía|se=guía|7 +sentada|sen=ta=da|7 +sepultado|se=pul=ta=do|7 +setenta|se=ten=ta|7 +señalado|se=ña=la=do|7 +Señores|Se=ño=res|7 +siguiese|si=guie=se|7 +sobremanera|so=bre=ma=ne=ra|7 +sosegada|so=se=ga=da|7 +sostenía|sos=te=nía|7 +sucedieron|su=ce=die=ron|7 +suelto|suel=to|7 +suspender|sus=pen=der|7 +suspensión|sus=pen=sión|7 +sustancia|sus=tan=cia|7 +sustenta|sus=ten=ta|7 +tamaño|ta=ma=ño|7 +tambores|tam=bo=res|7 +temerario|te=me=ra=rio|7 +tendrán|ten=drán|7 +tendrían|ten=drían|7 +ternera|ter=ne=ra|7 +tierno|tierno|7 +tiernos|tier=nos|7 +tiraba|ti=ra=ba|7 +titerero|ti=te=re=ro|7 +tontos|ton=tos|7 +tornaron|tor=na=ron|7 +Torralba|To=rral=ba|7 +tragedia|tra=ge=dia|7 +trujese|tru=je=se|7 +tuvieran|tu=vie=ran|7 +universal|uni=ver=sal|7 +Valencia|Va=len=cia|7 +valles|va=lles|7 +ventajas|ven=ta=jas|7 +venturas|ven=tu=ras|7 +vestiglos|ves=ti=glos|7 +vientre|vien=tre|7 +vieres|vie=res|7 +viéndome|vién=do=me|7 +volando|vo=lan=do|7 +voluntades|vo=lun=ta=des|7 +yangüeses|yan=güe=ses|7 +ánimos|áni=mos|7 +áspera|ás=pe=ra|7 +abismos|abis=mos|6 +abrazada|abra=za=da|6 +acaban|aca=ban|6 +ACADÉMICO|ACA=DÉ=MI=CO|6 +acierto|acier=to|6 +acomodar|aco=mo=dar|6 +acompañado|acom=pa=ña=do|6 +acompañados|acom=pa=ña=dos|6 +acontecimiento|acon=te=ci=mien=to|6 +acosado|aco=sa=do|6 +acrecentar|acre=cen=tar|6 +Acudieron|Acu=die=ron|6 +Adelante|Ade=lan=te|6 +adelantó|ade=lan=tó|6 +admirada|ad=mi=ra=da|6 +adornan|ador=nan|6 +adulación|adu=la=ción|6 +advertida|ad=ver=ti=da|6 +advirtiese|ad=vir=tie=se|6 +Agramante|Agra=man=te|6 +agüeros|agüe=ros|6 +ahorrar|aho=rrar|6 +alborotada|al=bo=ro=ta=da|6 +albricias|al=bri=cias|6 +alcaldes|al=cal=des|6 +alcance|al=can=ce|6 +alcanzaba|al=can=za=ba|6 +alcurnia|al=cur=nia|6 +aldeas|al=deas|6 +algodón|al=go=dón|6 +amanecer|ama=ne=cer|6 +amarga|amar=ga|6 +andamos|an=da=mos|6 +andanza|an=dan=za|6 +andurriales|an=du=rria=les|6 +anejas|ane=jas|6 +ansimismo|an=si=mis=mo|6 +antoja|an=to=ja|6 +antojos|an=to=jos|6 +apaleado|apa=lea=do|6 +apariencia|apa=rien=cia|6 +apartarse|apar=tar=se|6 +Apeáronse|Apeá=ron=se|6 +aprieto|aprie=to|6 +aprovechó|apro=ve=chó|6 +archivo|ar=chi=vo|6 +ardiendo|ar=dien=do|6 +ARGAMASILLA|AR=GA=MA=SI=LLA|6 +arroyos|arro=yos|6 +asidos|asi=dos|6 +atendiendo|aten=dien=do|6 +atengo|aten=go|6 +atenta|aten=ta|6 +atravesado|atra=ve=sa=do|6 +Aunque|Aun=que|6 +aventureros|aven=tu=re=ros|6 +averiguado|ave=ri=gua=do|6 +barato|ba=ra=to|6 +Barrabás|Ba=rra=bás|6 +Belianís|Be=lia=nís|6 +bellacos|be=lla=cos|6 +bigotes|bi=go=tes|6 +blandura|blan=du=ra|6 +borracho|bo=rra=cho|6 +borrica|bo=rri=ca|6 +boticario|bo=ti=ca=rio|6 +bravos|bra=vos|6 +brocado|bro=ca=do|6 +buenamente|bue=na=men=te|6 +buscalle|bus=ca=lle|6 +buscase|bus=ca=se|6 +cabalgadura|ca=bal=ga=du=ra|6 +caletre|ca=le=tre|6 +casada|ca=sa=da|6 +cautiverio|cau=ti=ve=rio|6 +cesaba|ce=sa=ba|6 +charge|char=ge|6 +chozas|cho=zas|6 +cierra|cie=rra|6 +Clavijo|Cla=vi=jo|6 +cogiendo|co=gien=do|6 +coloquios|co=lo=quios|6 +comedimientos|co=me=di=mien=tos|6 +comply|com=ply|6 +compra|com=pra|6 +comunicar|co=mu=ni=car|6 +conceptos|con=cep=tos|6 +concluir|con=cluir|6 +confusos|con=fu=sos|6 +conociera|co=no=cie=ra|6 +conseguir|con=se=guir|6 +consentimiento|con=sen=ti=mien=to|6 +consentiré|con=sen=ti=ré|6 +consoló|con=so=ló|6 +contienen|con=tie=nen|6 +continuas|con=ti=nuas|6 +contrahecha|contra=he=cha|6 +convertida|con=ver=ti=da|6 +corrió|co=rrió|6 +cortar|cor=tar|6 +creyeron|cre=ye=ron|6 +cristianas|cris=tia=nas|6 +cristiandad|cris=tian=dad|6 +cuadrilla|cua=dri=lla|6 +cuartillo|cuar=ti=llo|6 +cuartos|cuar=tos|6 +cubiertas|cu=bier=tas|6 +cubrían|cu=brían|6 +cuchillo|cu=chi=llo|6 +cuente|cuen=te|6 +cuerda|cuer=da|6 +culebras|cu=le=bras|6 +cumple|cum=ple|6 +cumplirá|cum=pli=rá|6 +darnos|dar=nos|6 +debemos|de=be=mos|6 +decidme|de=cid=me|6 +decille|de=ci=lle|6 +declaración|de=cla=ra=ción|6 +declarar|de=cla=rar|6 +dejadme|de=jad=me|6 +demasiada|de=ma=sia=da|6 +derechamente|de=re=cha=men=te|6 +derechas|de=re=chas|6 +derechos|de=re=chos|6 +desaforado|des=afo=ra=do|6 +desagradecido|des=agra=de=ci=do|6 +desalmados|des=al=ma=dos|6 +descargar|des=car=gar|6 +descubierta|des=cu=bier=ta|6 +descubrirse|des=cu=brir=se|6 +descuidado|des=cui=da=do|6 +descuidos|des=cui=dos|6 +deseada|de=sea=da|6 +desenvuelta|des=en=vuel=ta|6 +desesperaba|des=es=pe=ra=ba|6 +deshecho|des=he=cho|6 +deshizo|des=hi=zo|6 +desnuda|des=nu=da|6 +desnudez|des=nu=dez|6 +despertar|des=per=tar|6 +despidieron|des=pi=die=ron|6 +despierta|des=pier=ta|6 +destruir|des=truir|6 +Deteneos|De=te=neos|6 +determiné|de=ter=mi=né|6 +dichos|di=chos|6 +dieran|die=ran|6 +dignidad|dig=ni=dad|6 +diremos|di=re=mos|6 +disparatado|dis=pa=ra=ta=do|6 +distribution|dis=tri=bu=tion|6 +divina|di=vi=na|6 +divinas|di=vi=nas|6 +doctrina|doc=tri=na|6 +dudoso|du=do=so|6 +dársela|dár=se=la|6 +Díjome|Dí=jo=me|6 +echaba|echa=ba|6 +echándole|echán=do=le|6 +edades|eda=des|6 +elegante|ele=gan=te|6 +embrazó|em=bra=zó|6 +embustero|em=bus=te=ro|6 +empacho|em=pa=cho|6 +empleada|em=plea=da|6 +encamisados|en=ca=mi=sa=dos|6 +encantadas|en=can=ta=das|6 +encarecer|en=ca=re=cer|6 +encendida|en=cen=di=da|6 +encerrar|en=ce=rrar|6 +encerró|en=ce=rró|6 +entienden|en=tien=den|6 +entremeto|en=tre=me=to|6 +escopeta|es=co=pe=ta|6 +escucha|es=cu=cha|6 +escuchan|es=cu=chan|6 +espesas|es=pe=sas|6 +espías|es=pías|6 +estancias|es=tan=cias|6 +estandarte|es=tan=dar=te|6 +estender|es=ten=der|6 +estiende|es=tien=de|6 +estorbar|es=tor=bar|6 +estrañeza|es=tra=ñe=za|6 +estremado|es=tre=ma=do|6 +estribo|es=tri=bo|6 +estudiantes|es=tu=dian=tes|6 +estuvimos|es=tu=vi=mos|6 +faltará|fal=ta=rá|6 +faltase|fal=ta=se|6 +fealdad|feal=dad|6 +fechos|fe=chos|6 +fineza|fi=ne=za|6 +fingido|fin=gi=do|6 +fingidos|fin=gi=dos|6 +fingiendo|fin=gien=do|6 +firmar|fir=mar|6 +Flandes|Flan=des|6 +follón|fo=llón|6 +formar|for=mar|6 +fresco|fres=co|6 +frutas|fru=tas|6 +fuimos|fui=mos|6 +fábula|fá=bu=la|6 +galgos|gal=gos|6 +gallegos|ga=lle=gos|6 +García|Gar=cía|6 +gobiernan|go=bier=nan|6 +graduado|gra=dua=do|6 +gremio|gre=mio|6 +griego|grie=go|6 +guarden|guar=den|6 +gustos|gus=tos|6 +gustoso|gus=to=so|6 +habedes|ha=be=des|6 +haberos|ha=be=ros|6 +hablaban|ha=bla=ban|6 +habérsela|ha=bér=se=la|6 +hacernos|ha=cer=nos|6 +hacéis|ha=céis|6 +hallaban|ha=lla=ban|6 +hallamos|ha=lla=mos|6 +hallas|ha=llas|6 +hallóle|ha=lló=le|6 +haréis|ha=réis|6 +harían|ha=rían|6 +hechicero|he=chi=ce=ro|6 +hechura|he=chu=ra|6 +heredero|he=re=de=ro|6 +Hiciéronlo|Hi=cié=ron=lo|6 +hinojos|hi=no=jos|6 +honrosa|hon=ro=sa|6 +Hércules|Hércu=les|6 +igualmente|igual=men=te|6 +imitando|imi=tan=do|6 +impertinentes|im=per=ti=nen=tes|6 +importaba|im=por=ta=ba|6 +impresas|im=pre=sas|6 +inaudito|inau=di=to|6 +inclemencias|in=cle=men=cias|6 +inclinación|in=cli=na=ción|6 +Ingalaterra|In=ga=la=te=rra|6 +Italia|Ita=lia|6 +izquierda|iz=quier=da|6 +Jerónimo|Je=ró=ni=mo|6 +jineta|ji=ne=ta|6 +justamente|jus=ta=men=te|6 +largos|lar=gos|6 +leerla|leer=la|6 +legítimo|le=gí=ti=mo|6 +letrado|le=tra=do|6 +letura|le=tu=ra|6 +levantadas|le=van=ta=das|6 +license|li=cen=se|6 +llamaron|lla=ma=ron|6 +llegara|lle=ga=ra|6 +llevaría|lle=va=ría|6 +luciente|lu=cien=te|6 +lumbre|lum=bre|6 +madera|ma=de=ra|6 +Magalona|Ma=ga=lo=na|6 +magnificencia|mag=ni=fi=cen=cia|6 +malandrín|ma=lan=drín|6 +maldades|mal=da=des|6 +maldito|mal=di=to|6 +malferido|mal=fe=ri=do|6 +manifiesta|ma=ni=fies=ta|6 +manteado|man=tea=do|6 +maravillosa|ma=ra=vi=llo=sa|6 +mentiroso|men=ti=ro=so|6 +miembro|miem=bro|6 +millas|mi=llas|6 +Miróle|Mi=ró=le|6 +misterio|mis=te=rio|6 +mocedad|mo=ce=dad|6 +mochacho|mo=cha=cho|6 +moderno|mo=derno|6 +molimiento|mo=li=mien=to|6 +monarca|mo=nar=ca|6 +morrión|mo=rrión|6 +mostrase|mos=tra=se|6 +movieron|mo=vie=ron|6 +muchedumbre|mu=che=dum=bre|6 +muertas|muer=tas|6 +murmurando|mur=mu=ran=do|6 +muñeca|mu=ñe=ca|6 +músicas|mú=si=cas|6 +necesarios|ne=ce=sa=rios|6 +necesidades|ne=ce=si=da=des|6 +negras|ne=gras|6 +ninfas|nin=fas|6 +Ninguno|Nin=guno|6 +obligó|obli=gó|6 +ochenta|ochen=ta|6 +ociosas|ocio=sas|6 +ociosos|ocio=sos|6 +ocupada|ocu=pa=da|6 +ocupar|ocu=par|6 +ordinarios|or=di=na=rios|6 +orientales|orien=ta=les|6 +Orlando|Or=lan=do|6 +Ovidio|Ovi=dio|6 +pacífico|pa=cí=fi=co|6 +pajarillos|pa=ja=ri=llos|6 +Panzas|Pan=zas|6 +Parece|Pa=re=ce|6 +parecerles|pa=re=cer=les|6 +Parecióle|Pa=re=ció=le|6 +parecióle|pa=re=ció=le|6 +pasatiempos|pa=sa=tiem=pos|6 +pasmado|pas=ma=do|6 +peligroso|pe=li=gro=so|6 +pequeños|pe=que=ños|6 +perdices|per=di=ces|6 +perdida|per=di=da|6 +pergamino|per=ga=mino|6 +permission|per=mis=sion|6 +permita|per=mi=ta|6 +permite|per=mi=te|6 +personajes|per=so=na=jes|6 +pesaroso|pe=sa=ro=so|6 +pescadores|pes=ca=do=res|6 +piensan|pien=san|6 +pienses|pien=ses|6 +pierdo|pier=do|6 +plegarias|ple=ga=rias|6 +pollina|po=lli=na|6 +pondrá|pon=drá|6 +porfiar|por=fiar|6 +Portugal|Por=tu=gal|6 +Preguntáronle|Pre=gun=tá=ron=le|6 +Preguntéle|Pre=gun=té=le|6 +prestado|pres=ta=do|6 +princesas|prin=ce=sas|6 +procurase|pro=cu=ra=se|6 +profesamos|pro=fe=sa=mos|6 +profesan|pro=fe=san|6 +propiedad|pro=pie=dad|6 +propios|pro=pios|6 +prosiga|pro=si=ga|6 +provechoso|pro=ve=cho=so|6 +provincia|pro=vin=cia|6 +puntualmente|pun=tual=men=te|6 +pájaros|pá=ja=ros|6 +queden|que=den|6 +Querría|Que=rría|6 +quinientos|qui=nien=tos|6 +quisieran|qui=sie=ran|6 +quitasen|qui=ta=sen|6 +quitársela|qui=tár=se=la|6 +Radamanto|Ra=da=man=to|6 +rebuznos|re=buz=nos|6 +recebí|re=ce=bí|6 +recebían|re=ce=bían|6 +redondo|re=don=do|6 +redundar|re=dun=dar|6 +reglas|re=glas|6 +remate|re=ma=te|6 +renombre|re=nom=bre|6 +representa|re=pre=sen=ta|6 +representar|re=pre=sen=tar|6 +requiere|re=quie=re|6 +respetos|res=pe=tos|6 +Respondióle|Res=pon=dió=le|6 +retirada|re=ti=ra=da|6 +retiró|re=ti=ró|6 +retrato|re=tra=to|6 +Ricardo|Ri=car=do|6 +rodearon|ro=dea=ron|6 +rodeos|ro=deos|6 +rogaron|ro=ga=ron|6 +sabiduría|sa=bi=du=ría|6 +sacerdote|sacer=do=te|6 +saetas|sae=tas|6 +saldrán|sal=drán|6 +salgan|sal=gan|6 +saliera|salie=ra|6 +salteador|sal=tea=dor|6 +satisfaré|sa=tis=fa=ré|6 +satisfechos|sa=tis=fe=chos|6 +seamos|sea=mos|6 +secreta|se=cre=ta|6 +seguido|se=gui=do|6 +sendas|sen=das|6 +serpiente|ser=pien=te|6 +siesta|sies=ta|6 +simplicidades|sim=pli=ci=da=des|6 +sirvieron|sir=vie=ron|6 +soberbios|so=ber=bios|6 +sobras|so=bras|6 +sombrero|som=bre=ro|6 +sonaba|so=na=ba|6 +sonaban|so=na=ban|6 +subiendo|su=bien=do|6 +sueltos|suel=tos|6 +suertes|suer=tes|6 +sufrimiento|su=fri=mien=to|6 +suplicándole|su=pli=cán=do=le|6 +temerosos|te=me=ro=sos|6 +templo|tem=plo|6 +tenerme|te=ner=me|6 +tengamos|ten=ga=mos|6 +tengáis|ten=gáis|6 +tentar|ten=tar|6 +tercia|ter=cia|6 +tiendas|tien=das|6 +tomara|to=ma=ra|6 +tortas|tor=tas|6 +traerme|traer=me|6 +tragos|tra=gos|6 +transformada|trans=for=ma=da|6 +transformado|trans=for=ma=do|6 +trasladar|tras=la=dar|6 +tratando|tra=tan=do|6 +triunfo|triun=fo|6 +trocara|tro=ca=ra|6 +trofeo|tro=feo|6 +trompeta|trom=pe=ta|6 +Uchalí|Ucha=lí|6 +usaban|usa=ban|6 +usando|usan=do|6 +Vandalia|Van=da=lia|6 +vejigas|ve=ji=gas|6 +venció|ven=ció|6 +vengado|ven=ga=do|6 +venganzas|ven=gan=zas|6 +ventas|ven=tas|6 +venturosa|ven=tu=ro=sa|6 +Verdaderamente|Ver=da=de=ra=men=te|6 +verdugo|ver=du=go|6 +verdugos|ver=du=gos|6 +verlas|ver=las|6 +vestidas|ves=ti=das|6 +vicios|vi=cios|6 +vienes|vie=nes|6 +viniera|vi=nie=ra|6 +vinieren|vi=nie=ren|6 +virtuosos|vir=tuo=sos|6 +visitar|vi=si=tar|6 +Viéndose|Vién=do=se|6 +volvamos|vol=va=mos|6 +Volvióse|Vol=vió=se|6 +vomitar|vo=mi=tar|6 +vueltas|vuel=tas|6 +Válate|Vá=la=te|6 +zagalas|za=ga=las|6 +ángeles|án=ge=les|6 +aborrecido|abo=rre=ci=do|5 +abrasó|abra=só|5 +abrazándole|abra=zán=do=le|5 +abundante|abun=dan=te|5 +acabóse|aca=bó=se|5 +acaecimientos|acae=ci=mien=tos|5 +acertara|acer=ta=ra|5 +acertare|acer=ta=re|5 +acomete|aco=me=te|5 +acomodaron|aco=mo=da=ron|5 +acompañada|acom=pa=ña=da|5 +acontecer|acon=te=cer|5 +acordado|acor=da=do|5 +acordarse|acor=dar=se|5 +acorrer|aco=rrer|5 +acostumbradas|acos=tum=bra=das|5 +acostumbrados|acos=tum=bra=dos|5 +acreditar|acre=di=tar|5 +ademanes|ade=ma=nes|5 +adentro|aden=tro|5 +aderezado|ade=re=za=do|5 +admirarse|ad=mi=rar=se|5 +adondequiera|adon=de=quie=ra|5 +advertimiento|ad=ver=ti=mien=to|5 +Advierte|Ad=vier=te|5 +afligida|afli=gi=da|5 +afligidos|afli=gi=dos|5 +afrentado|afren=ta=do|5 +Afuera|Afue=ra|5 +agradables|agra=da=bles|5 +agradeció|agra=de=ció|5 +aguardando|aguar=dan=do|5 +agudeza|agu=de=za|5 +aguijón|agui=jón|5 +ahechando|ahe=chan=do|5 +ahorcado|ahor=ca=do|5 +ahorcar|ahor=car|5 +alabardas|ala=bar=das|5 +alameda|ala=me=da|5 +albogues|al=bo=gues|5 +alcahuete|al=cahue=te|5 +alcanzara|al=can=za=ra|5 +alcanzaron|al=can=za=ron|5 +alcázares|al=cá=za=res|5 +alegraron|ale=gra=ron|5 +alegró|ale=gró|5 +Alemania|Ale=ma=nia|5 +alfilerazos|al=fi=le=ra=zos|5 +alguaciles|al=gua=ci=les|5 +aljófar|al=jó=far|5 +altamente|al=ta=men=te|5 +alzaba|al=za=ba|5 +Amadises|Ama=di=ses|5 +amargo|amar=go|5 +ambición|am=bi=ción|5 +andará|an=da=rá|5 +anduviesen|an=du=vie=sen|5 +angustia|an=gus=tia|5 +anillo|ani=llo|5 +ansias|an=sias|5 +anyone|an=yo=ne|5 +aparato|apa=ra=to|5 +apellido|ape=lli=do|5 +apetito|ape=ti=to|5 +Apeóse|Apeó=se|5 +aporreado|apo=rrea=do|5 +apostura|apos=tu=ra|5 +aprendido|apren=di=do|5 +apretar|apre=tar|5 +apuesta|apues=ta|5 +Aquella|Aque=lla|5 +aquéllas|aqué=llas|5 +arenga|aren=ga|5 +Aristóteles|Aris=tó=te=les|5 +arrepentido|arre=pen=ti=do|5 +arrepentimiento|arre=pen=ti=mien=to|5 +arrimada|arri=ma=da|5 +arrimo|arri=mo|5 +arrimó|arri=mó|5 +asaltos|asal=tos|5 +ascuras|as=cu=ras|5 +asendereado|asen=de=rea=do|5 +atabales|ata=ba=les|5 +atambores|atam=bo=res|5 +atender|aten=der|5 +atendía|aten=día|5 +atentado|aten=ta=do|5 +atreva|atre=va|5 +atrevida|atre=vi=da|5 +atrevo|atre=vo|5 +atónitos|ató=ni=tos|5 +aumento|au=men=to|5 +avellanas|ave=lla=nas|5 +avemarías|ave=ma=rías|5 +averiguación|ave=ri=gua=ción|5 +avisado|avi=sa=do|5 +avisar|avi=sar|5 +avínole|aví=no=le|5 +ayudase|ayu=da=se|5 +bajaba|ba=ja=ba|5 +balcón|bal=cón|5 +barbado|bar=ba=do|5 +barriga|ba=rri=ga|5 +bastón|bas=tón|5 +bocací|bo=ca=cí|5 +bonísima|bo=ní=si=ma|5 +brebaje|bre=ba=je|5 +Bretaña|Bre=ta=ña|5 +brinco|brin=co|5 +buitre|bui=tre|5 +burlado|bur=la=do|5 +callaré|ca=lla=ré|5 +camarada|ca=ma=ra=da|5 +caminaba|ca=mi=na=ba|5 +caminado|ca=mi=na=do|5 +campea|cam=pea|5 +canséis|can=séis|5 +cantor|can=tor|5 +cariño|ca=ri=ño|5 +Carpio|Car=pio|5 +castigados|cas=ti=ga=dos|5 +catadura|ca=ta=du=ra|5 +católica|ca=tó=li=ca|5 +cayera|ca=ye=ra|5 +cayese|ca=ye=se|5 +caídos|caí=dos|5 +cebolla|ce=bo=lla|5 +celebrar|ce=le=brar|5 +Cirongilio|Ci=ron=gi=lio|5 +cocinero|co=ci=ne=ro|5 +codicia|co=di=cia|5 +cohecho|cohe=cho|5 +colcha|col=cha|5 +comencé|co=men=cé|5 +comieron|co=mie=ron|5 +comisión|co=mi=sión|5 +compañera|com=pa=ñe=ra|5 +competir|com=pe=tir|5 +comunique|co=mu=ni=que|5 +concedido|con=ce=di=do|5 +concertada|con=cer=ta=da|5 +concertó|con=cer=tó|5 +conducido|con=du=ci=do|5 +confío|con=fío|5 +conservar|con=ser=var|5 +considerado|con=si=de=ra=do|5 +consiguiente|con=si=guien=te|5 +contarle|con=tar=le|5 +contaré|con=ta=ré|5 +contentar|con=ten=tar|5 +contienda|con=tien=da|5 +convertido|con=ver=ti=do|5 +coronista|co=ro=nis=ta|5 +coroza|co=ro=za|5 +correspondencia|co=rres=pon=den=cia|5 +corresponder|co=rres=pon=der|5 +cortada|cor=ta=da|5 +cortado|cor=ta=do|5 +cortes|cor=tes|5 +costas|cos=tas|5 +crecer|cre=cer|5 +creciendo|cre=cien=do|5 +creyera|cre=ye=ra|5 +crujía|cru=jía|5 +cuadra|cua=dra|5 +cualesquiera|cua=les=quie=ra|5 +cubría|cu=bría|5 +cuenten|cuen=ten|5 +cuerdos|cuer=dos|5 +cumplan|cum=plan|5 +Cupido|Cu=pi=do|5 +curiosos|cu=rio=sos|5 +danzas|dan=zas|5 +debiera|de=bie=ra|5 +decirlo|de=cir=lo|5 +declarase|de=cla=ra=se|5 +declaró|de=cla=ró|5 +dejalle|de=ja=lle|5 +dejarte|de=jar=te|5 +dejéis|de=jéis|5 +deleitar|de=lei=tar|5 +delgado|del=ga=do|5 +delicado|de=li=ca=do|5 +delitos|de=li=tos|5 +denantes|de=nan=tes|5 +derramando|de=rra=man=do|5 +derramar|de=rra=mar|5 +derribar|de=rri=bar|5 +desaforados|des=afo=ra=dos|5 +desagradecida|des=agra=de=ci=da|5 +desalmado|des=al=ma=do|5 +descalzos|des=cal=zos|5 +descargó|des=car=gó|5 +desconocida|des=co=no=ci=da|5 +descontenta|des=con=ten=ta|5 +descubrirme|des=cu=brir=me|5 +desembarazado|des=em=ba=ra=za=do|5 +desencantar|des=en=can=tar|5 +desesperados|des=es=pe=ra=dos|5 +deshaciendo|des=ha=cien=do|5 +deshechas|des=he=chas|5 +desierta|de=sier=ta|5 +desierto|de=sier=to|5 +desmayado|des=ma=ya=do|5 +desmesurado|des=me=su=ra=do|5 +desnudar|des=nu=dar|5 +despachó|des=pa=chó|5 +desposado|des=po=sa=do|5 +desposorios|des=po=so=rios|5 +destierro|des=tie=rro|5 +desviados|des=via=dos|5 +detenerme|de=te=ner=me|5 +detenerse|de=te=ner=se|5 +detenga|de=ten=ga|5 +determinaba|de=ter=mi=na=ba|5 +detuvieron|de=tu=vie=ron|5 +detuviese|de=tu=vie=se|5 +devoción|de=vo=ción|5 +dichosa|di=cho=sa|5 +dificultosas|di=fi=cul=to=sas|5 +difícil|di=fí=cil|5 +dijeren|di=je=ren|5 +dilatado|di=la=ta=do|5 +disculpas|dis=cul=pas|5 +disgusto|dis=gus=to|5 +disignio|di=sig=nio|5 +disimular|di=si=mu=lar|5 +distribute|dis=tri=bu=te|5 +diversidad|di=ver=si=dad|5 +donate|do=na=te|5 +doradas|do=ra=das|5 +dudosos|du=do=sos|5 +duelos|due=los|5 +Duerme|Duer=me|5 +dulces|dul=ces|5 +dándose|dán=do=se|5 +Dígote|Dí=go=te|5 +embajador|em=ba=ja=dor|5 +embrazando|em=bra=zan=do|5 +emperatrices|em=pe=ra=tri=ces|5 +empresas|em=pre=sas|5 +encaja|en=ca=ja|5 +encaminadas|en=ca=mi=na=das|5 +encaminando|en=ca=mi=nan=do|5 +encaminar|en=ca=mi=nar|5 +encender|en=cen=der|5 +encerraba|en=ce=rra=ba|5 +encerrada|en=ce=rra=da|5 +encerrados|en=ce=rra=dos|5 +encoger|en=co=ger|5 +encontró|en=contró|5 +encrucijadas|en=cru=ci=ja=das|5 +encubre|en=cu=bre|5 +encuentros|en=cuen=tros|5 +enderezar|en=de=re=zar|5 +enfado|en=fa=do|5 +enferma|en=fer=ma|5 +engañaba|en=ga=ña=ba|5 +enhoramala|enho=ra=ma=la|5 +enjaulado|en=jau=la=do|5 +enmienda|en=mien=da|5 +enramada|en=ra=ma=da|5 +enseñado|en=se=ña=do|5 +enseñan|en=se=ñan|5 +enseñar|en=se=ñar|5 +entena|en=te=na|5 +entendiendo|en=ten=dien=do|5 +entendieron|en=ten=die=ron|5 +entendí|en=ten=dí|5 +enterado|en=te=ra=do|5 +enteros|en=te=ros|5 +enterrar|en=te=rrar|5 +Entraron|En=tra=ron|5 +entretenidos|en=tre=te=ni=dos|5 +entretenimientos|en=tre=te=ni=mien=tos|5 +entretiene|en=tre=tie=ne|5 +enviaba|en=via=ba|5 +enviarle|en=viar=le|5 +erratas|erra=tas|5 +escaparse|es=ca=par=se|5 +escogió|es=co=gió|5 +escrebir|es=cre=bir|5 +escribía|es=cri=bía|5 +escrutinio|es=cru=ti=nio|5 +escuchó|es=cu=chó|5 +escuderil|es=cu=de=ril|5 +espaciosa|es=pa=cio=sa|5 +espantoso|es=pan=to=so|5 +esperad|es=pe=rad|5 +estacada|es=ta=ca=da|5 +estatua|es=ta=tua|5 +estendió|es=ten=dió|5 +estimada|es=ti=ma=da|5 +estrado|es=tra=do|5 +estrechas|es=tre=chas|5 +estrema|es=tre=ma|5 +Estremadura|Es=tre=ma=du=ra|5 +estudios|es=tu=dios|5 +estéril|es=té=ril|5 +eternamente|eter=na=men=te|5 +excepto|ex=cep=to|5 +experiencias|ex=pe=rien=cias|5 +extraordinario|ex=tra=or=di=na=rio|5 +fabricada|fa=bri=ca=da|5 +facilitar|fa=ci=li=tar|5 +facultad|fa=cul=tad|5 +faltando|fal=tan=do|5 +faltare|fal=ta=re|5 +fantástico|fan=tás=ti=co|5 +fatigaban|fa=ti=ga=ban|5 +fatigado|fa=ti=ga=do|5 +felicemente|fe=li=ce=men=te|5 +felicidad|fe=li=ci=dad|5 +felicísima|fe=li=cí=si=ma|5 +fingida|fin=gi=da|5 +fingir|fin=gir|5 +flechas|fle=chas|5 +florestas|flo=res=tas|5 +formaban|for=ma=ban|5 +forzada|for=za=da|5 +forzado|for=za=do|5 +forzosamente|for=zo=sa=men=te|5 +fraile|frai=le|5 +franco|fran=co|5 +fueras|fue=ras|5 +fugitivo|fu=gi=ti=vo|5 +fulano|fu=lano|5 +fuésemos|fué=se=mos|5 +fábulas|fá=bu=las|5 +Galaor|Ga=la=or|5 +gallina|ga=lli=na|5 +gansos|gan=sos|5 +Gaspar|Gas=par|5 +generosos|ge=ne=ro=sos|5 +Ginebra|Gi=ne=bra|5 +Ginesillo|Gi=ne=si=llo|5 +gobierna|go=bier=na|5 +Gonzalo|Gon=za=lo|5 +Granada|Gra=na=da|5 +Grande|Gran=de|5 +guirnaldas|guir=nal=das|5 +gustosos|gus=to=sos|5 +hablara|ha=bla=ra|5 +hablas|ha=blas|5 +habremos|ha=bre=mos|5 +haceros|ha=ce=ros|5 +haciéndose|ha=cién=do=se|5 +hallando|ha=llan=do|5 +hallarme|ha=llar=me|5 +hallándose|ha=llán=do=se|5 +heridos|he=ri=dos|5 +hicieran|hi=cie=ran|5 +hierros|hie=rros|5 +holanda|ho=lan=da|5 +honestas|ho=nes=tas|5 +honestos|ho=nes=tos|5 +hábitos|há=bi=tos|5 +igualarse|igua=lar=se|5 +ilustres|ilus=tres|5 +imaginada|ima=gi=na=da|5 +impertinencias|im=per=ti=nen=cias|5 +impiden|im=pi=den|5 +impresos|im=pre=sos|5 +inclinada|in=cli=na=da|5 +inclinó|in=cli=nó|5 +incomodidades|in=co=mo=di=da=des|5 +indigno|in=dig=no|5 +infantería|in=fan=te=ría|5 +inferir|in=fe=rir|5 +infinidad|in=fi=ni=dad|5 +informado|in=for=ma=do|5 +Information|In=for=ma=tion|5 +ingratitud|in=gra=ti=tud|5 +intitulado|in=ti=tu=la=do|5 +intérprete|in=tér=pre=te|5 +invidiosos|in=vi=dio=sos|5 +jaulas|jau=las|5 +juntarse|jun=tar=se|5 +Júpiter|Jú=pi=ter|5 +lamentable|la=men=ta=ble|5 +lascivo|las=ci=vo|5 +leonado|leo=na=do|5 +levantados|le=van=ta=dos|5 +levante|le=van=te|5 +libertador|li=ber=ta=dor|5 +libras|li=bras|5 +librea|li=brea|5 +ligeras|li=ge=ras|5 +litera|li=te=ra|5 +llamarla|lla=mar=la|5 +llamándose|lla=mán=do=se|5 +llaneza|lla=ne=za|5 +llaves|lla=ves|5 +llevamos|lle=va=mos|5 +madura|ma=du=ra|5 +Mahoma|Maho=ma|5 +majadero|ma=ja=de=ro|5 +majestad|ma=jes=tad|5 +maldad|mal=dad|5 +maldecía|mal=de=cía|5 +maldita|mal=di=ta|5 +malparado|mal=pa=ra=do|5 +mangas|man=gas|5 +maravillado|ma=ra=vi=lla=do|5 +marina|ma=ri=na|5 +marras|ma=rras|5 +martirios|mar=ti=rios|5 +mascar|mas=car|5 +matarme|ma=tar=me|5 +mayorazgo|ma=yo=raz=go|5 +medianamente|me=dia=na=men=te|5 +medium|me=dium|5 +melindre|me=lin=dre|5 +melindrosa|me=lin=dro=sa|5 +melindroso|me=lin=dro=so|5 +mención|men=ción|5 +menguada|men=gua=da|5 +mentirosas|men=ti=ro=sas|5 +mirarle|mi=rar=le|5 +miraron|mi=ra=ron|5 +miserias|mi=se=rias|5 +molidos|mo=li=dos|5 +monstruo|mons=truo|5 +moreno|mo=reno|5 +mortales|mor=ta=les|5 +mostrara|mos=tra=ra|5 +mudando|mu=dan=do|5 +mudanzas|mu=dan=zas|5 +muestran|mues=tran|5 +Málaga|Má=la=ga|5 +Narváez|Nar=váez|5 +necios|ne=cios|5 +nombró|nom=bró|5 +notorio|no=to=rio|5 +novedad|no=ve=dad|5 +novelas|no=ve=las|5 +nueces|nue=ces|5 +Nuestra|Nues=tra|5 +obediencia|obe=dien=cia|5 +obliga|obli=ga|5 +ofreciere|ofre=cie=re|5 +ofrecieron|ofre=cie=ron|5 +ofreciese|ofre=cie=se|5 +ofreciéndole|ofre=cién=do=le|5 +ofrecían|ofre=cían|5 +olvidada|ol=vi=da=da|5 +oraciones|ora=cio=nes|5 +ordenaba|or=de=na=ba|5 +oyentes|oyen=tes|5 +oyéndole|oyén=do=le|5 +pacífica|pa=cí=fi=ca|5 +pagador|pa=ga=dor|5 +pagados|pa=ga=dos|5 +pagano|pa=gano|5 +pagaría|pa=ga=ría|5 +pareciere|pa=re=cie=re|5 +pareciéndoles|pa=re=cién=do=les|5 +parentela|pa=ren=te=la|5 +pariente|pa=rien=te|5 +pasear|pa=sear|5 +pañuelo|pa=ñue=lo|5 +pecadores|pe=ca=do=res|5 +pellizcos|pe=lliz=cos|5 +pendientes|pen=dien=tes|5 +pensarlo|pen=sar=lo|5 +pensaron|pen=sa=ron|5 +perderse|per=der=se|5 +perdidos|per=di=dos|5 +perdono|per=dono|5 +perdóneme|per=dó=ne=me|5 +perezosos|pe=re=zo=sos|5 +perseguida|per=se=gui=da|5 +persuadió|per=sua=dió|5 +persuasión|per=sua=sión|5 +pertrechos|per=tre=chos|5 +pesado|pe=sa=do|5 +picando|pi=can=do|5 +pidieron|pi=die=ron|5 +pintaba|pin=ta=ba|5 +pintadas|pin=ta=das|5 +pisando|pi=san=do|5 +platos|pla=tos|5 +poderlo|po=der=lo|5 +podridas|po=dri=das|5 +podrido|po=dri=do|5 +pongáis|pon=gáis|5 +porfiaba|por=fia=ba|5 +porqué|por=qué|5 +posaderas|po=sade=ras|5 +posibles|po=si=bles|5 +precia|pre=cia|5 +preguntan|pre=gun=tan|5 +preguntare|pre=gun=ta=re|5 +preguntarle|pre=gun=tar=le|5 +premiar|pre=miar|5 +prender|pren=der|5 +presentados|pre=sen=ta=dos|5 +presunción|pre=sun=ción|5 +privilegio|pri=vi=le=gio|5 +procure|pro=cu=re|5 +procuro|pro=cu=ro|5 +progreso|pro=gre=so|5 +prometía|pro=me=tía|5 +propria|pro=pria|5 +propuesto|pro=pues=to|5 +protected|pro=tec=ted|5 +proveídas|pro=veí=das|5 +proveído|pro=veí=do|5 +providencia|pro=vi=den=cia|5 +provincias|pro=vin=cias|5 +prólogo|pró=lo=go|5 +pudieres|pu=die=res|5 +pueblos|pue=blos|5 +pugnaba|pug=na=ba|5 +pupilos|pu=pi=los|5 +puñada|pu=ña=da|5 +pífaro|pí=fa=ro|5 +púlpito|púl=pi=to|5 +quedando|que=dan=do|5 +Quedaron|Que=da=ron|5 +quejaba|que=ja=ba|5 +quijadas|qui=ja=das|5 +Quijano|Qui=jano|5 +quimeras|qui=me=ras|5 +Quintañona|Quin=ta=ño=na|5 +quitarles|qui=tar=les|5 +quitarse|qui=tar=se|5 +quitarte|qui=tar=te|5 +quitándole|qui=tán=do=le|5 +quitármela|qui=tár=me=la|5 +quiénes|quié=nes|5 +randas|ran=das|5 +raíces|raíces|5 +recatada|re=ca=ta=da|5 +recebidos|re=ce=bi=dos|5 +recebía|re=ce=bía|5 +received|re=cei=ved|5 +recelo|re=ce=lo|5 +recogida|re=co=gi=da|5 +recompensa|re=com=pen=sa|5 +recordación|re=cor=da=ción|5 +recuesto|re=cues=to|5 +recámara|re=cá=ma=ra|5 +redoma|re=do=ma|5 +redondez|re=don=dez|5 +regidores|re=gi=do=res|5 +religiosos|re=li=gio=sos|5 +remedie|re=me=die|5 +remedios|re=me=dios|5 +rendida|ren=di=da|5 +renovar|re=no=var|5 +renovaron|re=no=va=ron|5 +renovó|re=no=vó|5 +replicar|re=pli=car|5 +reposado|re=po=sa=do|5 +repostería|re=pos=te=ría|5 +reprehendido|re=prehen=di=do|5 +rescatado|res=ca=ta=do|5 +resplandecientes|res=plan=de=cien=tes|5 +respondelle|res=pon=de=lle|5 +resquicios|res=qui=cios|5 +reventar|re=ven=tar|5 +reírse|reír=se|5 +rogaba|ro=ga=ba|5 +rogado|ro=ga=do|5 +romero|ro=me=ro|5 +ropilla|ro=pi=lla|5 +rústicos|rús=ti=cos|5 +saberlo|sa=ber=lo|5 +sabrosas|sa=bro=sas|5 +sacarán|sa=ca=rán|5 +sacristán|sa=cris=tán|5 +saldré|sal=dré|5 +saldría|sal=dría|5 +saliesen|salie=sen|5 +sandio|san=dio|5 +santas|san=tas|5 +Santiago|San=tia=go|5 +santiguada|san=ti=gua=da|5 +seguros|se=gu=ros|5 +seguían|se=guían|5 +semana|se=ma=na|5 +sentarse|sen=tar=se|5 +sentándose|sen=tán=do=se|5 +sentóse|sen=tó=se|5 +señalando|se=ña=lan=do|5 +Señoría|Se=ño=ría|5 +señorío|se=ño=río|5 +Siempre|Siem=pre|5 +sienta|sien=ta|5 +siguen|si=guen|5 +sinrazones|sin=ra=zo=nes|5 +sinrazón|sin=ra=zón|5 +sintieron|sin=tie=ron|5 +sirviendo|sir=vien=do|5 +sobrenombre|so=bre=nom=bre|5 +socorriese|so=co=rrie=se|5 +sosegadamente|so=se=ga=da=men=te|5 +sosegados|so=se=ga=dos|5 +sosegar|so=se=gar|5 +suaves|sua=ves|5 +subiese|su=bie=se|5 +sucediere|su=ce=die=re|5 +sucedióle|su=ce=dió=le|5 +suplicaba|su=pli=ca=ba|5 +suplir|su=plir|5 +suspenden|sus=pen=den|5 +suspirar|sus=pi=rar|5 +sustentado|sus=ten=ta=do|5 +sutiles|su=ti=les|5 +Sábete|Sá=be=te|5 +tafetán|ta=fe=tán|5 +tapices|ta=pi=ces|5 +teatro|tea=tro|5 +tendidas|ten=di=das|5 +teniéndole|te=nién=do=le|5 +tercio|ter=cio|5 +tesoros|te=so=ros|5 +tinajas|ti=na=jas|5 +Tirante|Ti=ran=te|5 +tocando|to=can=do|5 +tocare|to=ca=re|5 +tocino|to=cino|5 +tocinos|to=ci=nos|5 +tomarla|to=mar=la|5 +torpes|tor=pes|5 +toscano|tos=cano|5 +Tracia|Tra=cia|5 +traducir|tra=du=cir|5 +trances|tran=ces|5 +transformaciones|trans=for=ma=cio=nes|5 +transformación|trans=for=ma=ción|5 +trayendo|tra=yen=do|5 +trazas|tra=zas|5 +tristezas|tris=te=zas|5 +trujesen|tru=je=sen|5 +turbar|tur=bar|5 +tuvimos|tu=vi=mos|5 +universo|uni=ver=so|5 +Valdovinos|Val=do=vi=nos|5 +valerosa|va=le=ro=sa|5 +valían|va=lían|5 +varilla|va=ri=lla|5 +vencedora|ven=ce=do=ra|5 +vendrán|ven=drán|5 +vengamos|ven=ga=mos|5 +vengas|ven=gas|5 +vicario|vi=ca=rio|5 +vicioso|vi=cio=so|5 +vientos|vien=tos|5 +virtuoso|vir=tuo=so|5 +visiones|vi=sio=nes|5 +visita|vi=si=ta|5 +vistió|vis=tió|5 +vocablos|vo=ca=blos|5 +volvería|vol=ve=ría|5 +volváis|vol=váis|5 +volvían|vol=vían|5 +vuelvan|vuel=van|5 +vuelvas|vuel=vas|5 +vámonos|vá=mo=nos|5 +zarandajas|za=ran=da=jas|5 +íbamos|íba=mos|5 +últimamente|úl=ti=ma=men=te|5 +abrasar|abra=sar|4 +abrazado|abra=za=do|4 +abrazando|abra=zan=do|4 +abrazaron|abra=za=ron|4 +abreviar|abre=viar|4 +abriese|abrie=se|4 +abrían|abrían|4 +abundantes|abun=dan=tes|4 +acabadas|aca=ba=das|4 +acabarás|aca=ba=rás|4 +acabasen|aca=ba=sen|4 +Acabóse|Aca=bó=se|4 +Acaeció|Acae=ció|4 +accidentes|ac=ci=den=tes|4 +acentos|acen=tos|4 +acompañaron|acom=pa=ña=ron|4 +acompañase|acom=pa=ña=se|4 +aconsejaba|acon=se=ja=ba|4 +aconsejado|acon=se=ja=do|4 +acontece|acon=te=ce|4 +acreciente|acre=cien=te|4 +acuerde|acuer=de|4 +adelantado|ade=lan=ta=do|4 +aderezo|ade=re=zo|4 +adherentes|adhe=ren=tes|4 +adivinanzas|adi=vi=nan=zas|4 +admira|ad=mi=ra|4 +admirable|ad=mi=ra=ble|4 +Admiráronse|Ad=mi=rá=ron=se|4 +adornadas|ador=na=das|4 +adornado|ador=na=do|4 +advertencia|ad=ver=ten=cia|4 +Advertid|Ad=ver=tid|4 +advertimientos|ad=ver=ti=mien=tos|4 +Advierta|Ad=vier=ta|4 +advirtiendo|ad=vir=tien=do|4 +advirtieron|ad=vir=tie=ron|4 +afirma|afir=ma|4 +afligido|afli=gi=do|4 +afrentados|afren=ta=dos|4 +agradecer|agra=de=cer|4 +agradecidos|agra=de=ci=dos|4 +agradecía|agra=de=cía|4 +aguamanil|agua=ma=nil|4 +aguardaba|aguar=da=ba|4 +aguardaban|aguar=da=ban|4 +aguardase|aguar=da=se|4 +agujeros|agu=je=ros|4 +alabar|ala=bar|4 +alcanzase|al=can=za=se|4 +alcanzo|al=can=zo|4 +aldeanas|al=dea=nas|4 +alegra|ale=gra|4 +alegraba|ale=gra=ba|4 +alegran|ale=gran|4 +alevosía|ale=vo=sía|4 +alfiler|al=fi=ler|4 +Algunos|Al=gu=nos|4 +alhajas|alha=jas|4 +Almodóvar|Al=mo=dó=var|4 +alquiler|al=qui=ler|4 +altura|al=tu=ra|4 +alumbra|alum=bra|4 +amador|ama=dor|4 +amaneciese|ama=ne=cie=se|4 +amargamente|amar=ga=men=te|4 +amenazaba|ame=na=za=ba|4 +anciano|an=ciano|4 +ancianos|an=cia=nos|4 +andarse|an=dar=se|4 +andemos|an=de=mos|4 +anochecía|ano=che=cía|4 +anotación|ano=ta=ción|4 +antaño|an=ta=ño|4 +antifaces|anti=fa=ces|4 +antiguas|an=ti=guas|4 +apaciguó|apa=ci=guó|4 +apariencias|apa=rien=cias|4 +aparta|apar=ta|4 +Apartóse|Apar=tó=se|4 +Apostaré|Apos=ta=ré|4 +aprendí|apren=dí|4 +apretaba|apre=ta=ba|4 +apretó|apre=tó|4 +aprovecha|apro=ve=cha|4 +apócrifo|apó=cri=fo|4 +Arabia|Ara=bia|4 +aragonés|ara=go=nés|4 +Arcadia|Ar=ca=dia|4 +archivos|ar=chi=vos|4 +arenas|are=nas|4 +arguye|ar=gu=ye|4 +arminio|ar=mi=nio|4 +arrancaba|arran=ca=ba|4 +arrojando|arro=jan=do|4 +arrojarse|arro=jar=se|4 +asegurar|ase=gu=rar|4 +aseguro|ase=gu=ro|4 +asentar|asen=tar|4 +asistir|asis=tir|4 +atalaya|ata=la=ya|4 +atañen|ata=ñen|4 +atinar|ati=nar|4 +atrevimientos|atre=vi=mien=tos|4 +avengas|aven=gas|4 +avisase|avi=sa=se|4 +ayudan|ayu=dan|4 +ayudarme|ayu=dar=me|4 +ayudasen|ayu=da=sen|4 +azotar|azo=tar|4 +azotarme|azo=tar=me|4 +azotarse|azo=tar=se|4 +azules|azu=les|4 +añadiduras|aña=di=du=ras|4 +bajeles|ba=je=les|4 +bajeza|ba=je=za|4 +balcones|bal=co=nes|4 +bancos|ban=cos|4 +bandoleros|ban=do=le=ros|4 +banquete|ban=que=te|4 +barata|ba=ra=ta|4 +barras|ba=rras|4 +bautismo|bau=tis=mo|4 +bayeta|ba=ye=ta|4 +bebida|be=bi=da|4 +bebido|be=bi=do|4 +bebiendo|be=bien=do|4 +bellaquerías|be=lla=que=rías|4 +Beltenebros|Bel=te=ne=bros|4 +beneficiado|be=ne=fi=cia=do|4 +beneficios|be=ne=fi=cios|4 +Blanco|Blan=co|4 +blancura|blan=cu=ra|4 +blasfemia|blas=fe=mia|4 +blasfemias|blas=fe=mias|4 +bocados|bo=ca=dos|4 +bogaban|bo=ga=ban|4 +bosques|bos=ques|4 +boyero|bo=ye=ro|4 +bronces|bron=ces|4 +burladores|bur=la=do=res|4 +burlados|bur=la=dos|4 +buscan|bus=can|4 +buscándole|bus=cán=do=le|4 +bárbaros|bár=ba=ros|4 +bástame|bás=ta=me|4 +caballeresca|ca=ba=lle=res=ca|4 +caballerescas|ca=ba=lle=res=cas|4 +cabello|ca=be=llo|4 +calabazadas|ca=la=ba=za=das|4 +calamidades|ca=la=mi=da=des|4 +callado|ca=lla=do|4 +callejuelas|ca=lle=jue=las|4 +campana|cam=pa=na|4 +camuza|ca=mu=za|4 +cannot|can=not|4 +cansada|can=sa=da|4 +cansarse|can=sar=se|4 +capilla|ca=pi=lla|4 +captivo|cap=ti=vo|4 +capítulos|ca=pí=tu=los|4 +carecen|ca=re=cen|4 +cargado|car=ga=do|4 +cargaron|car=ga=ron|4 +carrillos|ca=rri=llos|4 +cartapacios|car=ta=pa=cios|4 +casadas|ca=sa=das|4 +cascabeles|cas=ca=be=les|4 +Cascajo|Cas=ca=jo|4 +castigado|cas=ti=ga=do|4 +Católica|Ca=tó=li=ca|4 +católicas|ca=tó=li=cas|4 +causaba|cau=sa=ba|4 +celosa|ce=lo=sa|4 +cencerros|cen=ce=rros|4 +centinelas|cen=ti=ne=las|4 +centro|cen=tro|4 +cerdas|cer=das|4 +ceremonia|ce=re=mo=nia|4 +cerradas|ce=rra=das|4 +cinchado|cin=cha=do|4 +circunvecinas|cir=cun=ve=ci=nas|4 +circustantes|cir=cus=tan=tes|4 +clarines|cla=ri=nes|4 +clérigos|clé=ri=gos|4 +cobardía|co=bar=día|4 +cobrado|co=bra=do|4 +colada|co=la=da|4 +colchones|col=cho=nes|4 +colgada|col=ga=da|4 +coligió|co=li=gió|4 +collar|co=llar|4 +collection|co=llec=tion|4 +comamos|co=ma=mos|4 +combatientes|com=ba=tien=tes|4 +comedidamente|co=me=di=da=men=te|4 +comenzamos|co=men=za=mos|4 +comienza|co=mien=za|4 +comiese|co=mie=se|4 +compaña|com=pa=ña|4 +compañeras|com=pa=ñe=ras|4 +complacer|com=pla=cer|4 +complexión|com=ple=xión|4 +compliance|com=plian=ce|4 +compone|com=po=ne|4 +comían|co=mían|4 +concavidad|con=ca=vi=dad|4 +concede|con=ce=de|4 +conceder|con=ce=der|4 +concluye|con=clu=ye|4 +condena|con=de=na|4 +confesión|con=fe=sión|4 +confiado|con=fia=do|4 +confirmo|con=fir=mo|4 +confusiones|con=fu=sio=nes|4 +congoja|con=go=ja|4 +conjeturas|con=je=tu=ras|4 +consejero|con=se=je=ro|4 +conserva|con=ser=va|4 +conservan|con=ser=van|4 +consigue|con=si=gue|4 +consintiera|con=sin=tie=ra|4 +consistía|con=sis=tía|4 +contarla|con=tar=la|4 +contentado|con=ten=ta=do|4 +contente|con=ten=te|4 +contentísimo|con=ten=tí=si=mo|4 +contenían|con=te=nían|4 +continencia|con=ti=nen=cia|4 +continuos|con=ti=nuos|4 +Contra|Contra|4 +convidó|con=vi=dó|4 +copying|co=p=ying|4 +cordobán|cor=do=bán|4 +coronada|co=ro=na=da|4 +coronado|co=ro=na=do|4 +corran|co=rran|4 +correrse|co=rrer=se|4 +corresponda|co=rres=pon=da|4 +corridos|co=rri=dos|4 +cortesana|cor=te=sa=na|4 +cortezas|cor=te=zas|4 +cosarios|co=sa=rios|4 +cosmógrafo|cos=mó=gra=fo|4 +Costantinopla|Cos=tan=ti=no=pla|4 +costilla|cos=ti=lla|4 +country|coun=try|4 +cristales|cris=ta=les|4 +crueles|crue=les|4 +cubrirse|cu=brir=se|4 +cuellos|cue=llos|4 +cuernos|cuer=nos|4 +cuervos|cuer=vos|4 +cueste|cues=te|4 +cuidados|cui=da=dos|4 +culpas|cul=pas|4 +cumpliese|cum=plie=se|4 +cumpliría|cum=pli=ría|4 +cumplió|cum=plió|4 +curarle|cu=rar=le|4 +curarse|cu=rar=se|4 +cásese|cá=se=se|4 +cómodo|có=mo=do|4 +debíamos|de=bía=mos|4 +decirlas|de=cir=las|4 +decirles|de=cir=les|4 +decírselo|de=cír=se=lo|4 +defenderme|de=fen=der=me|4 +defendía|de=fen=día|4 +dejaran|de=ja=ran|4 +dejémonos|de=jé=mo=nos|4 +delicada|de=li=ca=da|4 +delicadas|de=li=ca=das|4 +delicados|de=li=ca=dos|4 +delincuente|de=lin=cuen=te|4 +delito|de=li=to|4 +demandas|de=man=das|4 +deparase|de=pa=ra=se|4 +desaforadas|des=afo=ra=das|4 +desatinada|des=ati=na=da|4 +desatinos|des=a=ti=nos|4 +desayunado|des=ayu=na=do|4 +descomedidos|des=co=me=di=dos|4 +descortés|des=cor=tés|4 +describe|des=cri=be|4 +descubiertos|des=cu=bier=tos|4 +descubra|des=cu=bra|4 +descubrimos|des=cu=bri=mos|4 +descubro|des=cu=bro|4 +descubrí|des=cu=brí|4 +Desdichado|Des=di=cha=do|4 +deseamos|de=sea=mos|4 +desearse|de=sear=se|4 +desechar|de=se=char|4 +desengaños|des=en=ga=ños|4 +deseoso|de=seo=so|4 +desesperada|des=es=pe=ra=da|4 +deseáis|de=seáis|4 +desfacedor|des=fa=ce=dor|4 +deshonesta|des=ho=nes=ta|4 +deshonrado|des=hon=ra=do|4 +designio|de=sig=nio|4 +desiguales|de=si=gua=les|4 +desinteresada|de=sin=te=re=sa=da|4 +despacho|des=pa=cho|4 +despedir|des=pe=dir|4 +despensa|des=pen=sa|4 +despertaba|des=per=ta=ba|4 +despertado|des=per=ta=do|4 +despertase|des=per=ta=se|4 +despoblado|des=po=bla=do|4 +despojaron|des=po=ja=ron|4 +desposorio|des=po=so=rio|4 +desviado|des=via=do|4 +detenerle|de=te=ner=le|4 +devota|de=vo=ta|4 +diciplinas|di=ci=pli=nas|4 +diestro|dies=tro|4 +dificultosa|di=fi=cul=to=sa|4 +dijeres|di=je=res|4 +dijiste|di=jis=te|4 +dilatar|di=la=tar|4 +discordia|dis=cor=dia|4 +disparaba|dis=pa=ra=ba|4 +disparatadas|dis=pa=ra=ta=das|4 +displaying|dis=pla=ying|4 +dispone|dis=po=ne|4 +distributed|dis=tri=buted|4 +divide|di=vi=de|4 +dividía|di=vi=día|4 +Divina|Di=vi=na|4 +docientas|do=cien=tas|4 +documentos|do=cu=men=tos|4 +dolencia|do=len=cia|4 +doloridas|do=lo=ri=das|4 +doquiera|do=quie=ra|4 +dorada|do=ra=da|4 +dorado|do=ra=do|4 +duermo|duer=mo|4 +dulcísima|dul=cí=si=ma|4 +dureza|du=re=za|4 +Déjeme|Dé=je=me|4 +déstas|dés=tas|4 +dígalo|dí=ga=lo|4 +díganme|dí=gan=me|4 +dígolo|dí=go=lo|4 +Díjele|Dí=je=le|4 +díjome|dí=jo=me|4 +echarle|echar=le|4 +echará|echa=rá|4 +echase|echa=se|4 +editions|edi=tions|4 +Egipto|Egip=to|4 +ejecutar|eje=cu=tar|4 +ejercitan|ejer=ci=tan|4 +ejercitar|ejer=ci=tar|4 +elección|elec=ción|4 +elegantemente|ele=gante=men=te|4 +Elisabat|Eli=sa=bat|4 +embozo|em=bo=zo|4 +eminente|emi=nen=te|4 +Emperador|Em=pe=ra=dor|4 +enamorar|ena=mo=rar|4 +encaminase|en=ca=mi=na=se|4 +encamine|en=ca=mi=ne|4 +encarecimiento|en=ca=re=ci=mien=to|4 +encendido|en=cen=di=do|4 +encinas|en=ci=nas|4 +encomendarme|en=co=men=dar=me|4 +encubierta|en=cu=bier=ta|4 +encubría|en=cu=bría|4 +endereza|en=de=re=za|4 +enderezando|en=de=re=zan=do|4 +endiablada|en=dia=bla=da|4 +endriagos|en=dria=gos|4 +engañados|en=ga=ña=dos|4 +engañan|en=ga=ñan|4 +engañarme|en=ga=ñar=me|4 +enjalmas|en=jal=mas|4 +enjuto|en=ju=to|4 +enojado|eno=ja=do|4 +enriquecer|en=ri=que=cer|4 +ensalada|en=sa=la=da|4 +ensartando|en=sar=tan=do|4 +entendidos|en=ten=di=dos|4 +entendiese|en=ten=die=se|4 +entendían|en=ten=dían|4 +entiendan|en=tien=dan|4 +entonada|en=to=na=da|4 +entrega|en=tre=ga|4 +entretenerse|en=tre=te=ner=se|4 +enviaron|en=via=ron|4 +equivalente|equi=va=len=te|4 +erudición|eru=di=ción|4 +escarlata|es=car=la=ta|4 +esconderse|es=con=der=se|4 +escondidas|es=con=di=das|4 +escondidos|es=con=di=dos|4 +escote|es=co=te|4 +escriba|es=cri=ba|4 +escriban|es=cri=ban|4 +escriben|es=cri=ben|4 +escribiendo|es=cri=bien=do|4 +escribiese|es=cri=bie=se|4 +escribí|es=cri=bí|4 +escuadras|es=cua=dras|4 +Escucha|Es=cu=cha|4 +escuderiles|es=cu=de=ri=les|4 +escuelas|es=cue=las|4 +esotro|eso=tro|4 +espanta|es=pan=ta|4 +esparto|es=par=to|4 +especial|es=pe=cial|4 +esperado|es=pe=ra=do|4 +esperamos|es=pe=ra=mos|4 +espere|es=pe=re|4 +espinazo|es=pi=na=zo|4 +esquife|es=qui=fe|4 +estemos|es=te=mos|4 +estera|es=te=ra|4 +estimadas|es=ti=ma=das|4 +estimado|es=ti=ma=do|4 +estimo|es=ti=mo|4 +estoque|es=to=que|4 +estorbarlo|es=tor=bar=lo|4 +estorbase|es=tor=ba=se|4 +estorbó|es=tor=bó|4 +estotro|es=to=tro|4 +estratagemas|es=tra=ta=ge=mas|4 +Estraño|Es=tra=ño|4 +estremada|es=tre=ma=da|4 +estudiado|es=tu=dia=do|4 +estupenda|es=tu=pen=da|4 +etcétera|etcé=te=ra|4 +excede|ex=ce=de|4 +faldellín|fal=de=llín|4 +falsos|fal=sos|4 +faltaron|fal=ta=ron|4 +favorece|fa=vo=re=ce|4 +favores|fa=vo=res|4 +fementido|fe=men=ti=do|4 +fermosas|fer=mo=sas|4 +ficción|fic=ción|4 +fieros|fie=ros|4 +filósofo|fi=ló=so=fo|4 +filósofos|fi=ló=so=fos|4 +fingió|fin=gió|4 +fingía|fin=gía|4 +finísima|fi=ní=si=ma|4 +floresta|flo=res=ta|4 +following|fo=llo=wing|4 +formado|for=ma=do|4 +format|for=mat|4 +forzar|for=zar|4 +Francisco|Fran=cis=co|4 +francés|fran=cés|4 +freely|free=ly|4 +frutos|fru=tos|4 +Fueron|Fue=ron|4 +fáciles|fá=ci=les|4 +gallardos|ga=llar=dos|4 +ganase|ga=na=se|4 +General|Ge=ne=ral|4 +gobernado|go=ber=na=do|4 +golpear|gol=pear|4 +Gracias|Gra=cias|4 +grados|gra=dos|4 +grandísimos|gran=dí=si=mos|4 +granos|gra=nos|4 +griega|grie=ga|4 +griegos|grie=gos|4 +grillos|gri=llos|4 +guardarme|guar=dar=me|4 +guardase|guar=da=se|4 +guardó|guar=dó|4 +guerrero|gue=rre=ro|4 +guiando|guian=do|4 +guijarro|gui=ja=rro|4 +gustaba|gus=ta=ba|4 +gustar|gus=tar|4 +gustosa|gus=to=sa|4 +gustáis|gus=táis|4 +GUTENBERG|GU=TEN=BERG|4 +Gutiérrez|Gu=tié=rrez|4 +haberles|ha=ber=les|4 +haberlo|ha=ber=lo|4 +habitación|ha=bi=ta=ción|4 +habiéndola|ha=bién=do=la|4 +habiéndome|ha=bién=do=me|4 +hablarla|ha=blar=la|4 +hablarle|ha=blar=le|4 +habrían|ha=brían|4 +hagamos|ha=ga=mos|4 +hallados|ha=lla=dos|4 +hallares|ha=lla=res|4 +hallarla|ha=llar=la|4 +hallaros|ha=lla=ros|4 +hallaréis|ha=lla=réis|4 +hallastes|ha=llas=tes|4 +hermanas|her=ma=nas|4 +hermosuras|her=mo=su=ras|4 +hermosísimas|her=mo=sí=si=mas|4 +hiciste|hi=cis=te|4 +Hidalgo|Hi=dal=go|4 +hileras|hi=le=ras|4 +holder|hol=der|4 +honrar|hon=rar|4 +honras|hon=ras|4 +Horacio|Ho=ra=cio|4 +horrendo|ho=rren=do|4 +hubiéredes|hu=bié=re=des|4 +huelen|hue=len|4 +humanos|hu=ma=nos|4 +huéspeda|huéspe=da|4 +ijadas|ija=das|4 +imaginados|ima=gi=na=dos|4 +impedido|im=pe=di=do|4 +impedimento|im=pe=di=men=to|4 +impida|im=pi=da|4 +importunidades|im=por=tu=ni=da=des|4 +impresor|im=pre=sor|4 +inauditas|inau=di=tas|4 +incomodidad|in=co=mo=di=dad|4 +individual|in=di=vi=dual|4 +infiere|in=fie=re|4 +Ingenioso|In=ge=nio=so|4 +injuria|in=ju=ria|4 +inmortalidad|in=mor=ta=li=dad|4 +insolencia|in=so=len=cia|4 +insolencias|in=so=len=cias|4 +insolentes|in=so=len=tes|4 +intentar|in=ten=tar|4 +interese|in=te=re=se|4 +inumerables|inu=me=ra=bles|4 +inventor|in=ven=tor|4 +jardines|jar=di=nes|4 +juzgada|juz=ga=da|4 +lamentaciones|la=men=ta=cio=nes|4 +lanzada|lan=za=da|4 +largamente|lar=ga=men=te|4 +lascivos|las=ci=vos|4 +latina|la=ti=na|4 +lavatorio|la=va=to=rio|4 +leales|lea=les|4 +levantaban|le=van=ta=ban|4 +levantarle|le=van=tar=le|4 +leyeren|le=ye=ren|4 +libranza|li=bran=za|4 +librar|li=brar|4 +librase|li=bra=se|4 +limpiar|lim=piar|4 +lindezas|lin=de=zas|4 +lisura|li=su=ra|4 +llamase|lla=ma=se|4 +Llegando|Lle=gan=do|4 +llevados|lle=va=dos|4 +llevarme|lle=var=me|4 +llevará|lle=va=rá|4 +llevaré|lle=va=ré|4 +llevándole|lle=ván=do=le|4 +logrado|lo=gra=do|4 +luenga|luen=ga|4 +lunares|lu=na=res|4 +lícita|lí=ci=ta|4 +líquidos|lí=qui=dos|4 +maduro|ma=du=ro|4 +Madásima|Ma=dá=si=ma|4 +magnífico|mag=ní=fi=co|4 +maguer|ma=guer|4 +Maguncia|Ma=gun=cia|4 +majada|ma=ja=da|4 +maldiciendo|mal=di=cien=do|4 +maliciosa|ma=li=cio=sa|4 +malicioso|ma=li=cio=so|4 +maligno|ma=lig=no|4 +Mallorca|Ma=llor=ca|4 +manada|ma=na=da|4 +mandare|man=da=re|4 +mansedumbre|man=s=e=dum=bre|4 +manteamiento|man=tea=mien=to|4 +maravedí|ma=ra=ve=dí|4 +maravillosamente|ma=ra=vi=llo=sa=men=te|4 +maridos|ma=ri=dos|4 +matando|ma=tan=do|4 +medianera|me=dia=ne=ra|4 +medidas|me=di=das|4 +mejilla|me=ji=lla|4 +mejillas|me=ji=llas|4 +mejoría|me=jo=ría|4 +memorables|me=mo=ra=bles|4 +mengua|men=gua|4 +menores|me=no=res|4 +menosprecio|me=nos=pre=cio|4 +mentirosa|men=ti=ro=sa|4 +mentís|men=tís|4 +menuda|me=nu=da|4 +menudas|me=nu=das|4 +mercancía|mer=can=cía|4 +merezcan|me=rez=can|4 +meterse|me=ter=se|4 +mezcladas|mez=cla=das|4 +mezclando|mez=clan=do|4 +mezclar|mez=clar|4 +mienten|mien=ten|4 +millares|mi=lla=res|4 +millón|mi=llón|4 +ministro|mi=nis=tro|4 +modernos|mo=der=nos|4 +mojicones|mo=ji=co=nes|4 +Montiel|Mon=tiel|4 +montón|mon=tón|4 +morada|mo=ra=da|4 +moscas|mos=cas|4 +mostraban|mos=tra=ban|4 +mostrando|mos=tran=do|4 +mostrenco|mos=tren=co|4 +mostráis|mos=tráis|4 +Muchos|Mu=chos|4 +mueren|mue=ren|4 +muertes|muer=tes|4 +muestren|mues=tren|4 +mundos|mun=dos|4 +murallas|mu=ra=llas|4 +Murcia|Mur=cia|4 +murmurar|mur=mu=rar|4 +mármoles|már=mo=les|4 +mísero|mí=se=ro|4 +músicos|mú=si=cos|4 +nacieron|na=cie=ron|4 +naipes|nai=pes|4 +navaja|na=va=ja|4 +necesitados|ne=ce=si=ta=dos|4 +negociante|ne=go=cian=te|4 +negociar|ne=go=ciar|4 +Neptuno|Nep=tuno|4 +nietos|nie=tos|4 +Ninguna|Nin=gu=na|4 +nobles|no=bles|4 +nombraba|nom=bra=ba|4 +norabuena|no=ra=bue=na|4 +obispos|obis=pos|4 +obligar|obli=gar|4 +ocioso|ocio=so|4 +ocupación|ocu=pa=ción|4 +online|on=li=ne|4 +opiniones|opi=nio=nes|4 +ordenare|or=de=na=re|4 +ordenaron|or=de=na=ron|4 +ovillo|ovi=llo|4 +pacíficamente|pa=cí=fi=ca=men=te|4 +pagaba|pa=ga=ba|4 +pagase|pa=ga=se|4 +paletas|pa=le=tas|4 +Palmerín|Pal=me=rín|4 +paraban|pa=ra=ban|4 +Parapilla|Pa=ra=pi=lla|4 +pararon|pa=ra=ron|4 +parasismo|pa=ra=sis=mo|4 +pareciéndome|pa=re=cién=do=me|4 +Paredes|Pa=re=des|4 +parezco|pa=rez=co|4 +partieron|par=tie=ron|4 +paréceme|pa=ré=ce=me|4 +pasajero|pa=sa=je=ro|4 +pasaremos|pa=sa=re=mos|4 +pasearse|pa=sear=se|4 +paseándose|pa=seán=do=se|4 +pasión|pa=sión|4 +patente|pa=ten=te|4 +paveses|pa=ve=ses|4 +pañizuelo|pa=ñi=zue=lo|4 +pedirme|pe=dir=me|4 +pedrada|pe=dra=da|4 +pelean|pe=lean|4 +peleando|pe=lean=do|4 +pellizcaron|pe=lliz=ca=ron|4 +pensamos|pen=sa=mos|4 +pensábamos|pen=sá=ba=mos|4 +Pensáis|Pen=sáis|4 +Pentapolín|Pen=ta=po=lín|4 +pequeñas|pe=que=ñas|4 +peregrina|pe=re=gri=na|4 +permitía|per=mi=tía|4 +perpetua|per=pe=tua|4 +perseverar|per=se=ve=rar|4 +persigue|per=si=gue|4 +person|per=son|4 +persuasiones|per=sua=sio=nes|4 +pescador|pes=ca=dor|4 +petición|pe=ti=ción|4 +phrase|ph=ra=se|4 +Pidiéronle|Pi=dié=ron=le|4 +piedad|pie=dad|4 +pierden|pier=den|4 +pintados|pin=ta=dos|4 +pintando|pin=tan=do|4 +pintura|pin=tu=ra|4 +pirámide|pi=rá=mi=de|4 +plantas|plan=tas|4 +pliegos|plie=gos|4 +podenco|po=den=co|4 +poderosas|po=de=ro=sas|4 +poderse|po=der=se|4 +podáis|po=dáis|4 +Poesía|Poesía|4 +polvareda|pol=va=re=da|4 +pondrás|pon=drás|4 +ponerla|po=ner=la|4 +pongas|pon=gas|4 +poniéndome|po=nién=do=me|4 +portal|por=tal|4 +posted|pos=ted|4 +postura|pos=tu=ra|4 +pradecillo|pra=de=ci=llo|4 +precioso|pre=cio=so|4 +precisas|pre=ci=sas|4 +preguntaban|pre=gun=ta=ban|4 +preguntaron|pre=gun=ta=ron|4 +pregunte|pre=gun=te|4 +pregón|pre=gón|4 +premios|pre=mios|4 +presentar|pre=sen=tar|4 +presta|pres=ta|4 +presté|pres=té|4 +prevención|pre=ven=ción|4 +Primero|Pri=me=ro|4 +probado|pro=ba=do|4 +procurado|pro=cu=ra=do|4 +procuran|pro=cu=ran|4 +proezas|proe=zas|4 +profecía|pro=fe=cía|4 +profundidad|pro=fun=di=dad|4 +PROJECT|PRO=JECT|4 +providing|pro=vi=ding|4 +pruebas|prue=bas|4 +pródigo|pró=di=go|4 +próspero|prós=pe=ro|4 +prósperos|prós=pe=ros|4 +pudieras|pu=die=ras|4 +Puestos|Pues=tos|4 +pugnando|pug=nan=do|4 +puntual|pun=tual|4 +purísimo|pu=rí=si=mo|4 +pusimos|pu=si=mos|4 +Pusiéronle|Pu=sié=ron=le|4 +públicas|pú=bli=cas|4 +quedarme|que=dar=me|4 +quedaría|que=da=ría|4 +quedasen|que=da=sen|4 +quedéis|que=déis|4 +quemado|que=ma=do|4 +queremos|que=re=mos|4 +querrá|que=rrá|4 +quicios|qui=cios|4 +quiebra|quie=bra|4 +Quijana|Qui=ja=na|4 +Quijotes|Qui=jo=tes|4 +quilates|qui=la=tes|4 +quitalle|qui=ta=lle|4 +quitara|qui=ta=ra|4 +quitase|qui=ta=se|4 +Quiñones|Qui=ño=nes|4 +rancor|ran=cor|4 +rebaño|re=ba=ño|4 +rebuznaron|re=buz=na=ron|4 +recebirla|re=ce=bir=la|4 +reciba|re=ci=ba|4 +recitantes|re=ci=tan=tes|4 +recoger|re=co=ger|4 +recogieron|re=co=gie=ron|4 +recogiese|re=co=gie=se|4 +refriega|re=frie=ga|4 +regalados|re=ga=la=dos|4 +regalar|re=ga=lar|4 +regiones|re=gio=nes|4 +regoldar|re=gol=dar|4 +relinchos|re=lin=chos|4 +rematado|re=ma=ta=do|4 +remisión|re=mi=sión|4 +rendir|ren=dir|4 +renegados|re=ne=ga=dos|4 +repente|re=pen=te|4 +repliques|re=pli=ques|4 +representaba|re=pre=sen=ta=ba|4 +representaban|re=pre=sen=ta=ban|4 +representadas|re=pre=sen=ta=das|4 +representantes|re=pre=sen=tan=tes|4 +requirements|re=qui=re=men=ts|4 +residen|re=si=den|4 +resistir|re=sis=tir|4 +resplandeciente|res=plan=de=cien=te|4 +resplandor|res=plan=dor|4 +respondiendo|res=pon=dien=do|4 +respondían|res=pon=dían|4 +ribera|ri=be=ra|4 +ricamente|ri=ca=men=te|4 +riendo|rien=do|4 +rindieron|rin=die=ron|4 +rindiese|rin=die=se|4 +riquísimas|ri=quí=si=mas|4 +riquísimo|ri=quí=si=mo|4 +robada|ro=ba=da|4 +robador|ro=ba=dor|4 +rocino|ro=cino|4 +rodeados|ro=dea=dos|4 +rodear|ro=dear|4 +rollizo|ro=lli=zo|4 +Romana|Ro=ma=na|4 +romances|ro=man=ces|4 +rompiendo|rom=pien=do|4 +ruines|rui=nes|4 +réplica|répli=ca|4 +rétulo|ré=tu=lo|4 +rústicas|rús=ti=cas|4 +sabida|sa=bi=da|4 +sabidor|sa=bi=dor|4 +sabidores|sa=bi=do=res|4 +sabroso|sa=bro=so|4 +sabría|sa=bría|4 +sacadas|saca=das|4 +sacara|sa=ca=ra|4 +sacasen|saca=sen|4 +Sacripante|Sa=cri=pan=te|4 +sacudiendo|sa=cu=dien=do|4 +salarios|sa=la=rios|4 +salgamos|sal=ga=mos|4 +salidas|sali=das|4 +saliere|salie=re|4 +salteadores|sal=tea=do=res|4 +saltos|sal=tos|4 +saludó|salu=dó|4 +salvajes|sal=va=jes|4 +satisfaga|sa=tis=fa=ga|4 +satisfecha|sa=tis=fe=cha|4 +seguirle|se=guir=le|4 +semblante|sem=blan=te|4 +sentase|sen=ta=se|4 +Sentóse|Sen=tó=se|4 +sepamos|se=pa=mos|4 +Sepamos|Se=pa=mos|4 +sepulcro|se=pul=cro|4 +sepulturas|se=pul=tu=ras|4 +sereno|se=reno|4 +servirla|ser=vir=la|4 +servirá|ser=vi=rá|4 +seréis|se=réis|4 +señoríos|se=ño=ríos|4 +siestas|sies=tas|4 +siguientes|si=guien=tes|4 +siguiéndole|si=guién=do=le|4 +sillas|si=llas|4 +sillón|si=llón|4 +sintiendo|sin=tien=do|4 +sobresaltos|so=bre=sal=tos|4 +Sobrino|So=brino|4 +socorrerle|so=co=rrer=le|4 +soguilla|so=gui=lla|4 +sonaron|so=na=ron|4 +soplar|so=plar|4 +sospiros|sos=pi=ros|4 +soñadas|so=ña=das|4 +states|sta=tes|4 +status|sta=tus|4 +suavidad|sua=vi=dad|4 +subjeto|sub=je=to|4 +sucede|su=ce=de|4 +sucedidos|su=ce=di=dos|4 +sueños|sue=ños|4 +suficiente|su=fi=cien=te|4 +sufrido|su=fri=do|4 +supieron|su=pie=ron|4 +suplicó|su=pli=có|4 +support|su=pport|4 +sábana|sá=ba=na|4 +tachas|ta=chas|4 +tahalí|taha=lí|4 +tamaña|ta=ma=ña|4 +tardaba|tar=da=ba|4 +tardado|tar=da=do|4 +techado|te=cha=do|4 +temblando|tem=blan=do|4 +temiera|te=mie=ra|4 +temores|te=mo=res|4 +temple|tem=ple|4 +tendremos|ten=dre=mos|4 +teníamos|te=nía=mos|4 +Tienes|Tie=nes|4 +tiernamente|tier=na=men=te|4 +tiernas|tier=nas|4 +tintero|tin=te=ro|4 +tirones|ti=ro=nes|4 +toallas|toa=llas|4 +tocarle|to=car=le|4 +tocase|to=ca=se|4 +toledano|to=le=dano|4 +tomaré|to=ma=ré|4 +tomasen|to=ma=sen|4 +tomándola|to=mán=do=la|4 +toméis|to=méis|4 +Tomóle|To=mó=le|4 +topaba|to=pa=ba|4 +topado|to=pa=do|4 +topase|to=pa=se|4 +torcer|tor=cer|4 +torcido|tor=ci=do|4 +Tordesillas|Tor=de=si=llas|4 +tradición|tra=di=ción|4 +traeré|trae=ré|4 +traigáis|trai=gáis|4 +transformaron|trans=for=ma=ron|4 +transparente|trans=pa=ren=te|4 +traspasado|tras=pa=sa=do|4 +trasudando|tra=su=dan=do|4 +tratamos|tra=ta=mos|4 +tratasen|tra=ta=sen|4 +tratáredes|tra=tá=re=des|4 +traéis|traéis|4 +tropezando|tro=pe=zan=do|4 +turbación|tur=ba=ción|4 +turbes|tur=bes|4 +turbio|tur=bio|4 +Turpín|Tur=pín|4 +turquesca|tur=ques=ca|4 +tálamo|tá=la=mo|4 +Ténganse|Tén=gan=se|4 +umbrales|um=bra=les|4 +Urganda|Ur=gan=da|4 +valenciano|va=len=ciano|4 +valentísimo|va=len=tí=si=mo|4 +valerosas|va=le=ro=sas|4 +vaquilla|va=qui=lla|4 +varias|va=rias|4 +varones|va=ro=nes|4 +vehemencia|vehe=men=cia|4 +velando|ve=lan=do|4 +vengada|ven=ga=da|4 +vengarme|ven=gar=me|4 +venidero|ve=ni=de=ro|4 +verjas|ver=jas|4 +vestiglo|ves=ti=glo|4 +vestirse|ves=tir=se|4 +viajes|via=jes|4 +Viedma|Vied=ma|4 +viejas|vie=jas|4 +Vireno|Vi=reno|4 +vislumbres|vis=lum=bres|4 +vistos|vis=tos|4 +vituperio|vi=tu=pe=rio|4 +volunteers|vo=lun=teers|4 +volvernos|vol=ver=nos|4 +volviera|vol=vie=ra|4 +volvióse|vol=vió=se|4 +volvámonos|vol=vá=mo=nos|4 +vosotras|vo=so=tras|4 +Vuelve|Vuel=ve|4 +vuelves|vuel=ves|4 +Vuestras|Vues=tras|4 +Vuestro|Vues=tro|4 +website|web=si=te|4 +within|wi=thin|4 +zagales|za=ga=les|4 +átomos|áto=mos|4 +ímpetu|ím=pe=tu|4 +últimas|úl=ti=mas|4 +abejas|abe=jas|3 +Abindarráez|Abin=da=rráez|3 +aborrece|abo=rre=ce|3 +aborrecimiento|abo=rre=ci=mien=to|3 +aborrezco|abo=rrez=co|3 +abrasa|abra=sa|3 +Abrazóle|Abra=zó=le|3 +abriera|abrie=ra|3 +Abrióle|Abrió=le|3 +absorto|ab=sor=to|3 +abundoso|abun=do=so|3 +acababan|aca=ba=ban|3 +acabamiento|aca=ba=mien=to|3 +acabara|aca=ba=ra|3 +Acabaron|Aca=ba=ron|3 +acabarse|aca=bar=se|3 +acabará|aca=ba=rá|3 +acabaré|aca=ba=ré|3 +acaben|aca=ben|3 +acatamiento|aca=ta=mien=to|3 +acercando|acer=can=do|3 +acertaba|acer=ta=ba|3 +acertada|acer=ta=da|3 +aciago|acia=go|3 +acidente|aci=den=te|3 +acierta|acier=ta|3 +acogida|aco=gi=da|3 +acogió|aco=gió|3 +acometa|aco=me=ta|3 +acometen|aco=me=ten|3 +acometido|aco=me=ti=do|3 +acometimientos|aco=me=ti=mien=tos|3 +acometía|aco=me=tía|3 +acomodaba|aco=mo=da=ba|3 +acomodándose|aco=mo=dán=do=se|3 +acompaña|acom=pa=ña|3 +acompañaba|acom=pa=ña=ba|3 +acompañamos|acom=pa=ña=mos|3 +acompañan|acom=pa=ñan|3 +acompañando|acom=pa=ñan=do|3 +aconsejar|acon=se=jar|3 +aconsejarte|acon=se=jar=te|3 +acordamos|acor=da=mos|3 +acordar|acor=dar|3 +acordarme|acor=dar=me|3 +acordaron|acor=da=ron|3 +acordándose|acor=dán=do=se|3 +acortar|acor=tar|3 +acostó|acos=tó|3 +acotaciones|aco=ta=cio=nes|3 +acrecentó|acre=cen=tó|3 +acuerdan|acuer=dan|3 +aderezados|ade=re=za=dos|3 +adevinaba|ade=vi=na=ba|3 +admiraban|ad=mi=ra=ban|3 +Admirada|Ad=mi=ra=da|3 +admiran|ad=mi=ran|3 +admiren|ad=mi=ren|3 +admirábanse|ad=mi=rá=ban=se|3 +admirábase|ad=mi=rá=ba=se|3 +admitir|ad=mi=tir|3 +adornar|ador=nar|3 +adornos|ador=nos|3 +adversos|ad=ver=sos|3 +advertidos|ad=ver=ti=dos|3 +afable|afa=ble|3 +afectación|afec=ta=ción|3 +afincamiento|afin=ca=mien=to|3 +afirmaba|afir=ma=ba|3 +afirmo|afir=mo|3 +afirmó|afir=mó|3 +afortunada|afor=tu=na=da|3 +afrentar|afren=tar|3 +afrentas|afren=tas|3 +Africa|Afri=ca|3 +afuera|afue=ra|3 +agobiado|ago=bia=do|3 +agorero|ago=re=ro|3 +agosto|agos=to|3 +agradare|agra=da=re|3 +agradecieron|agra=de=cie=ron|3 +aguardó|aguar=dó|3 +agudezas|agu=de=zas|3 +agudos|agu=dos|3 +Aguilar|Agui=lar|3 +aguileña|agui=le=ña|3 +agüela|agüe=la|3 +Agüero|Agüe=ro|3 +ahorquen|ahor=quen|3 +alabados|ala=ba=dos|3 +alabastro|ala=bas=tro|3 +alargaba|alar=ga=ba|3 +albaceas|al=ba=ceas|3 +alborotó|al=bo=ro=tó|3 +Alborotóse|Al=bo=ro=tó=se|3 +alborozado|al=bo=ro=za=do|3 +Albraca|Al=bra=ca|3 +alcabalas|al=ca=ba=las|3 +alcances|al=can=ces|3 +alcancé|al=can=cé|3 +alcanzarle|al=can=zar=le|3 +alcornoques|al=cor=no=ques|3 +alférez|al=fé=rez|3 +alguacil|al=gua=cil|3 +alheña|alhe=ña|3 +alhombra|alhom=bra|3 +aliviado|ali=via=do|3 +aljaba|al=ja=ba|3 +almalafa|al=ma=la=fa|3 +almenas|al=me=nas|3 +almohadas|al=moha=das|3 +alojado|alo=ja=do|3 +alojar|alo=jar|3 +altanería|al=ta=ne=ría|3 +alzado|al=za=do|3 +alzarse|al=zar=se|3 +alzándose|al=zán=do=se|3 +alárabes|alá=ra=bes|3 +amable|ama=ble|3 +amanece|ama=ne=ce|3 +amanecerá|ama=ne=ce=rá|3 +amaneció|ama=ne=ció|3 +Amaneció|Ama=ne=ció|3 +amarillas|ama=ri=llas|3 +amarillez|ama=ri=llez|3 +amenazando|ame=na=zan=do|3 +amojamado|amo=ja=ma=do|3 +amorosamente|amo=ro=sa=men=te|3 +amparar|am=pa=rar|3 +ampare|am=pa=re|3 +Andaba|An=da=ba|3 +andantesca|an=dan=tes=ca|3 +andarme|an=dar=me|3 +Andrada|An=dra=da|3 +anduve|an=du=ve|3 +anduviera|an=du=vie=ra|3 +anduviese|an=du=vie=se|3 +andáis|an=dáis|3 +animoso|ani=mo=so|3 +aniquilar|ani=qui=lar|3 From 83315b61795e216d9e87c77dd2ee67c1b1cb416b Mon Sep 17 00:00:00 2001 From: Daniel Chelling Date: Tue, 27 Jan 2026 06:29:15 -0800 Subject: [PATCH 28/36] perf: optimize large EPUB indexing from O(n^2) to O(n) (#458) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Optimizes EPUB metadata indexing for large books (2000+ chapters) from ~30 minutes to ~50 seconds by replacing O(n²) algorithms with O(n log n) hash-indexed lookups. Fixes #134 ## Problem Three phases had O(n²) complexity due to nested loops: | Phase | Operation | Before (2768 chapters) | |-------|-----------|------------------------| | OPF Pass | For each spine ref, scan all manifest items | ~25 min | | TOC Pass | For each TOC entry, scan all spine items | ~5 min | | buildBookBin | For each spine item, scan ZIP central directory | ~8.4 min | Total: **~30+ minutes** for first-time indexing of large EPUBs. ## Solution Replace linear scans with sorted hash indexes + binary search: - **OPF Pass**: Build `{hash(id), len, offset}` index from manifest, binary search for each spine ref - **TOC Pass**: Build `{hash(href), len, spineIndex}` index from spine, binary search for each TOC entry - **buildBookBin**: New `ZipFile::fillUncompressedSizes()` API - single ZIP central directory scan with batch hash matching All indexes use FNV-1a hashing with length as secondary key to minimize collisions. Indexes are freed immediately after each phase. ## Results **Shadow Slave EPUB (2768 chapters):** | Phase | Before | After | Speedup | |-------|--------|-------|---------| | OPF pass | ~25 min | 10.8 sec | ~140x | | TOC pass | ~5 min | 4.7 sec | ~60x | | buildBookBin | 506 sec | 34.6 sec | ~15x | | **Total** | **~30+ min** | **~50 sec** | **~36x** | **Normal EPUB (87 chapters):** 1.7 sec - no regression. ## Memory Peak temporary memory during indexing: - OPF index: ~33KB (2770 items × 12 bytes) - TOC index: ~33KB (2768 items × 12 bytes) - ZIP batch: ~44KB (targets + sizes arrays) All indexes cleared immediately after each phase. No OOM risk on ESP32-C3. ## Note on Threshold All optimizations are gated by `LARGE_SPINE_THRESHOLD = 400` to preserve existing behavior for small books. However, the algorithms work correctly for any book size and are faster even for small books: | Book Size | Old O(n²) | New O(n log n) | Improvement | |-----------|-----------|----------------|-------------| | 10 ch | 100 ops | 50 ops | 2x | | 100 ch | 10K ops | 800 ops | 12x | | 400 ch | 160K ops | 4K ops | 40x | If preferred, the threshold could be removed to use the optimized path universally. ## Testing - [x] Shadow Slave (2768 chapters): 50s first-time indexing, loads and navigates correctly - [x] Normal book (87 chapters): 1.7s indexing, no regression - [x] Build passes - [x] clang-format passes ## Files Changed - `lib/Epub/Epub/parsers/ContentOpfParser.h/.cpp` - OPF manifest index - `lib/Epub/Epub/BookMetadataCache.h/.cpp` - TOC index + batch size lookup - `lib/ZipFile/ZipFile.h/.cpp` - New `fillUncompressedSizes()` API - `lib/Epub/Epub.cpp` - Timing logs
Algorithm Details (click to expand) ### Phase 1: OPF Pass - Manifest to Spine Lookup **Problem**: Each `` in spine must find matching `` in manifest. ``` OLD: For each of 2768 spine refs, scan all 2770 manifest items = 7.6M string comparisons NEW: While parsing manifest, build index: { hash("ch001"), len=5, file_offset=120 } Sort index, then binary search for each spine ref: 2768 × log₂(2770) ≈ 2768 × 11 = 30K comparisons ``` ### Phase 2: TOC Pass - TOC Entry to Spine Index Lookup **Problem**: Each TOC entry with `href="chapter0001.xhtml"` must find its spine index. ``` OLD: For each of 2768 TOC entries, scan all 2768 spine entries = 7.6M string comparisons NEW: At beginTocPass(), read spine once and build index: { hash("OEBPS/chapter0001.xhtml"), len=25, spineIndex=0 } Sort index, binary search for each TOC entry: 2768 × log₂(2768) ≈ 30K comparisons Clear index at endTocPass() to free memory. ``` ### Phase 3: buildBookBin - ZIP Size Lookup **Problem**: Need uncompressed file size for each spine item (for reading progress). Sizes are in ZIP central directory. ``` OLD: For each of 2768 spine items, scan ZIP central directory (2773 entries) = 7.6M filename reads + string comparisons Time: 506 seconds NEW: Step 1: Build targets from spine { hash("OEBPS/chapter0001.xhtml"), len=25, index=0 } Sort by (hash, len) Step 2: Single pass through ZIP central directory For each entry: - Compute hash ON THE FLY (no string allocation) - Binary search targets - If match: sizes[target.index] = uncompressedSize Step 3: Use sizes array directly (O(1) per spine item) Total: 2773 entries × log₂(2768) ≈ 33K comparisons Time: 35 seconds ``` ### Why Hash + Length? Using 64-bit FNV-1a hash + string length as a composite key: - Collision probability: ~1 in 2⁶⁴ × typical_path_lengths - No string storage needed in index (just 12-16 bytes per entry) - Integer comparisons are faster than string comparisons - Verification on match handles the rare collision case
--- _AI-assisted development. All changes tested on hardware._ --- lib/Epub/Epub.cpp | 9 ++ lib/Epub/Epub/BookMetadataCache.cpp | 165 ++++++++++++++++----- lib/Epub/Epub/BookMetadataCache.h | 23 +++ lib/Epub/Epub/parsers/ContentOpfParser.cpp | 74 +++++++-- lib/Epub/Epub/parsers/ContentOpfParser.h | 24 +++ lib/ZipFile/ZipFile.cpp | 134 +++++++++++++++-- lib/ZipFile/ZipFile.h | 26 ++++ 7 files changed, 396 insertions(+), 59 deletions(-) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 33f920b4..7559e3b3 100644 --- a/lib/Epub/Epub.cpp +++ b/lib/Epub/Epub.cpp @@ -226,6 +226,8 @@ bool Epub::load(const bool buildIfMissing) { Serial.printf("[%lu] [EBP] Cache not found, building spine/TOC cache\n", millis()); setupCacheDir(); + const uint32_t indexingStart = millis(); + // Begin building cache - stream entries to disk immediately if (!bookMetadataCache->beginWrite()) { Serial.printf("[%lu] [EBP] Could not begin writing cache\n", millis()); @@ -233,6 +235,7 @@ bool Epub::load(const bool buildIfMissing) { } // OPF Pass + const uint32_t opfStart = millis(); BookMetadataCache::BookMetadata bookMetadata; if (!bookMetadataCache->beginContentOpfPass()) { Serial.printf("[%lu] [EBP] Could not begin writing content.opf pass\n", millis()); @@ -246,8 +249,10 @@ bool Epub::load(const bool buildIfMissing) { Serial.printf("[%lu] [EBP] Could not end writing content.opf pass\n", millis()); return false; } + Serial.printf("[%lu] [EBP] OPF pass completed in %lu ms\n", millis(), millis() - opfStart); // TOC Pass - try EPUB 3 nav first, fall back to NCX + const uint32_t tocStart = millis(); if (!bookMetadataCache->beginTocPass()) { Serial.printf("[%lu] [EBP] Could not begin writing toc pass\n", millis()); return false; @@ -276,6 +281,7 @@ bool Epub::load(const bool buildIfMissing) { Serial.printf("[%lu] [EBP] Could not end writing toc pass\n", millis()); return false; } + Serial.printf("[%lu] [EBP] TOC pass completed in %lu ms\n", millis(), millis() - tocStart); // Close the cache files if (!bookMetadataCache->endWrite()) { @@ -284,10 +290,13 @@ bool Epub::load(const bool buildIfMissing) { } // Build final book.bin + const uint32_t buildStart = millis(); if (!bookMetadataCache->buildBookBin(filepath, bookMetadata)) { Serial.printf("[%lu] [EBP] Could not update mappings and sizes\n", millis()); return false; } + Serial.printf("[%lu] [EBP] buildBookBin completed in %lu ms\n", millis(), millis() - buildStart); + Serial.printf("[%lu] [EBP] Total indexing completed in %lu ms\n", millis(), millis() - indexingStart); if (!bookMetadataCache->cleanupTmpFiles()) { Serial.printf("[%lu] [EBP] Could not cleanup tmp files - ignoring\n", millis()); diff --git a/lib/Epub/Epub/BookMetadataCache.cpp b/lib/Epub/Epub/BookMetadataCache.cpp index 374cad2f..e7242138 100644 --- a/lib/Epub/Epub/BookMetadataCache.cpp +++ b/lib/Epub/Epub/BookMetadataCache.cpp @@ -40,7 +40,6 @@ bool BookMetadataCache::endContentOpfPass() { bool BookMetadataCache::beginTocPass() { Serial.printf("[%lu] [BMC] Beginning toc pass\n", millis()); - // Open spine file for reading if (!SdMan.openFileForRead("BMC", cachePath + tmpSpineBinFile, spineFile)) { return false; } @@ -48,12 +47,41 @@ bool BookMetadataCache::beginTocPass() { spineFile.close(); return false; } + + if (spineCount >= LARGE_SPINE_THRESHOLD) { + spineHrefIndex.clear(); + spineHrefIndex.reserve(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(entry.href.size()); + idx.spineIndex = static_cast(i); + spineHrefIndex.push_back(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; + Serial.printf("[%lu] [BMC] Using fast index for %d spine items\n", millis(), spineCount); + } else { + useSpineHrefIndex = false; + } + return true; } bool BookMetadataCache::endTocPass() { tocFile.close(); spineFile.close(); + + spineHrefIndex.clear(); + spineHrefIndex.shrink_to_fit(); + useSpineHrefIndex = false; + return true; } @@ -124,6 +152,18 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta // 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::vector 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(j); + } + } + } + ZipFile zip(epubPath); // Pre-open zip file to speed up size calculations if (!zip.open()) { @@ -133,31 +173,56 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta tocFile.close(); return false; } - // TODO: For large ZIPs loading the all localHeaderOffsets will crash. - // However not having them loaded is extremely slow. Need a better solution here. - // Perhaps only a cache of spine items or a better way to speedup lookups? - if (!zip.loadAllFileStatSlims()) { - Serial.printf("[%lu] [BMC] Could not load zip local header offsets for size calculations\n", millis()); - bookFile.close(); - spineFile.close(); - tocFile.close(); - zip.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::vector spineSizes; + bool useBatchSizes = false; + + if (spineCount >= LARGE_SPINE_THRESHOLD) { + Serial.printf("[%lu] [BMC] Using batch size lookup for %d spine items\n", millis(), spineCount); + + std::vector targets; + targets.reserve(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(path.size()); + t.index = static_cast(i); + targets.push_back(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); + Serial.printf("[%lu] [BMC] Batch lookup matched %d/%d spine items\n", millis(), 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); - tocFile.seek(0); - for (int j = 0; j < tocCount; j++) { - auto tocEntry = readTocEntry(tocFile); - if (tocEntry.spineIndex == i) { - spineEntry.tocIndex = j; - break; - } - } + 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 @@ -169,16 +234,25 @@ bool BookMetadataCache::buildBookBin(const std::string& epubPath, const BookMeta } lastSpineTocIndex = spineEntry.tocIndex; - // Calculate size for cumulative size size_t itemSize = 0; - const std::string path = FsHelpers::normalisePath(spineEntry.href); - if (zip.getInflatedFileSize(path.c_str(), &itemSize)) { - cumSize += itemSize; - spineEntry.cumulativeSize = cumSize; + if (useBatchSizes) { + itemSize = spineSizes[i]; + if (itemSize == 0) { + const std::string path = FsHelpers::normalisePath(spineEntry.href); + if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) { + Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str()); + } + } } else { - Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str()); + const std::string path = FsHelpers::normalisePath(spineEntry.href); + if (!zip.getInflatedFileSize(path.c_str(), &itemSize)) { + Serial.printf("[%lu] [BMC] Warning: Could not get size for spine item: %s\n", millis(), path.c_str()); + } } + cumSize += itemSize; + spineEntry.cumulativeSize = cumSize; + // Write out spine data to book.bin writeSpineEntry(bookFile, spineEntry); } @@ -248,21 +322,38 @@ void BookMetadataCache::createTocEntry(const std::string& title, const std::stri return; } - int spineIndex = -1; - // find spine index - // TODO: This lookup is slow as need to scan through all items each time. We can't hold it all in memory due to size. - // But perhaps we can load just the hrefs in a vector/list to do an index lookup? - spineFile.seek(0); - for (int i = 0; i < spineCount; i++) { - auto spineEntry = readSpineEntry(spineFile); - if (spineEntry.href == href) { - spineIndex = i; + int16_t spineIndex = -1; + + if (useSpineHrefIndex) { + uint64_t targetHash = fnvHash64(href); + uint16_t targetLen = static_cast(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) { - Serial.printf("[%lu] [BMC] addTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str()); + if (spineIndex == -1) { + Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), 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(i); + break; + } + } + if (spineIndex == -1) { + Serial.printf("[%lu] [BMC] createTocEntry: Could not find spine item for TOC href %s\n", millis(), href.c_str()); + } } const TocEntry entry(title, href, anchor, level, spineIndex); diff --git a/lib/Epub/Epub/BookMetadataCache.h b/lib/Epub/Epub/BookMetadataCache.h index 29b2ae4a..20ce6559 100644 --- a/lib/Epub/Epub/BookMetadataCache.h +++ b/lib/Epub/Epub/BookMetadataCache.h @@ -2,7 +2,9 @@ #include +#include #include +#include class BookMetadataCache { public: @@ -53,6 +55,27 @@ class BookMetadataCache { FsFile spineFile; FsFile 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::vector 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(c); + hash *= 1099511628211ull; + } + return hash; + } + uint32_t writeSpineEntry(FsFile& file, const SpineEntry& entry) const; uint32_t writeTocEntry(FsFile& file, const TocEntry& entry) const; SpineEntry readSpineEntry(FsFile& file) const; diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.cpp b/lib/Epub/Epub/parsers/ContentOpfParser.cpp index 9fbeb386..ce0e22ea 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.cpp +++ b/lib/Epub/Epub/parsers/ContentOpfParser.cpp @@ -38,6 +38,9 @@ ContentOpfParser::~ContentOpfParser() { if (SdMan.exists((cachePath + itemCacheFile).c_str())) { SdMan.remove((cachePath + itemCacheFile).c_str()); } + itemIndex.clear(); + itemIndex.shrink_to_fit(); + useItemIndex = false; } size_t ContentOpfParser::write(const uint8_t data) { return write(&data, 1); } @@ -129,6 +132,15 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name "[%lu] [COF] Couldn't open temp items file for reading. This is probably going to be a fatal error.\n", millis()); } + + // 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; + Serial.printf("[%lu] [COF] Using fast index for %zu manifest items\n", millis(), self->itemIndex.size()); + } return; } @@ -180,6 +192,15 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name } } + // Record index entry for fast lookup later + if (self->tempItemStore) { + ItemIndexEntry entry; + entry.idHash = fnvHash(itemId); + entry.idLen = static_cast(itemId.size()); + entry.fileOffset = static_cast(self->tempItemStore.position()); + self->itemIndex.push_back(entry); + } + // Write items down to SD card serialization::writeString(self->tempItemStore, itemId); serialization::writeString(self->tempItemStore, href); @@ -215,19 +236,50 @@ void XMLCALL ContentOpfParser::startElement(void* userData, const XML_Char* name for (int i = 0; atts[i]; i += 2) { if (strcmp(atts[i], "idref") == 0) { const std::string idref = atts[i + 1]; - // Resolve the idref to href using items map - // 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; std::string href; - while (self->tempItemStore.available()) { - serialization::readString(self->tempItemStore, itemId); - serialization::readString(self->tempItemStore, href); - if (itemId == idref) { - self->cache->createSpineEntry(href); - break; + bool found = false; + + if (self->useItemIndex) { + // Fast path: binary search + uint32_t targetHash = fnvHash(idref); + uint16_t targetLen = static_cast(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); } } } diff --git a/lib/Epub/Epub/parsers/ContentOpfParser.h b/lib/Epub/Epub/parsers/ContentOpfParser.h index 8c56a86f..b40a3787 100644 --- a/lib/Epub/Epub/parsers/ContentOpfParser.h +++ b/lib/Epub/Epub/parsers/ContentOpfParser.h @@ -1,6 +1,9 @@ #pragma once #include +#include +#include + #include "Epub.h" #include "expat.h" @@ -28,6 +31,27 @@ class ContentOpfParser final : public Print { FsFile 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::vector 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(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); diff --git a/lib/ZipFile/ZipFile.cpp b/lib/ZipFile/ZipFile.cpp index 2a97858a..a5f65ea3 100644 --- a/lib/ZipFile/ZipFile.cpp +++ b/lib/ZipFile/ZipFile.cpp @@ -4,6 +4,8 @@ #include #include +#include + bool inflateOneShot(const uint8_t* inputBuf, const size_t deflatedSize, uint8_t* outputBuf, const size_t inflatedSize) { // Setup inflator const auto inflator = static_cast(malloc(sizeof(tinfl_decompressor))); @@ -74,6 +76,10 @@ bool ZipFile::loadAllFileStatSlims() { file.seekCur(m + k); } + // Set cursor to start of central directory for sequential access + lastCentralDirPos = zipDetails.centralDirOffset; + lastCentralDirPosValid = true; + if (!wasOpen) { close(); } @@ -102,15 +108,35 @@ bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) { return false; } - file.seek(zipDetails.centralDirOffset); + // Phase 1: Try scanning from cursor position first + uint32_t startPos = lastCentralDirPosValid ? lastCentralDirPos : zipDetails.centralDirOffset; + uint32_t wrapPos = zipDetails.centralDirOffset; + bool wrapped = false; + bool found = false; + + file.seek(startPos); uint32_t sig; char itemName[256]; - bool found = false; - while (file.available()) { - file.read(&sig, 4); - if (sig != 0x02014b50) break; // End of list + 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); @@ -123,15 +149,25 @@ bool ZipFile::loadFileStatSlim(const char* filename, FileStatSlim* fileStat) { file.read(&k, 2); file.seekCur(8); file.read(&fileStat->localHeaderOffset, 4); - file.read(itemName, nameLen); - itemName[nameLen] = '\0'; - if (strcmp(itemName, filename) == 0) { - found = true; - break; + 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 the rest of this entry (extra field + comment) + // Skip extra field + comment file.seekCur(m + k); } @@ -253,6 +289,8 @@ bool ZipFile::close() { if (file) { file.close(); } + lastCentralDirPos = 0; + lastCentralDirPosValid = false; return true; } @@ -266,6 +304,80 @@ bool ZipFile::getInflatedFileSize(const char* filename, size_t* size) { return true; } +int ZipFile::fillUncompressedSizes(std::vector& targets, std::vector& sizes) { + if (targets.empty()) { + return 0; + } + + const bool wasOpen = isOpen(); + if (!wasOpen && !open()) { + return 0; + } + + if (!loadZipDetails()) { + if (!wasOpen) { + close(); + } + return 0; + } + + file.seek(zipDetails.centralDirOffset); + + int matched = 0; + 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; + } + } else { + file.seekCur(nameLen); + } + + file.seekCur(m + k); + } + + if (!wasOpen) { + close(); + } + + return matched; +} + uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, const bool trailingNullByte) { const bool wasOpen = isOpen(); if (!wasOpen && !open()) { diff --git a/lib/ZipFile/ZipFile.h b/lib/ZipFile/ZipFile.h index 0144ed42..0c82e5a0 100644 --- a/lib/ZipFile/ZipFile.h +++ b/lib/ZipFile/ZipFile.h @@ -3,6 +3,7 @@ #include #include +#include class ZipFile { public: @@ -19,12 +20,33 @@ class ZipFile { 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(s[i]); + hash *= 1099511628211ull; + } + return hash; + } + private: const std::string& filePath; FsFile file; ZipDetails zipDetails = {0, 0, false}; std::unordered_map 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(); @@ -39,6 +61,10 @@ class ZipFile { 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::vector& targets, std::vector& 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); From e0b6b9b28a4ed34bd60a51a1e851ca8d75a81ebb Mon Sep 17 00:00:00 2001 From: V Date: Tue, 27 Jan 2026 14:30:27 +0000 Subject: [PATCH 29/36] refactor: Re-work for OTA feature (#509) ## Summary Finally, I have received my device and got to chance to work on OTA. https://github.com/crosspoint-reader/crosspoint-reader/issues/176 * **What is the goal of this PR?** (e.g., Implements the new feature for file uploading.) Existing OTA functionality is very buggy, many of times (I would say 8 out of 10) are end up with fail for me. When the time that it works it is very slow and take ages. For others looks like end up with crash or different issues. * **What changes are included?** To be honest, I'm not familiar with Arduino APIs of OTA process, but looks like not good as much esp-idf itself. I always found Arduino APIs very bulky for esp32. Wrappers and wrappers. ## Additional Context Right now, OTA takes ~ 3min 10sec (of course depends on size of .bin file). Can be tested with playing version info inside from `platform.ini` file. ``` [crosspoint] version = 0.14.0 ``` --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**< NO >**_ --- src/activities/settings/OtaUpdateActivity.cpp | 11 +- src/network/OtaUpdater.cpp | 219 +++++++++++++----- src/network/OtaUpdater.h | 15 +- 3 files changed, 174 insertions(+), 71 deletions(-) diff --git a/src/activities/settings/OtaUpdateActivity.cpp b/src/activities/settings/OtaUpdateActivity.cpp index 0393847d..86dcf2ac 100644 --- a/src/activities/settings/OtaUpdateActivity.cpp +++ b/src/activities/settings/OtaUpdateActivity.cpp @@ -97,7 +97,7 @@ void OtaUpdateActivity::onExit() { void OtaUpdateActivity::displayTaskLoop() { while (true) { - if (updateRequired) { + if (updateRequired || updater.getRender()) { updateRequired = false; xSemaphoreTake(renderingMutex, portMAX_DELAY); render(); @@ -115,8 +115,9 @@ void OtaUpdateActivity::render() { float updaterProgress = 0; if (state == UPDATE_IN_PROGRESS) { - Serial.printf("[%lu] [OTA] Update progress: %d / %d\n", millis(), updater.processedSize, updater.totalSize); - updaterProgress = static_cast(updater.processedSize) / static_cast(updater.totalSize); + Serial.printf("[%lu] [OTA] Update progress: %d / %d\n", millis(), updater.getProcessedSize(), + updater.getTotalSize()); + updaterProgress = static_cast(updater.getProcessedSize()) / static_cast(updater.getTotalSize()); // Only update every 2% at the most if (static_cast(updaterProgress * 50) == lastUpdaterPercentage / 2) { return; @@ -154,7 +155,7 @@ void OtaUpdateActivity::render() { (std::to_string(static_cast(updaterProgress * 100)) + "%").c_str()); renderer.drawCenteredText( UI_10_FONT_ID, 440, - (std::to_string(updater.processedSize) + " / " + std::to_string(updater.totalSize)).c_str()); + (std::to_string(updater.getProcessedSize()) + " / " + std::to_string(updater.getTotalSize())).c_str()); renderer.displayBuffer(); return; } @@ -194,7 +195,7 @@ void OtaUpdateActivity::loop() { xSemaphoreGive(renderingMutex); updateRequired = true; vTaskDelay(10 / portTICK_PERIOD_MS); - const auto res = updater.installUpdate([this](const size_t, const size_t) { updateRequired = true; }); + const auto res = updater.installUpdate(); if (res != OtaUpdater::OK) { Serial.printf("[%lu] [OTA] Update failed: %d\n", millis(), res); diff --git a/src/network/OtaUpdater.cpp b/src/network/OtaUpdater.cpp index d831af0a..1733e136 100644 --- a/src/network/OtaUpdater.cpp +++ b/src/network/OtaUpdater.cpp @@ -1,38 +1,123 @@ #include "OtaUpdater.h" #include -#include -#include + +#include "esp_http_client.h" +#include "esp_https_ota.h" +#include "esp_wifi.h" namespace { constexpr char latestReleaseUrl[] = "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/latest"; + +/* This is buffer and size holder to keep upcoming data from latestReleaseUrl */ +char* local_buf; +int output_len; + +/* + * When esp_crt_bundle.h included, it is pointing wrong header file + * which is something under WifiClientSecure because of our framework based on arduno platform. + * To manage this obstacle, don't include anything, just extern and it will point correct one. + */ +extern "C" { +extern esp_err_t esp_crt_bundle_attach(void* conf); } +esp_err_t http_client_set_header_cb(esp_http_client_handle_t http_client) { + return esp_http_client_set_header(http_client, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); +} + +esp_err_t event_handler(esp_http_client_event_t* event) { + /* We do interested in only HTTP_EVENT_ON_DATA event only */ + if (event->event_id != HTTP_EVENT_ON_DATA) return ESP_OK; + + if (!esp_http_client_is_chunked_response(event->client)) { + int content_len = esp_http_client_get_content_length(event->client); + int copy_len = 0; + + if (local_buf == NULL) { + /* local_buf life span is tracked by caller checkForUpdate */ + local_buf = static_cast(calloc(content_len + 1, sizeof(char))); + output_len = 0; + if (local_buf == NULL) { + Serial.printf("[%lu] [OTA] HTTP Client Out of Memory Failed, Allocation %d\n", millis(), content_len); + return ESP_ERR_NO_MEM; + } + } + copy_len = min(event->data_len, (content_len - output_len)); + if (copy_len) { + memcpy(local_buf + output_len, event->data, copy_len); + } + output_len += copy_len; + } else { + /* Code might be hits here, It happened once (for version checking) but I need more logs to handle that */ + int chunked_len; + esp_http_client_get_chunk_length(event->client, &chunked_len); + Serial.printf("[%lu] [OTA] esp_http_client_is_chunked_response failed, chunked_len: %d\n", millis(), chunked_len); + } + + return ESP_OK; +} /* event_handler */ +} /* namespace */ + OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() { - const std::unique_ptr client(new WiFiClientSecure); - client->setInsecure(); - HTTPClient http; + JsonDocument filter; + esp_err_t esp_err; + JsonDocument doc; - Serial.printf("[%lu] [OTA] Fetching: %s\n", millis(), latestReleaseUrl); + esp_http_client_config_t client_config = { + .url = latestReleaseUrl, + .event_handler = event_handler, + /* Default HTTP client buffer size 512 byte only */ + .buffer_size = 8192, + .buffer_size_tx = 8192, + .skip_cert_common_name_check = true, + .crt_bundle_attach = esp_crt_bundle_attach, + .keep_alive_enable = true, + }; - http.begin(*client, latestReleaseUrl); - http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); + /* To track life time of local_buf, dtor will be called on exit from that function */ + struct localBufCleaner { + char** bufPtr; + ~localBufCleaner() { + if (*bufPtr) { + free(*bufPtr); + *bufPtr = NULL; + } + } + } localBufCleaner = {&local_buf}; - const int httpCode = http.GET(); - if (httpCode != HTTP_CODE_OK) { - Serial.printf("[%lu] [OTA] HTTP error: %d\n", millis(), httpCode); - http.end(); + esp_http_client_handle_t client_handle = esp_http_client_init(&client_config); + if (!client_handle) { + Serial.printf("[%lu] [OTA] HTTP Client Handle Failed\n", millis()); + return INTERNAL_UPDATE_ERROR; + } + + esp_err = esp_http_client_set_header(client_handle, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); + if (esp_err != ESP_OK) { + Serial.printf("[%lu] [OTA] esp_http_client_set_header Failed : %s\n", millis(), esp_err_to_name(esp_err)); + esp_http_client_cleanup(client_handle); + return INTERNAL_UPDATE_ERROR; + } + + esp_err = esp_http_client_perform(client_handle); + if (esp_err != ESP_OK) { + Serial.printf("[%lu] [OTA] esp_http_client_perform Failed : %s\n", millis(), esp_err_to_name(esp_err)); + esp_http_client_cleanup(client_handle); return HTTP_ERROR; } - JsonDocument doc; - JsonDocument filter; + /* esp_http_client_close will be called inside cleanup as well*/ + esp_err = esp_http_client_cleanup(client_handle); + if (esp_err != ESP_OK) { + Serial.printf("[%lu] [OTA] esp_http_client_cleanupp Failed : %s\n", millis(), esp_err_to_name(esp_err)); + return INTERNAL_UPDATE_ERROR; + } + filter["tag_name"] = true; filter["assets"][0]["name"] = true; filter["assets"][0]["browser_download_url"] = true; filter["assets"][0]["size"] = true; - const DeserializationError error = deserializeJson(doc, *client, DeserializationOption::Filter(filter)); - http.end(); + const DeserializationError error = deserializeJson(doc, local_buf, DeserializationOption::Filter(filter)); if (error) { Serial.printf("[%lu] [OTA] JSON parse failed: %s\n", millis(), error.c_str()); return JSON_PARSE_ERROR; @@ -42,6 +127,7 @@ OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() { Serial.printf("[%lu] [OTA] No tag_name found\n", millis()); return JSON_PARSE_ERROR; } + if (!doc["assets"].is()) { Serial.printf("[%lu] [OTA] No assets found\n", millis()); return JSON_PARSE_ERROR; @@ -104,67 +190,74 @@ bool OtaUpdater::isUpdateNewer() const { const std::string& OtaUpdater::getLatestVersion() const { return latestVersion; } -OtaUpdater::OtaUpdaterError OtaUpdater::installUpdate(const std::function& onProgress) { +OtaUpdater::OtaUpdaterError OtaUpdater::installUpdate() { if (!isUpdateNewer()) { return UPDATE_OLDER_ERROR; } - const std::unique_ptr client(new WiFiClientSecure); - client->setInsecure(); - HTTPClient http; + esp_https_ota_handle_t ota_handle = NULL; + esp_err_t esp_err; + /* Signal for OtaUpdateActivity */ + render = false; - Serial.printf("[%lu] [OTA] Fetching: %s\n", millis(), otaUrl.c_str()); + esp_http_client_config_t client_config = { + .url = otaUrl.c_str(), + .timeout_ms = 15000, + /* Default HTTP client buffer size 512 byte only + * not sufficent to handle URL redirection cases or + * parsing of large HTTP headers. + */ + .buffer_size = 8192, + .buffer_size_tx = 8192, + .skip_cert_common_name_check = true, + .crt_bundle_attach = esp_crt_bundle_attach, + .keep_alive_enable = true, + }; - http.begin(*client, otaUrl.c_str()); - http.setFollowRedirects(HTTPC_STRICT_FOLLOW_REDIRECTS); - http.addHeader("User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION); - const int httpCode = http.GET(); + esp_https_ota_config_t ota_config = { + .http_config = &client_config, + .http_client_init_cb = http_client_set_header_cb, + }; - if (httpCode != HTTP_CODE_OK) { - Serial.printf("[%lu] [OTA] Download failed: %d\n", millis(), httpCode); - http.end(); + /* For better timing and connectivity, we disable power saving for WiFi */ + esp_wifi_set_ps(WIFI_PS_NONE); + + esp_err = esp_https_ota_begin(&ota_config, &ota_handle); + if (esp_err != ESP_OK) { + Serial.printf("[%lu] [OTA] HTTP OTA Begin Failed: %s\n", millis(), esp_err_to_name(esp_err)); + return INTERNAL_UPDATE_ERROR; + } + + do { + esp_err = esp_https_ota_perform(ota_handle); + processedSize = esp_https_ota_get_image_len_read(ota_handle); + /* Sent signal to OtaUpdateActivity */ + render = true; + vTaskDelay(10 / portTICK_PERIOD_MS); + } while (esp_err == ESP_ERR_HTTPS_OTA_IN_PROGRESS); + + /* Return back to default power saving for WiFi in case of failing */ + esp_wifi_set_ps(WIFI_PS_MIN_MODEM); + + if (esp_err != ESP_OK) { + Serial.printf("[%lu] [OTA] esp_https_ota_perform Failed: %s\n", millis(), esp_err_to_name(esp_err)); + esp_https_ota_finish(ota_handle); return HTTP_ERROR; } - // 2. Get length and stream - const size_t contentLength = http.getSize(); - - if (contentLength != otaSize) { - Serial.printf("[%lu] [OTA] Invalid content length\n", millis()); - http.end(); - return HTTP_ERROR; - } - - // 3. Begin the ESP-IDF Update process - if (!Update.begin(otaSize)) { - Serial.printf("[%lu] [OTA] Not enough space. Error: %s\n", millis(), Update.errorString()); - http.end(); + if (!esp_https_ota_is_complete_data_received(ota_handle)) { + Serial.printf("[%lu] [OTA] esp_https_ota_is_complete_data_received Failed: %s\n", millis(), + esp_err_to_name(esp_err)); + esp_https_ota_finish(ota_handle); return INTERNAL_UPDATE_ERROR; } - this->totalSize = otaSize; - Serial.printf("[%lu] [OTA] Update started\n", millis()); - Update.onProgress([this, onProgress](const size_t progress, const size_t total) { - this->processedSize = progress; - this->totalSize = total; - onProgress(progress, total); - }); - const size_t written = Update.writeStream(*client); - http.end(); - - if (written == otaSize) { - Serial.printf("[%lu] [OTA] Successfully written %u bytes\n", millis(), written); - } else { - Serial.printf("[%lu] [OTA] Written only %u/%u bytes. Error: %s\n", millis(), written, otaSize, - Update.errorString()); + esp_err = esp_https_ota_finish(ota_handle); + if (esp_err != ESP_OK) { + Serial.printf("[%lu] [OTA] esp_https_ota_finish Failed: %s\n", millis(), esp_err_to_name(esp_err)); return INTERNAL_UPDATE_ERROR; } - if (Update.end() && Update.isFinished()) { - Serial.printf("[%lu] [OTA] Update complete\n", millis()); - return OK; - } else { - Serial.printf("[%lu] [OTA] Error Occurred: %s\n", millis(), Update.errorString()); - return INTERNAL_UPDATE_ERROR; - } + Serial.printf("[%lu] [OTA] Update completed\n", millis()); + return OK; } diff --git a/src/network/OtaUpdater.h b/src/network/OtaUpdater.h index 817f24b1..24e04cf5 100644 --- a/src/network/OtaUpdater.h +++ b/src/network/OtaUpdater.h @@ -8,6 +8,9 @@ class OtaUpdater { std::string latestVersion; std::string otaUrl; size_t otaSize = 0; + size_t processedSize = 0; + size_t totalSize = 0; + bool render = false; public: enum OtaUpdaterError { @@ -19,12 +22,18 @@ class OtaUpdater { INTERNAL_UPDATE_ERROR, OOM_ERROR, }; - size_t processedSize = 0; - size_t totalSize = 0; + + size_t getOtaSize() const { return otaSize; } + + size_t getProcessedSize() const { return processedSize; } + + size_t getTotalSize() const { return totalSize; } + + bool getRender() const { return render; } OtaUpdater() = default; bool isUpdateNewer() const; const std::string& getLatestVersion() const; OtaUpdaterError checkForUpdate(); - OtaUpdaterError installUpdate(const std::function& onProgress); + OtaUpdaterError installUpdate(); }; From 140fcb9db56c3d271116b98fcd2e8ecf28bb174a Mon Sep 17 00:00:00 2001 From: Arthur Tazhitdinov Date: Tue, 27 Jan 2026 20:09:05 +0500 Subject: [PATCH 30/36] fix: missing front layout in mapLabels() (#564) ## Summary * adds missing front layout to mapLabels function --- src/MappedInputManager.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 14c45deb..25095be7 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -85,6 +85,8 @@ MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const return {previous, next, back, confirm}; case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: return {previous, back, confirm, next}; + case CrossPointSettings::BACK_CONFIRM_RIGHT_LEFT: + return {back, confirm, next, previous}; case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: default: return {back, confirm, previous, next}; From 8c1c80787a1620b728c5afa926a7f95c66c8503d Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Wed, 28 Jan 2026 02:43:04 +1100 Subject: [PATCH 31/36] fix: Render keyboard entry over multiple lines (#567) ## Summary * Render keyboard entry over multiple lines * Grows display areas based on input text * Shown on OPDS entry, but applies everywhere ## Additional Context * Fixes https://github.com/crosspoint-reader/crosspoint-reader/issues/554 | One line | Multi-line | | --- | --- | | ![IMG_5925](https://github.com/user-attachments/assets/28be00a8-7b90-4bf6-9ebf-4d4ad6642bc9) | ![IMG_5926](https://github.com/user-attachments/assets/1c69a96f-d868-49a1-866c-546ca7b784ab) | --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? No --- src/activities/util/KeyboardEntryActivity.cpp | 35 ++++++++++++------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/activities/util/KeyboardEntryActivity.cpp b/src/activities/util/KeyboardEntryActivity.cpp index 8c36ac33..3a6befac 100644 --- a/src/activities/util/KeyboardEntryActivity.cpp +++ b/src/activities/util/KeyboardEntryActivity.cpp @@ -256,8 +256,9 @@ void KeyboardEntryActivity::render() const { renderer.drawCenteredText(UI_10_FONT_ID, startY, title.c_str()); // Draw input field - const int inputY = startY + 22; - renderer.drawText(UI_10_FONT_ID, 10, inputY, "["); + const int inputStartY = startY + 22; + int inputEndY = startY + 22; + renderer.drawText(UI_10_FONT_ID, 10, inputStartY, "["); std::string displayText; if (isPassword) { @@ -269,19 +270,29 @@ void KeyboardEntryActivity::render() const { // Show cursor at end displayText += "_"; - // Truncate if too long for display - use actual character width from font - int approxCharWidth = renderer.getSpaceWidth(UI_10_FONT_ID); - if (approxCharWidth < 1) approxCharWidth = 8; // Fallback to approximate width - const int maxDisplayLen = (pageWidth - 40) / approxCharWidth; - if (displayText.length() > static_cast(maxDisplayLen)) { - displayText = "..." + displayText.substr(displayText.length() - maxDisplayLen + 3); - } + // Render input text across multiple lines + int lineStartIdx = 0; + int lineEndIdx = displayText.length(); + while (true) { + std::string lineText = displayText.substr(lineStartIdx, lineEndIdx - lineStartIdx); + const int textWidth = renderer.getTextWidth(UI_10_FONT_ID, lineText.c_str()); + if (textWidth <= pageWidth - 40) { + renderer.drawText(UI_10_FONT_ID, 20, inputEndY, lineText.c_str()); + if (lineEndIdx == displayText.length()) { + break; + } - renderer.drawText(UI_10_FONT_ID, 20, inputY, displayText.c_str()); - renderer.drawText(UI_10_FONT_ID, pageWidth - 15, inputY, "]"); + inputEndY += renderer.getLineHeight(UI_10_FONT_ID); + lineStartIdx = lineEndIdx; + lineEndIdx = displayText.length(); + } else { + lineEndIdx -= 1; + } + } + renderer.drawText(UI_10_FONT_ID, pageWidth - 15, inputEndY, "]"); // Draw keyboard - use compact spacing to fit 5 rows on screen - const int keyboardStartY = inputY + 25; + const int keyboardStartY = inputEndY + 25; constexpr int keyWidth = 18; constexpr int keyHeight = 18; constexpr int keySpacing = 3; From 5894ae5afe728efec73e8ab1ceaf1b0bfe4216cc Mon Sep 17 00:00:00 2001 From: Boris Faure Date: Tue, 27 Jan 2026 17:32:33 +0100 Subject: [PATCH 32/36] chore: .gitignore: add compile_commands.json & .cache (#568) ## Summary * **What is the goal of this PR?** Quality of Life * **What changes are included?** Add compile_commands.json & .cache to .gitignore . Both are use by clangd that can help IDE support. Run `pio run --target compiledb` to generate `compile_commands.json`. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**NO**_ --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 754c9f68..ec281eb9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ lib/EpdFont/fontsrc *.generated.h .vs build -**/__pycache__/ \ No newline at end of file +**/__pycache__/ +/compile_commands.json +/.cache From 712c566664577020a7ee382531346d51c464c35f Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Wed, 28 Jan 2026 03:33:36 +1100 Subject: [PATCH 33/36] fix: Correctly render italics on image alt placeholders (#569) ## Summary * Correctly render italics on image alt placeholders * Parser incorrectly handled depth of self-closing tags * Self-closing tags immediately call start and end tag ## Additional Context * Previously, it would incorrectly make the whole chapter bold/italics, or not italicised the image alt --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? No --- .../Epub/parsers/ChapterHtmlSlimParser.cpp | 86 ++++++++++++------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index f6d96be4..298c4ec6 100644 --- a/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp +++ b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp @@ -84,41 +84,42 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (strcmp(name, "table") == 0) { // Add placeholder text self->startNewTextBlock(TextBlock::CENTER_ALIGN); - if (self->currentTextBlock) { - self->currentTextBlock->addWord("[Table omitted]", EpdFontFamily::ITALIC); - } - // Skip table contents - self->skipUntilDepth = self->depth; + self->italicUntilDepth = min(self->italicUntilDepth, self->depth); + // Advance depth before processing character data (like you would for a element with text) self->depth += 1; + self->characterData(userData, "[Table omitted]", strlen("[Table omitted]")); + + // Skip table contents (skip until parent as we pre-advanced depth above) + self->skipUntilDepth = self->depth - 1; return; } if (matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS)) { // TODO: Start processing image tags - std::string alt; + std::string alt = "[Image]"; if (atts != nullptr) { for (int i = 0; atts[i]; i += 2) { if (strcmp(atts[i], "alt") == 0) { - // add " " (counts as whitespace) at the end of alt - // so the corresponding text block ends. - // TODO: A zero-width breaking space would be more appropriate (once/if we support it) - alt = "[Image: " + std::string(atts[i + 1]) + "] "; + if (strlen(atts[i + 1]) > 0) { + alt = "[Image: " + std::string(atts[i + 1]) + "]"; + } + break; } } - Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str()); - - self->startNewTextBlock(TextBlock::CENTER_ALIGN); - self->italicUntilDepth = min(self->italicUntilDepth, self->depth); - self->depth += 1; - self->characterData(userData, alt.c_str(), alt.length()); - return; - } else { - // Skip for now - self->skipUntilDepth = self->depth; - self->depth += 1; - return; } + + Serial.printf("[%lu] [EHP] Image alt: %s\n", millis(), alt.c_str()); + + self->startNewTextBlock(TextBlock::CENTER_ALIGN); + self->italicUntilDepth = min(self->italicUntilDepth, self->depth); + // Advance depth before processing character data (like you would for a element with text) + self->depth += 1; + self->characterData(userData, alt.c_str(), alt.length()); + + // Skip table contents (skip until parent as we pre-advanced depth above) + self->skipUntilDepth = self->depth - 1; + return; } if (matches(name, SKIP_TAGS, NUM_SKIP_TAGS)) { @@ -143,25 +144,43 @@ void XMLCALL ChapterHtmlSlimParser::startElement(void* userData, const XML_Char* if (matches(name, HEADER_TAGS, NUM_HEADER_TAGS)) { self->startNewTextBlock(TextBlock::CENTER_ALIGN); self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); - } else if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { + self->depth += 1; + return; + } + + if (matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS)) { if (strcmp(name, "br") == 0) { if (self->partWordBufferIndex > 0) { // flush word preceding
to currentTextBlock before calling startNewTextBlock self->flushPartWordBuffer(); } self->startNewTextBlock(self->currentTextBlock->getStyle()); - } else { - self->startNewTextBlock((TextBlock::Style)self->paragraphAlignment); - if (strcmp(name, "li") == 0) { - self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR); - } + self->depth += 1; + return; } - } else if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) { - self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); - } else if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) { - self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth); + + self->startNewTextBlock(static_cast(self->paragraphAlignment)); + if (strcmp(name, "li") == 0) { + self->currentTextBlock->addWord("\xe2\x80\xa2", EpdFontFamily::REGULAR); + } + + self->depth += 1; + return; } + if (matches(name, BOLD_TAGS, NUM_BOLD_TAGS)) { + self->boldUntilDepth = std::min(self->boldUntilDepth, self->depth); + self->depth += 1; + return; + } + + if (matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS)) { + self->italicUntilDepth = std::min(self->italicUntilDepth, self->depth); + self->depth += 1; + return; + } + + // Unprocessed tag, just increasing depth and continue forward self->depth += 1; } @@ -227,7 +246,8 @@ void XMLCALL ChapterHtmlSlimParser::endElement(void* userData, const XML_Char* n // text styling needs to be overhauled to fix it. const bool shouldBreakText = matches(name, BLOCK_TAGS, NUM_BLOCK_TAGS) || matches(name, HEADER_TAGS, NUM_HEADER_TAGS) || - matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || self->depth == 1; + matches(name, BOLD_TAGS, NUM_BOLD_TAGS) || matches(name, ITALIC_TAGS, NUM_ITALIC_TAGS) || + strcmp(name, "table") == 0 || matches(name, IMAGE_TAGS, NUM_IMAGE_TAGS) || self->depth == 1; if (shouldBreakText) { self->flushPartWordBuffer(); From ebcd813ff6c49ccf9bafb3146b51d2c0d2278170 Mon Sep 17 00:00:00 2001 From: Dave Allie Date: Wed, 28 Jan 2026 04:08:04 +1100 Subject: [PATCH 34/36] chore: Cut release 0.16.0 --- platformio.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/platformio.ini b/platformio.ini index 7f42637d..e8574470 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,7 @@ default_envs = default [crosspoint] -version = 0.15.0 +version = 0.16.0 [base] platform = espressif32 @ 6.12.0 From 172916afd41e6f92ee32970055a2c982a7d6b3ec Mon Sep 17 00:00:00 2001 From: Eliz Date: Tue, 27 Jan 2026 17:25:03 +0000 Subject: [PATCH 35/36] feat: Display epub metadata on Recents (#511) * **What is the goal of this PR?** Implement a metadata viewer for the Recents screen * **What changes are included?** | Recents | Files | | --- | --- | | image | image | For the Files screen, I have not made any changes on purpose. For the Recents screen, we now display the Book title and author. If it is a file with no epub metadata like txt or md, we display the file name without the file extension. --- Did you use AI tools to help write this code? _**< YES >**_ Although I went trough all the code manually and made changes as well, please be aware the majority of the code is AI generated. --------- Co-authored-by: Eliz Kilic --- src/RecentBooksStore.cpp | 58 ++++++++++------ src/RecentBooksStore.h | 18 +++-- src/activities/home/MyLibraryActivity.cpp | 72 +++++++++++--------- src/activities/home/MyLibraryActivity.h | 4 +- src/activities/reader/EpubReaderActivity.cpp | 2 +- src/activities/reader/TxtReaderActivity.cpp | 2 +- src/activities/reader/XtcReaderActivity.cpp | 2 +- 7 files changed, 95 insertions(+), 63 deletions(-) diff --git a/src/RecentBooksStore.cpp b/src/RecentBooksStore.cpp index 03cfbbd7..5932de36 100644 --- a/src/RecentBooksStore.cpp +++ b/src/RecentBooksStore.cpp @@ -7,22 +7,23 @@ #include namespace { -constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 1; +constexpr uint8_t RECENT_BOOKS_FILE_VERSION = 2; constexpr char RECENT_BOOKS_FILE[] = "/.crosspoint/recent.bin"; constexpr int MAX_RECENT_BOOKS = 10; } // namespace RecentBooksStore RecentBooksStore::instance; -void RecentBooksStore::addBook(const std::string& path) { +void RecentBooksStore::addBook(const std::string& path, const std::string& title, const std::string& author) { // Remove existing entry if present - auto it = std::find(recentBooks.begin(), recentBooks.end(), path); + auto it = + std::find_if(recentBooks.begin(), recentBooks.end(), [&](const RecentBook& book) { return book.path == path; }); if (it != recentBooks.end()) { recentBooks.erase(it); } // Add to front - recentBooks.insert(recentBooks.begin(), path); + recentBooks.insert(recentBooks.begin(), {path, title, author}); // Trim to max size if (recentBooks.size() > MAX_RECENT_BOOKS) { @@ -46,7 +47,9 @@ bool RecentBooksStore::saveToFile() const { serialization::writePod(outputFile, count); for (const auto& book : recentBooks) { - serialization::writeString(outputFile, book); + serialization::writeString(outputFile, book.path); + serialization::writeString(outputFile, book.title); + serialization::writeString(outputFile, book.author); } outputFile.close(); @@ -63,24 +66,41 @@ bool RecentBooksStore::loadFromFile() { uint8_t version; serialization::readPod(inputFile, version); if (version != RECENT_BOOKS_FILE_VERSION) { - Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version); - inputFile.close(); - return false; - } + if (version == 1) { + // Old version, just read paths + uint8_t count; + serialization::readPod(inputFile, count); + recentBooks.clear(); + recentBooks.reserve(count); + for (uint8_t i = 0; i < count; i++) { + std::string path; + serialization::readString(inputFile, path); + // Title and author will be empty, they will be filled when the book is + // opened again + recentBooks.push_back({path, "", ""}); + } + } else { + Serial.printf("[%lu] [RBS] Deserialization failed: Unknown version %u\n", millis(), version); + inputFile.close(); + return false; + } + } else { + uint8_t count; + serialization::readPod(inputFile, count); - uint8_t count; - serialization::readPod(inputFile, count); + recentBooks.clear(); + recentBooks.reserve(count); - recentBooks.clear(); - recentBooks.reserve(count); - - for (uint8_t i = 0; i < count; i++) { - std::string path; - serialization::readString(inputFile, path); - recentBooks.push_back(path); + for (uint8_t i = 0; i < count; i++) { + std::string path, title, author; + serialization::readString(inputFile, path); + serialization::readString(inputFile, title); + serialization::readString(inputFile, author); + recentBooks.push_back({path, title, author}); + } } inputFile.close(); - Serial.printf("[%lu] [RBS] Recent books loaded from file (%d entries)\n", millis(), count); + Serial.printf("[%lu] [RBS] Recent books loaded from file (%d entries)\n", millis(), recentBooks.size()); return true; } diff --git a/src/RecentBooksStore.h b/src/RecentBooksStore.h index b98bd406..7b87f1e0 100644 --- a/src/RecentBooksStore.h +++ b/src/RecentBooksStore.h @@ -2,11 +2,19 @@ #include #include +struct RecentBook { + std::string path; + std::string title; + std::string author; + + bool operator==(const RecentBook& other) const { return path == other.path; } +}; + class RecentBooksStore { // Static instance static RecentBooksStore instance; - std::vector recentBooks; + std::vector recentBooks; public: ~RecentBooksStore() = default; @@ -14,11 +22,11 @@ class RecentBooksStore { // Get singleton instance static RecentBooksStore& getInstance() { return instance; } - // Add a book path to the recent list (moves to front if already exists) - void addBook(const std::string& path); + // Add a book to the recent list (moves to front if already exists) + void addBook(const std::string& path, const std::string& title, const std::string& author); - // Get the list of recent book paths (most recent first) - const std::vector& getBooks() const { return recentBooks; } + // Get the list of recent books (most recent first) + const std::vector& getBooks() const { return recentBooks; } // Get the count of recent books int getCount() const { return static_cast(recentBooks.size()); } diff --git a/src/activities/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 1db32397..29c6ea73 100644 --- a/src/activities/home/MyLibraryActivity.cpp +++ b/src/activities/home/MyLibraryActivity.cpp @@ -16,6 +16,7 @@ namespace { constexpr int TAB_BAR_Y = 15; constexpr int CONTENT_START_Y = 60; constexpr int LINE_HEIGHT = 30; +constexpr int RECENTS_LINE_HEIGHT = 65; // Increased for two-line items constexpr int LEFT_MARGIN = 20; constexpr int RIGHT_MARGIN = 40; // Extra space for scroll indicator @@ -47,7 +48,7 @@ int MyLibraryActivity::getPageItems() const { int MyLibraryActivity::getCurrentItemCount() const { if (currentTab == Tab::Recent) { - return static_cast(bookTitles.size()); + return static_cast(recentBooks.size()); } return static_cast(files.size()); } @@ -65,34 +66,16 @@ int MyLibraryActivity::getCurrentPage() const { } void MyLibraryActivity::loadRecentBooks() { - constexpr size_t MAX_RECENT_BOOKS = 20; - - bookTitles.clear(); - bookPaths.clear(); + recentBooks.clear(); const auto& books = RECENT_BOOKS.getBooks(); - bookTitles.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); - bookPaths.reserve(std::min(books.size(), MAX_RECENT_BOOKS)); - - for (const auto& path : books) { - // Limit to maximum number of recent books - if (bookTitles.size() >= MAX_RECENT_BOOKS) { - break; - } + recentBooks.reserve(books.size()); + for (const auto& book : books) { // Skip if file no longer exists - if (!SdMan.exists(path.c_str())) { + if (!SdMan.exists(book.path.c_str())) { continue; } - - // Extract filename from path for display - std::string title = path; - const size_t lastSlash = title.find_last_of('/'); - if (lastSlash != std::string::npos) { - title = title.substr(lastSlash + 1); - } - - bookTitles.push_back(title); - bookPaths.push_back(path); + recentBooks.push_back(book); } } @@ -176,8 +159,6 @@ void MyLibraryActivity::onExit() { vSemaphoreDelete(renderingMutex); renderingMutex = nullptr; - bookTitles.clear(); - bookPaths.clear(); files.clear(); } @@ -207,8 +188,8 @@ void MyLibraryActivity::loop() { // Confirm button - open selected item if (mappedInput.wasReleased(MappedInputManager::Button::Confirm)) { if (currentTab == Tab::Recent) { - if (!bookPaths.empty() && selectorIndex < static_cast(bookPaths.size())) { - onSelectBook(bookPaths[selectorIndex], currentTab); + if (!recentBooks.empty() && selectorIndex < static_cast(recentBooks.size())) { + onSelectBook(recentBooks[selectorIndex].path, currentTab); } } else { // Files tab @@ -333,7 +314,7 @@ void MyLibraryActivity::render() const { void MyLibraryActivity::renderRecentTab() const { const auto pageWidth = renderer.getScreenWidth(); const int pageItems = getPageItems(); - const int bookCount = static_cast(bookTitles.size()); + const int bookCount = static_cast(recentBooks.size()); if (bookCount == 0) { renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books"); @@ -343,14 +324,37 @@ void MyLibraryActivity::renderRecentTab() const { const auto pageStartIndex = selectorIndex / pageItems * pageItems; // Draw selection highlight - renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * LINE_HEIGHT - 2, pageWidth - RIGHT_MARGIN, - LINE_HEIGHT); + renderer.fillRect(0, CONTENT_START_Y + (selectorIndex % pageItems) * RECENTS_LINE_HEIGHT - 2, + pageWidth - RIGHT_MARGIN, RECENTS_LINE_HEIGHT); // Draw items for (int i = pageStartIndex; i < bookCount && i < pageStartIndex + pageItems; i++) { - auto item = renderer.truncatedText(UI_10_FONT_ID, bookTitles[i].c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); - renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y + (i % pageItems) * LINE_HEIGHT, item.c_str(), - i != selectorIndex); + const auto& book = recentBooks[i]; + const int y = CONTENT_START_Y + (i % pageItems) * RECENTS_LINE_HEIGHT; + + // Line 1: Title + std::string title = book.title; + if (title.empty()) { + // Fallback for older entries or files without metadata + title = book.path; + const size_t lastSlash = title.find_last_of('/'); + if (lastSlash != std::string::npos) { + title = title.substr(lastSlash + 1); + } + const size_t dot = title.find_last_of('.'); + if (dot != std::string::npos) { + title.resize(dot); + } + } + auto truncatedTitle = renderer.truncatedText(UI_12_FONT_ID, title.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_12_FONT_ID, LEFT_MARGIN, y + 2, truncatedTitle.c_str(), i != selectorIndex); + + // Line 2: Author + if (!book.author.empty()) { + auto truncatedAuthor = + renderer.truncatedText(UI_10_FONT_ID, book.author.c_str(), pageWidth - LEFT_MARGIN - RIGHT_MARGIN); + renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, y + 32, truncatedAuthor.c_str(), i != selectorIndex); + } } } diff --git a/src/activities/home/MyLibraryActivity.h b/src/activities/home/MyLibraryActivity.h index c6c52b68..39a27ed7 100644 --- a/src/activities/home/MyLibraryActivity.h +++ b/src/activities/home/MyLibraryActivity.h @@ -8,6 +8,7 @@ #include #include "../Activity.h" +#include "RecentBooksStore.h" class MyLibraryActivity final : public Activity { public: @@ -22,8 +23,7 @@ class MyLibraryActivity final : public Activity { bool updateRequired = false; // Recent tab state - std::vector bookTitles; // Display titles for each book - std::vector bookPaths; // Paths for each visible book (excludes missing) + std::vector recentBooks; // Files tab state (from FileSelectionActivity) std::string basepath = "/"; diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 89be3bc7..509f2eaf 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -85,7 +85,7 @@ void EpubReaderActivity::onEnter() { // Save current epub as last opened epub and add to recent books APP_STATE.openEpubPath = epub->getPath(); APP_STATE.saveToFile(); - RECENT_BOOKS.addBook(epub->getPath()); + RECENT_BOOKS.addBook(epub->getPath(), epub->getTitle(), epub->getAuthor()); // Trigger first update updateRequired = true; diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index 7df083a6..e4978221 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -60,7 +60,7 @@ void TxtReaderActivity::onEnter() { // Save current txt as last opened file and add to recent books APP_STATE.openEpubPath = txt->getPath(); APP_STATE.saveToFile(); - RECENT_BOOKS.addBook(txt->getPath()); + RECENT_BOOKS.addBook(txt->getPath(), "", ""); // Trigger first update updateRequired = true; diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 9761e27d..c97f2094 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -45,7 +45,7 @@ void XtcReaderActivity::onEnter() { // Save current XTC as last opened book and add to recent books APP_STATE.openEpubPath = xtc->getPath(); APP_STATE.saveToFile(); - RECENT_BOOKS.addBook(xtc->getPath()); + RECENT_BOOKS.addBook(xtc->getPath(), xtc->getTitle(), xtc->getAuthor()); // Trigger first update updateRequired = true; From da4d3b5ea5d0141567ea598a332c92c9b1655e3e Mon Sep 17 00:00:00 2001 From: Xuan-Son Nguyen Date: Tue, 27 Jan 2026 18:50:15 +0100 Subject: [PATCH 36/36] feat: add HalDisplay and HalGPIO (#522) ## Summary Extracted some changes from https://github.com/crosspoint-reader/crosspoint-reader/pull/500 to make reviewing easier This PR adds HAL (Hardware Abstraction Layer) for display and GPIO components, making it easier to write a stub or an emulated implementation of the hardware. SD card HAL will be added via another PR, because it's a bit more tricky. --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? **NO** --- lib/GfxRenderer/GfxRenderer.cpp | 62 ++++++------ lib/GfxRenderer/GfxRenderer.h | 12 +-- lib/hal/HalDisplay.cpp | 51 ++++++++++ lib/hal/HalDisplay.h | 52 ++++++++++ lib/hal/HalGPIO.cpp | 55 +++++++++++ lib/hal/HalGPIO.h | 61 ++++++++++++ src/MappedInputManager.cpp | 48 +++++---- src/MappedInputManager.h | 8 +- src/activities/boot_sleep/SleepActivity.cpp | 6 +- src/activities/reader/EpubReaderActivity.cpp | 4 +- src/activities/reader/TxtReaderActivity.cpp | 4 +- src/activities/reader/XtcReaderActivity.cpp | 4 +- .../util/FullScreenMessageActivity.h | 6 +- src/main.cpp | 97 ++++++------------- 14 files changed, 322 insertions(+), 148 deletions(-) create mode 100644 lib/hal/HalDisplay.cpp create mode 100644 lib/hal/HalDisplay.h create mode 100644 lib/hal/HalGPIO.cpp create mode 100644 lib/hal/HalGPIO.h diff --git a/lib/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 1dbe8ee6..fa1c61c6 100644 --- a/lib/GfxRenderer/GfxRenderer.cpp +++ b/lib/GfxRenderer/GfxRenderer.cpp @@ -10,19 +10,19 @@ void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int // Logical portrait (480x800) → panel (800x480) // Rotation: 90 degrees clockwise *rotatedX = y; - *rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - x; + *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - x; break; } case LandscapeClockwise: { // Logical landscape (800x480) rotated 180 degrees (swap top/bottom and left/right) - *rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - x; - *rotatedY = EInkDisplay::DISPLAY_HEIGHT - 1 - y; + *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - x; + *rotatedY = HalDisplay::DISPLAY_HEIGHT - 1 - y; break; } case PortraitInverted: { // Logical portrait (480x800) → panel (800x480) // Rotation: 90 degrees counter-clockwise - *rotatedX = EInkDisplay::DISPLAY_WIDTH - 1 - y; + *rotatedX = HalDisplay::DISPLAY_WIDTH - 1 - y; *rotatedY = x; break; } @@ -36,7 +36,7 @@ void GfxRenderer::rotateCoordinates(const int x, const int y, int* rotatedX, int } void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { - uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + uint8_t* frameBuffer = display.getFrameBuffer(); // Early return if no framebuffer is set if (!frameBuffer) { @@ -49,14 +49,13 @@ void GfxRenderer::drawPixel(const int x, const int y, const bool state) const { rotateCoordinates(x, y, &rotatedX, &rotatedY); // Bounds checking against physical panel dimensions - if (rotatedX < 0 || rotatedX >= EInkDisplay::DISPLAY_WIDTH || rotatedY < 0 || - rotatedY >= EInkDisplay::DISPLAY_HEIGHT) { + if (rotatedX < 0 || rotatedX >= HalDisplay::DISPLAY_WIDTH || rotatedY < 0 || rotatedY >= HalDisplay::DISPLAY_HEIGHT) { Serial.printf("[%lu] [GFX] !! Outside range (%d, %d) -> (%d, %d)\n", millis(), x, y, rotatedX, rotatedY); return; } // Calculate byte position and bit position - const uint16_t byteIndex = rotatedY * EInkDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8); + const uint16_t byteIndex = rotatedY * HalDisplay::DISPLAY_WIDTH_BYTES + (rotatedX / 8); const uint8_t bitPosition = 7 - (rotatedX % 8); // MSB first if (state) { @@ -164,7 +163,7 @@ void GfxRenderer::drawImage(const uint8_t bitmap[], const int x, const int y, co break; } // TODO: Rotate bits - einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); + display.drawImage(bitmap, rotatedX, rotatedY, width, height); } void GfxRenderer::drawBitmap(const Bitmap& bitmap, const int x, const int y, const int maxWidth, const int maxHeight, @@ -399,22 +398,20 @@ void GfxRenderer::fillPolygon(const int* xPoints, const int* yPoints, int numPoi free(nodeX); } -void GfxRenderer::clearScreen(const uint8_t color) const { einkDisplay.clearScreen(color); } +void GfxRenderer::clearScreen(const uint8_t color) const { display.clearScreen(color); } void GfxRenderer::invertScreen() const { - uint8_t* buffer = einkDisplay.getFrameBuffer(); + uint8_t* buffer = display.getFrameBuffer(); if (!buffer) { Serial.printf("[%lu] [GFX] !! No framebuffer in invertScreen\n", millis()); return; } - for (int i = 0; i < EInkDisplay::BUFFER_SIZE; i++) { + for (int i = 0; i < HalDisplay::BUFFER_SIZE; i++) { buffer[i] = ~buffer[i]; } } -void GfxRenderer::displayBuffer(const EInkDisplay::RefreshMode refreshMode) const { - einkDisplay.displayBuffer(refreshMode); -} +void GfxRenderer::displayBuffer(const HalDisplay::RefreshMode refreshMode) const { display.displayBuffer(refreshMode); } std::string GfxRenderer::truncatedText(const int fontId, const char* text, const int maxWidth, const EpdFontFamily::Style style) const { @@ -433,13 +430,13 @@ int GfxRenderer::getScreenWidth() const { case Portrait: case PortraitInverted: // 480px wide in portrait logical coordinates - return EInkDisplay::DISPLAY_HEIGHT; + return HalDisplay::DISPLAY_HEIGHT; case LandscapeClockwise: case LandscapeCounterClockwise: // 800px wide in landscape logical coordinates - return EInkDisplay::DISPLAY_WIDTH; + return HalDisplay::DISPLAY_WIDTH; } - return EInkDisplay::DISPLAY_HEIGHT; + return HalDisplay::DISPLAY_HEIGHT; } int GfxRenderer::getScreenHeight() const { @@ -447,13 +444,13 @@ int GfxRenderer::getScreenHeight() const { case Portrait: case PortraitInverted: // 800px tall in portrait logical coordinates - return EInkDisplay::DISPLAY_WIDTH; + return HalDisplay::DISPLAY_WIDTH; case LandscapeClockwise: case LandscapeCounterClockwise: // 480px tall in landscape logical coordinates - return EInkDisplay::DISPLAY_HEIGHT; + return HalDisplay::DISPLAY_HEIGHT; } - return EInkDisplay::DISPLAY_WIDTH; + return HalDisplay::DISPLAY_WIDTH; } int GfxRenderer::getSpaceWidth(const int fontId) const { @@ -653,17 +650,18 @@ void GfxRenderer::drawTextRotated90CW(const int fontId, const int x, const int y } } -uint8_t* GfxRenderer::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } +uint8_t* GfxRenderer::getFrameBuffer() const { return display.getFrameBuffer(); } -size_t GfxRenderer::getBufferSize() { return EInkDisplay::BUFFER_SIZE; } +size_t GfxRenderer::getBufferSize() { return HalDisplay::BUFFER_SIZE; } -void GfxRenderer::grayscaleRevert() const { einkDisplay.grayscaleRevert(); } +// unused +// void GfxRenderer::grayscaleRevert() const { display.grayscaleRevert(); } -void GfxRenderer::copyGrayscaleLsbBuffers() const { einkDisplay.copyGrayscaleLsbBuffers(einkDisplay.getFrameBuffer()); } +void GfxRenderer::copyGrayscaleLsbBuffers() const { display.copyGrayscaleLsbBuffers(display.getFrameBuffer()); } -void GfxRenderer::copyGrayscaleMsbBuffers() const { einkDisplay.copyGrayscaleMsbBuffers(einkDisplay.getFrameBuffer()); } +void GfxRenderer::copyGrayscaleMsbBuffers() const { display.copyGrayscaleMsbBuffers(display.getFrameBuffer()); } -void GfxRenderer::displayGrayBuffer() const { einkDisplay.displayGrayBuffer(); } +void GfxRenderer::displayGrayBuffer() const { display.displayGrayBuffer(); } void GfxRenderer::freeBwBufferChunks() { for (auto& bwBufferChunk : bwBufferChunks) { @@ -681,7 +679,7 @@ void GfxRenderer::freeBwBufferChunks() { * Returns true if buffer was stored successfully, false if allocation failed. */ bool GfxRenderer::storeBwBuffer() { - const uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + const uint8_t* frameBuffer = display.getFrameBuffer(); if (!frameBuffer) { Serial.printf("[%lu] [GFX] !! No framebuffer in storeBwBuffer\n", millis()); return false; @@ -736,7 +734,7 @@ void GfxRenderer::restoreBwBuffer() { return; } - uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + uint8_t* frameBuffer = display.getFrameBuffer(); if (!frameBuffer) { Serial.printf("[%lu] [GFX] !! No framebuffer in restoreBwBuffer\n", millis()); freeBwBufferChunks(); @@ -755,7 +753,7 @@ void GfxRenderer::restoreBwBuffer() { memcpy(frameBuffer + offset, bwBufferChunks[i], BW_BUFFER_CHUNK_SIZE); } - einkDisplay.cleanupGrayscaleBuffers(frameBuffer); + display.cleanupGrayscaleBuffers(frameBuffer); freeBwBufferChunks(); Serial.printf("[%lu] [GFX] Restored and freed BW buffer chunks\n", millis()); @@ -766,9 +764,9 @@ void GfxRenderer::restoreBwBuffer() { * Use this when BW buffer was re-rendered instead of stored/restored. */ void GfxRenderer::cleanupGrayscaleWithFrameBuffer() const { - uint8_t* frameBuffer = einkDisplay.getFrameBuffer(); + uint8_t* frameBuffer = display.getFrameBuffer(); if (frameBuffer) { - einkDisplay.cleanupGrayscaleBuffers(frameBuffer); + display.cleanupGrayscaleBuffers(frameBuffer); } } diff --git a/lib/GfxRenderer/GfxRenderer.h b/lib/GfxRenderer/GfxRenderer.h index b1fea69b..733975f4 100644 --- a/lib/GfxRenderer/GfxRenderer.h +++ b/lib/GfxRenderer/GfxRenderer.h @@ -1,7 +1,7 @@ #pragma once -#include #include +#include #include @@ -21,11 +21,11 @@ class GfxRenderer { private: static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory - static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE; - static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE, + static constexpr size_t BW_BUFFER_NUM_CHUNKS = HalDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE; + static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == HalDisplay::BUFFER_SIZE, "BW buffer chunking does not line up with display buffer size"); - EInkDisplay& einkDisplay; + HalDisplay& display; RenderMode renderMode; Orientation orientation; uint8_t* bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; @@ -36,7 +36,7 @@ class GfxRenderer { void rotateCoordinates(int x, int y, int* rotatedX, int* rotatedY) const; public: - explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {} + explicit GfxRenderer(HalDisplay& halDisplay) : display(halDisplay), renderMode(BW), orientation(Portrait) {} ~GfxRenderer() { freeBwBufferChunks(); } static constexpr int VIEWABLE_MARGIN_TOP = 9; @@ -54,7 +54,7 @@ class GfxRenderer { // Screen ops int getScreenWidth() const; int getScreenHeight() const; - void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const; + void displayBuffer(HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) const; // EXPERIMENTAL: Windowed update - display only a rectangular region void displayWindow(int x, int y, int width, int height) const; void invertScreen() const; diff --git a/lib/hal/HalDisplay.cpp b/lib/hal/HalDisplay.cpp new file mode 100644 index 00000000..6f69d7fc --- /dev/null +++ b/lib/hal/HalDisplay.cpp @@ -0,0 +1,51 @@ +#include +#include + +#define SD_SPI_MISO 7 + +HalDisplay::HalDisplay() : einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY) {} + +HalDisplay::~HalDisplay() {} + +void HalDisplay::begin() { einkDisplay.begin(); } + +void HalDisplay::clearScreen(uint8_t color) const { einkDisplay.clearScreen(color); } + +void HalDisplay::drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, + bool fromProgmem) const { + einkDisplay.drawImage(imageData, x, y, w, h, fromProgmem); +} + +EInkDisplay::RefreshMode convertRefreshMode(HalDisplay::RefreshMode mode) { + switch (mode) { + case HalDisplay::FULL_REFRESH: + return EInkDisplay::FULL_REFRESH; + case HalDisplay::HALF_REFRESH: + return EInkDisplay::HALF_REFRESH; + case HalDisplay::FAST_REFRESH: + default: + return EInkDisplay::FAST_REFRESH; + } +} + +void HalDisplay::displayBuffer(HalDisplay::RefreshMode mode) { einkDisplay.displayBuffer(convertRefreshMode(mode)); } + +void HalDisplay::refreshDisplay(HalDisplay::RefreshMode mode, bool turnOffScreen) { + einkDisplay.refreshDisplay(convertRefreshMode(mode), turnOffScreen); +} + +void HalDisplay::deepSleep() { einkDisplay.deepSleep(); } + +uint8_t* HalDisplay::getFrameBuffer() const { return einkDisplay.getFrameBuffer(); } + +void HalDisplay::copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer) { + einkDisplay.copyGrayscaleBuffers(lsbBuffer, msbBuffer); +} + +void HalDisplay::copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer) { einkDisplay.copyGrayscaleLsbBuffers(lsbBuffer); } + +void HalDisplay::copyGrayscaleMsbBuffers(const uint8_t* msbBuffer) { einkDisplay.copyGrayscaleMsbBuffers(msbBuffer); } + +void HalDisplay::cleanupGrayscaleBuffers(const uint8_t* bwBuffer) { einkDisplay.cleanupGrayscaleBuffers(bwBuffer); } + +void HalDisplay::displayGrayBuffer() { einkDisplay.displayGrayBuffer(); } diff --git a/lib/hal/HalDisplay.h b/lib/hal/HalDisplay.h new file mode 100644 index 00000000..6eb7156b --- /dev/null +++ b/lib/hal/HalDisplay.h @@ -0,0 +1,52 @@ +#pragma once +#include +#include + +class HalDisplay { + public: + // Constructor with pin configuration + HalDisplay(); + + // Destructor + ~HalDisplay(); + + // Refresh modes + enum RefreshMode { + FULL_REFRESH, // Full refresh with complete waveform + HALF_REFRESH, // Half refresh (1720ms) - balanced quality and speed + FAST_REFRESH // Fast refresh using custom LUT + }; + + // Initialize the display hardware and driver + void begin(); + + // Display dimensions + static constexpr uint16_t DISPLAY_WIDTH = EInkDisplay::DISPLAY_WIDTH; + static constexpr uint16_t DISPLAY_HEIGHT = EInkDisplay::DISPLAY_HEIGHT; + static constexpr uint16_t DISPLAY_WIDTH_BYTES = DISPLAY_WIDTH / 8; + static constexpr uint32_t BUFFER_SIZE = DISPLAY_WIDTH_BYTES * DISPLAY_HEIGHT; + + // Frame buffer operations + void clearScreen(uint8_t color = 0xFF) const; + void drawImage(const uint8_t* imageData, uint16_t x, uint16_t y, uint16_t w, uint16_t h, + bool fromProgmem = false) const; + + void displayBuffer(RefreshMode mode = RefreshMode::FAST_REFRESH); + void refreshDisplay(RefreshMode mode = RefreshMode::FAST_REFRESH, bool turnOffScreen = false); + + // Power management + void deepSleep(); + + // Access to frame buffer + uint8_t* getFrameBuffer() const; + + void copyGrayscaleBuffers(const uint8_t* lsbBuffer, const uint8_t* msbBuffer); + void copyGrayscaleLsbBuffers(const uint8_t* lsbBuffer); + void copyGrayscaleMsbBuffers(const uint8_t* msbBuffer); + void cleanupGrayscaleBuffers(const uint8_t* bwBuffer); + + void displayGrayBuffer(); + + private: + EInkDisplay einkDisplay; +}; diff --git a/lib/hal/HalGPIO.cpp b/lib/hal/HalGPIO.cpp new file mode 100644 index 00000000..803efba0 --- /dev/null +++ b/lib/hal/HalGPIO.cpp @@ -0,0 +1,55 @@ +#include +#include +#include + +void HalGPIO::begin() { + inputMgr.begin(); + SPI.begin(EPD_SCLK, SPI_MISO, EPD_MOSI, EPD_CS); + pinMode(BAT_GPIO0, INPUT); + pinMode(UART0_RXD, INPUT); +} + +void HalGPIO::update() { inputMgr.update(); } + +bool HalGPIO::isPressed(uint8_t buttonIndex) const { return inputMgr.isPressed(buttonIndex); } + +bool HalGPIO::wasPressed(uint8_t buttonIndex) const { return inputMgr.wasPressed(buttonIndex); } + +bool HalGPIO::wasAnyPressed() const { return inputMgr.wasAnyPressed(); } + +bool HalGPIO::wasReleased(uint8_t buttonIndex) const { return inputMgr.wasReleased(buttonIndex); } + +bool HalGPIO::wasAnyReleased() const { return inputMgr.wasAnyReleased(); } + +unsigned long HalGPIO::getHeldTime() const { return inputMgr.getHeldTime(); } + +void HalGPIO::startDeepSleep() { + esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); + // Ensure that the power button has been released to avoid immediately turning back on if you're holding it + while (inputMgr.isPressed(BTN_POWER)) { + delay(50); + inputMgr.update(); + } + // Enter Deep Sleep + esp_deep_sleep_start(); +} + +int HalGPIO::getBatteryPercentage() const { + static const BatteryMonitor battery = BatteryMonitor(BAT_GPIO0); + return battery.readPercentage(); +} + +bool HalGPIO::isUsbConnected() const { + // U0RXD/GPIO20 reads HIGH when USB is connected + return digitalRead(UART0_RXD) == HIGH; +} + +bool HalGPIO::isWakeupByPowerButton() const { + const auto wakeupCause = esp_sleep_get_wakeup_cause(); + const auto resetReason = esp_reset_reason(); + if (isUsbConnected()) { + return wakeupCause == ESP_SLEEP_WAKEUP_GPIO; + } else { + return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON); + } +} diff --git a/lib/hal/HalGPIO.h b/lib/hal/HalGPIO.h new file mode 100644 index 00000000..11ffb22e --- /dev/null +++ b/lib/hal/HalGPIO.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) +#define EPD_SCLK 8 // SPI Clock +#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) +#define EPD_CS 21 // Chip Select +#define EPD_DC 4 // Data/Command +#define EPD_RST 5 // Reset +#define EPD_BUSY 6 // Busy + +#define SPI_MISO 7 // SPI MISO, shared between SD card and display (Master In Slave Out) + +#define BAT_GPIO0 0 // Battery voltage + +#define UART0_RXD 20 // Used for USB connection detection + +class HalGPIO { +#if CROSSPOINT_EMULATED == 0 + InputManager inputMgr; +#endif + + public: + HalGPIO() = default; + + // Start button GPIO and setup SPI for screen and SD card + void begin(); + + // Button input methods + void update(); + bool isPressed(uint8_t buttonIndex) const; + bool wasPressed(uint8_t buttonIndex) const; + bool wasAnyPressed() const; + bool wasReleased(uint8_t buttonIndex) const; + bool wasAnyReleased() const; + unsigned long getHeldTime() const; + + // Setup wake up GPIO and enter deep sleep + void startDeepSleep(); + + // Get battery percentage (range 0-100) + int getBatteryPercentage() const; + + // Check if USB is connected + bool isUsbConnected() const; + + // Check if wakeup was caused by power button press + bool isWakeupByPowerButton() const; + + // Button indices + static constexpr uint8_t BTN_BACK = 0; + static constexpr uint8_t BTN_CONFIRM = 1; + static constexpr uint8_t BTN_LEFT = 2; + static constexpr uint8_t BTN_RIGHT = 3; + static constexpr uint8_t BTN_UP = 4; + static constexpr uint8_t BTN_DOWN = 5; + static constexpr uint8_t BTN_POWER = 6; +}; diff --git a/src/MappedInputManager.cpp b/src/MappedInputManager.cpp index 25095be7..e5423724 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -19,20 +19,20 @@ struct SideLayoutMap { // Order matches CrossPointSettings::FRONT_BUTTON_LAYOUT. constexpr FrontLayoutMap kFrontLayouts[] = { - {InputManager::BTN_BACK, InputManager::BTN_CONFIRM, InputManager::BTN_LEFT, InputManager::BTN_RIGHT}, - {InputManager::BTN_LEFT, InputManager::BTN_RIGHT, InputManager::BTN_BACK, InputManager::BTN_CONFIRM}, - {InputManager::BTN_CONFIRM, InputManager::BTN_LEFT, InputManager::BTN_BACK, InputManager::BTN_RIGHT}, - {InputManager::BTN_BACK, InputManager::BTN_CONFIRM, InputManager::BTN_RIGHT, InputManager::BTN_LEFT}, + {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT}, + {HalGPIO::BTN_LEFT, HalGPIO::BTN_RIGHT, HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM}, + {HalGPIO::BTN_CONFIRM, HalGPIO::BTN_LEFT, HalGPIO::BTN_BACK, HalGPIO::BTN_RIGHT}, + {HalGPIO::BTN_BACK, HalGPIO::BTN_CONFIRM, HalGPIO::BTN_RIGHT, HalGPIO::BTN_LEFT}, }; // Order matches CrossPointSettings::SIDE_BUTTON_LAYOUT. constexpr SideLayoutMap kSideLayouts[] = { - {InputManager::BTN_UP, InputManager::BTN_DOWN}, - {InputManager::BTN_DOWN, InputManager::BTN_UP}, + {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN}, + {HalGPIO::BTN_DOWN, HalGPIO::BTN_UP}, }; } // namespace -bool MappedInputManager::mapButton(const Button button, bool (InputManager::*fn)(uint8_t) const) const { +bool MappedInputManager::mapButton(const Button button, bool (HalGPIO::*fn)(uint8_t) const) const { const auto frontLayout = static_cast(SETTINGS.frontButtonLayout); const auto sideLayout = static_cast(SETTINGS.sideButtonLayout); const auto& front = kFrontLayouts[frontLayout]; @@ -40,41 +40,39 @@ bool MappedInputManager::mapButton(const Button button, bool (InputManager::*fn) switch (button) { case Button::Back: - return (inputManager.*fn)(front.back); + return (gpio.*fn)(front.back); case Button::Confirm: - return (inputManager.*fn)(front.confirm); + return (gpio.*fn)(front.confirm); case Button::Left: - return (inputManager.*fn)(front.left); + return (gpio.*fn)(front.left); case Button::Right: - return (inputManager.*fn)(front.right); + return (gpio.*fn)(front.right); case Button::Up: - return (inputManager.*fn)(InputManager::BTN_UP); + return (gpio.*fn)(HalGPIO::BTN_UP); case Button::Down: - return (inputManager.*fn)(InputManager::BTN_DOWN); + return (gpio.*fn)(HalGPIO::BTN_DOWN); case Button::Power: - return (inputManager.*fn)(InputManager::BTN_POWER); + return (gpio.*fn)(HalGPIO::BTN_POWER); case Button::PageBack: - return (inputManager.*fn)(side.pageBack); + return (gpio.*fn)(side.pageBack); case Button::PageForward: - return (inputManager.*fn)(side.pageForward); + return (gpio.*fn)(side.pageForward); } return false; } -bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &InputManager::wasPressed); } +bool MappedInputManager::wasPressed(const Button button) const { return mapButton(button, &HalGPIO::wasPressed); } -bool MappedInputManager::wasReleased(const Button button) const { - return mapButton(button, &InputManager::wasReleased); -} +bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); } -bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &InputManager::isPressed); } +bool MappedInputManager::isPressed(const Button button) const { return mapButton(button, &HalGPIO::isPressed); } -bool MappedInputManager::wasAnyPressed() const { return inputManager.wasAnyPressed(); } +bool MappedInputManager::wasAnyPressed() const { return gpio.wasAnyPressed(); } -bool MappedInputManager::wasAnyReleased() const { return inputManager.wasAnyReleased(); } +bool MappedInputManager::wasAnyReleased() const { return gpio.wasAnyReleased(); } -unsigned long MappedInputManager::getHeldTime() const { return inputManager.getHeldTime(); } +unsigned long MappedInputManager::getHeldTime() const { return gpio.getHeldTime(); } MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const { @@ -91,4 +89,4 @@ MappedInputManager::Labels MappedInputManager::mapLabels(const char* back, const default: return {back, confirm, previous, next}; } -} +} \ No newline at end of file diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index bee7cd4b..f507a928 100644 --- a/src/MappedInputManager.h +++ b/src/MappedInputManager.h @@ -1,6 +1,6 @@ #pragma once -#include +#include class MappedInputManager { public: @@ -13,7 +13,7 @@ class MappedInputManager { const char* btn4; }; - explicit MappedInputManager(InputManager& inputManager) : inputManager(inputManager) {} + explicit MappedInputManager(HalGPIO& gpio) : gpio(gpio) {} bool wasPressed(Button button) const; bool wasReleased(Button button) const; @@ -24,7 +24,7 @@ class MappedInputManager { Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const; private: - InputManager& inputManager; + HalGPIO& gpio; - bool mapButton(Button button, bool (InputManager::*fn)(uint8_t) const) const; + bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const; }; diff --git a/src/activities/boot_sleep/SleepActivity.cpp b/src/activities/boot_sleep/SleepActivity.cpp index 95fe742f..aace2095 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -133,7 +133,7 @@ void SleepActivity::renderDefaultSleepScreen() const { renderer.invertScreen(); } - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { @@ -189,7 +189,7 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { renderer.invertScreen(); } - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); if (hasGreyscale) { bitmap.rewindToData(); @@ -280,5 +280,5 @@ void SleepActivity::renderCoverSleepScreen() const { void SleepActivity::renderBlankSleepScreen() const { renderer.clearScreen(); - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 509f2eaf..58668c68 100644 --- a/src/activities/reader/EpubReaderActivity.cpp +++ b/src/activities/reader/EpubReaderActivity.cpp @@ -345,7 +345,7 @@ void EpubReaderActivity::renderScreen() { auto progressCallback = [this, barX, barY, barWidth, barHeight](int progress) { const int fillWidth = (barWidth - 2) * progress / 100; renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); - renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); }; if (!section->createSectionFile(SETTINGS.getReaderFontId(), SETTINGS.getReaderLineCompression(), @@ -428,7 +428,7 @@ void EpubReaderActivity::renderContents(std::unique_ptr page, const int or page->render(renderer, SETTINGS.getReaderFontId(), orientedMarginLeft, orientedMarginTop); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { renderer.displayBuffer(); diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index e4978221..e9303de3 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -256,7 +256,7 @@ void TxtReaderActivity::buildPageIndex() { // Fill progress bar const int fillWidth = (barWidth - 2) * progressPercent / 100; renderer.fillRect(barX + 1, barY + 1, fillWidth, barHeight - 2, true); - renderer.displayBuffer(EInkDisplay::FAST_REFRESH); + renderer.displayBuffer(HalDisplay::FAST_REFRESH); } // Yield to other tasks periodically @@ -484,7 +484,7 @@ void TxtReaderActivity::renderPage() { renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { renderer.displayBuffer(); diff --git a/src/activities/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index c97f2094..f579abcd 100644 --- a/src/activities/reader/XtcReaderActivity.cpp +++ b/src/activities/reader/XtcReaderActivity.cpp @@ -276,7 +276,7 @@ void XtcReaderActivity::renderPage() { // Display BW with conditional refresh based on pagesUntilFullRefresh if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { renderer.displayBuffer(); @@ -356,7 +356,7 @@ void XtcReaderActivity::renderPage() { // Display with appropriate refresh if (pagesUntilFullRefresh <= 1) { - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); pagesUntilFullRefresh = SETTINGS.getRefreshFrequency(); } else { renderer.displayBuffer(); diff --git a/src/activities/util/FullScreenMessageActivity.h b/src/activities/util/FullScreenMessageActivity.h index 3e975c91..93909503 100644 --- a/src/activities/util/FullScreenMessageActivity.h +++ b/src/activities/util/FullScreenMessageActivity.h @@ -1,6 +1,6 @@ #pragma once -#include #include +#include #include #include @@ -10,12 +10,12 @@ class FullScreenMessageActivity final : public Activity { std::string text; EpdFontFamily::Style style; - EInkDisplay::RefreshMode refreshMode; + HalDisplay::RefreshMode refreshMode; public: explicit FullScreenMessageActivity(GfxRenderer& renderer, MappedInputManager& mappedInput, std::string text, const EpdFontFamily::Style style = EpdFontFamily::REGULAR, - const EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) + const HalDisplay::RefreshMode refreshMode = HalDisplay::FAST_REFRESH) : Activity("FullScreenMessage", renderer, mappedInput), text(std::move(text)), style(style), diff --git a/src/main.cpp b/src/main.cpp index 8a081fd8..2308f0a2 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,8 @@ #include -#include #include #include -#include +#include +#include #include #include #include @@ -26,23 +26,10 @@ #include "activities/util/FullScreenMessageActivity.h" #include "fontIds.h" -#define SPI_FQ 40000000 -// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults) -#define EPD_SCLK 8 // SPI Clock -#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In) -#define EPD_CS 21 // Chip Select -#define EPD_DC 4 // Data/Command -#define EPD_RST 5 // Reset -#define EPD_BUSY 6 // Busy - -#define UART0_RXD 20 // Used for USB connection detection - -#define SD_SPI_MISO 7 - -EInkDisplay einkDisplay(EPD_SCLK, EPD_MOSI, EPD_CS, EPD_DC, EPD_RST, EPD_BUSY); -InputManager inputManager; -MappedInputManager mappedInputManager(inputManager); -GfxRenderer renderer(einkDisplay); +HalDisplay display; +HalGPIO gpio; +MappedInputManager mappedInputManager(gpio); +GfxRenderer renderer(display); Activity* currentActivity; // Fonts @@ -170,21 +157,20 @@ void verifyPowerButtonDuration() { const uint16_t calibratedPressDuration = (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1; - inputManager.update(); - // Verify the user has actually pressed + gpio.update(); // Needed because inputManager.isPressed() may take up to ~500ms to return the correct state - while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) { + while (!gpio.isPressed(HalGPIO::BTN_POWER) && millis() - start < 1000) { delay(10); // only wait 10ms each iteration to not delay too much in case of short configured duration. - inputManager.update(); + gpio.update(); } t2 = millis(); - if (inputManager.isPressed(InputManager::BTN_POWER)) { + if (gpio.isPressed(HalGPIO::BTN_POWER)) { do { delay(10); - inputManager.update(); - } while (inputManager.isPressed(InputManager::BTN_POWER) && inputManager.getHeldTime() < calibratedPressDuration); - abort = inputManager.getHeldTime() < calibratedPressDuration; + gpio.update(); + } while (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() < calibratedPressDuration); + abort = gpio.getHeldTime() < calibratedPressDuration; } else { abort = true; } @@ -192,16 +178,15 @@ void verifyPowerButtonDuration() { if (abort) { // Button released too early. Returning to sleep. // IMPORTANT: Re-arm the wakeup trigger before sleeping again - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); - esp_deep_sleep_start(); + gpio.startDeepSleep(); } } void waitForPowerRelease() { - inputManager.update(); - while (inputManager.isPressed(InputManager::BTN_POWER)) { + gpio.update(); + while (gpio.isPressed(HalGPIO::BTN_POWER)) { delay(50); - inputManager.update(); + gpio.update(); } } @@ -210,14 +195,11 @@ void enterDeepSleep() { exitActivity(); enterNewActivity(new SleepActivity(renderer, mappedInputManager)); - einkDisplay.deepSleep(); + display.deepSleep(); Serial.printf("[%lu] [ ] Power button press calibration value: %lu ms\n", millis(), t2 - t1); Serial.printf("[%lu] [ ] Entering deep sleep.\n", millis()); - esp_deep_sleep_enable_gpio_wakeup(1ULL << InputManager::POWER_BUTTON_PIN, ESP_GPIO_WAKEUP_GPIO_LOW); - // Ensure that the power button has been released to avoid immediately turning back on if you're holding it - waitForPowerRelease(); - // Enter Deep Sleep - esp_deep_sleep_start(); + + gpio.startDeepSleep(); } void onGoHome(); @@ -261,7 +243,7 @@ void onGoHome() { } void setupDisplayAndFonts() { - einkDisplay.begin(); + display.begin(); Serial.printf("[%lu] [ ] Display initialized\n", millis()); renderer.insertFont(BOOKERLY_14_FONT_ID, bookerly14FontFamily); #ifndef OMIT_FONTS @@ -284,27 +266,13 @@ void setupDisplayAndFonts() { Serial.printf("[%lu] [ ] Fonts setup\n", millis()); } -bool isUsbConnected() { - // U0RXD/GPIO20 reads HIGH when USB is connected - return digitalRead(UART0_RXD) == HIGH; -} - -bool isWakeupByPowerButton() { - const auto wakeupCause = esp_sleep_get_wakeup_cause(); - const auto resetReason = esp_reset_reason(); - if (isUsbConnected()) { - return wakeupCause == ESP_SLEEP_WAKEUP_GPIO; - } else { - return (wakeupCause == ESP_SLEEP_WAKEUP_UNDEFINED) && (resetReason == ESP_RST_POWERON); - } -} - void setup() { t1 = millis(); + gpio.begin(); + // Only start serial if USB connected - pinMode(UART0_RXD, INPUT); - if (isUsbConnected()) { + if (gpio.isUsbConnected()) { Serial.begin(115200); // Wait up to 3 seconds for Serial to be ready to catch early logs unsigned long start = millis(); @@ -313,13 +281,6 @@ void setup() { } } - inputManager.begin(); - // Initialize pins - pinMode(BAT_GPIO0, INPUT); - - // Initialize SPI with custom pins - SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS); - // SD Card Initialization // We need 6 open files concurrently when parsing a new chapter if (!SdMan.begin()) { @@ -333,7 +294,7 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); - if (isWakeupByPowerButton()) { + if (gpio.isWakeupByPowerButton()) { // For normal wakeups, verify power button press duration Serial.printf("[%lu] [ ] Verifying power button press duration\n", millis()); verifyPowerButtonDuration(); @@ -370,7 +331,7 @@ void loop() { const unsigned long loopStartTime = millis(); static unsigned long lastMemPrint = 0; - inputManager.update(); + gpio.update(); if (Serial && millis() - lastMemPrint >= 10000) { Serial.printf("[%lu] [MEM] Free: %d bytes, Total: %d bytes, Min Free: %d bytes\n", millis(), ESP.getFreeHeap(), @@ -380,8 +341,7 @@ void loop() { // Check for any user activity (button press or release) or active background work static unsigned long lastActivityTime = millis(); - if (inputManager.wasAnyPressed() || inputManager.wasAnyReleased() || - (currentActivity && currentActivity->preventAutoSleep())) { + if (gpio.wasAnyPressed() || gpio.wasAnyReleased() || (currentActivity && currentActivity->preventAutoSleep())) { lastActivityTime = millis(); // Reset inactivity timer } @@ -393,8 +353,7 @@ void loop() { return; } - if (inputManager.isPressed(InputManager::BTN_POWER) && - inputManager.getHeldTime() > SETTINGS.getPowerButtonDuration()) { + if (gpio.isPressed(HalGPIO::BTN_POWER) && gpio.getHeldTime() > SETTINGS.getPowerButtonDuration()) { enterDeepSleep(); // This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start return;