diff --git a/.gitignore b/.gitignore index 0cc30a26..ec281eb9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,8 @@ .vscode lib/EpdFont/fontsrc *.generated.h +.vs build -**/__pycache__/ \ No newline at end of file +**/__pycache__/ +/compile_commands.json +/.cache 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 0c852691..06973c92 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): @@ -60,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: @@ -72,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 @@ -95,6 +132,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 @@ -110,7 +148,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 @@ -156,6 +194,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 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 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)) diff --git a/lib/Epub/Epub.cpp b/lib/Epub/Epub.cpp index 78607573..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()); @@ -359,7 +368,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 +391,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 +410,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/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/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; 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/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp b/lib/Epub/Epub/parsers/ChapterHtmlSlimParser.cpp index 1d7e2ab3..298c4ec6 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) { @@ -67,38 +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) { - 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()); - - } 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)) { @@ -123,21 +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)) { - if (strcmp(name, "br") == 0) { - 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); - } - } - } 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->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()); + self->depth += 1; + return; + } + + 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; } @@ -149,22 +192,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 +218,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]; @@ -216,21 +246,11 @@ 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) { - 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); 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/GfxRenderer/GfxRenderer.cpp b/lib/GfxRenderer/GfxRenderer.cpp index 08420bf9..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) { @@ -145,11 +144,26 @@ 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); - einkDisplay.drawImage(bitmap, rotatedX, rotatedY, width, height); + // 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 + 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, @@ -384,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 { @@ -418,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 { @@ -432,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 { @@ -638,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) { @@ -666,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; @@ -721,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(); @@ -740,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()); @@ -751,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/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/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); } 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/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/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); 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/platformio.ini b/platformio.ini index ef27ffd5..e8574470 100644 --- a/platformio.ini +++ b/platformio.ini @@ -2,7 +2,7 @@ default_envs = default [crosspoint] -version = 0.14.0 +version = 0.16.0 [base] platform = espressif32 @ 6.12.0 diff --git a/src/CrossPointSettings.cpp b/src/CrossPointSettings.cpp index 87ede84b..ba558057 100644 --- a/src/CrossPointSettings.cpp +++ b/src/CrossPointSettings.cpp @@ -11,10 +11,18 @@ // 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 -constexpr uint8_t SETTINGS_COUNT = 21; +constexpr uint8_t SETTINGS_COUNT = 24; constexpr char SETTINGS_FILE[] = "/.crosspoint/settings.bin"; } // namespace @@ -50,6 +58,9 @@ bool CrossPointSettings::saveToFile() const { serialization::writePod(outputFile, longPressChapterSkip); serialization::writePod(outputFile, hyphenationEnabled); serialization::writeString(outputFile, std::string(selectedSleepBmp)); + serialization::writeString(outputFile, std::string(opdsUsername)); + serialization::writeString(outputFile, std::string(opdsPassword)); + serialization::writePod(outputFile, sleepScreenCoverFilter); outputFile.close(); Serial.printf("[%lu] [CPS] Settings saved to file\n", millis()); @@ -76,35 +87,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; @@ -115,7 +126,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; @@ -128,6 +139,22 @@ bool CrossPointSettings::loadFromFile() { selectedSleepBmp[sizeof(selectedSleepBmp) - 1] = '\0'; } if (++settingsRead >= fileSettingsCount) break; + { + 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; + readAndValidate(inputFile, sleepScreenCoverFilter, SLEEP_SCREEN_COVER_FILTER_COUNT); + if (++settingsRead >= fileSettingsCount) break; } while (false); inputFile.close(); diff --git a/src/CrossPointSettings.h b/src/CrossPointSettings.h index 21509197..7b66bb54 100644 --- a/src/CrossPointSettings.h +++ b/src/CrossPointSettings.h @@ -15,53 +15,94 @@ 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 }; + 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 { NONE = 0, NO_PROGRESS = 1, FULL = 2 }; + 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) - 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 // 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, + 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; // 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 @@ -90,6 +131,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/MappedInputManager.cpp b/src/MappedInputManager.cpp index 1b038446..e5423724 100644 --- a/src/MappedInputManager.cpp +++ b/src/MappedInputManager.cpp @@ -2,87 +2,77 @@ #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[] = { + {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[] = { + {HalGPIO::BTN_UP, HalGPIO::BTN_DOWN}, + {HalGPIO::BTN_DOWN, HalGPIO::BTN_UP}, +}; +} // namespace + +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]; + 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: - default: - return InputManager::BTN_BACK; - } + return (gpio.*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: - default: - return InputManager::BTN_CONFIRM; - } + return (gpio.*fn)(front.confirm); case Button::Left: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - return InputManager::BTN_BACK; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - default: - return InputManager::BTN_LEFT; - } + return (gpio.*fn)(front.left); case Button::Right: - switch (frontLayout) { - case CrossPointSettings::LEFT_RIGHT_BACK_CONFIRM: - return InputManager::BTN_CONFIRM; - case CrossPointSettings::BACK_CONFIRM_LEFT_RIGHT: - case CrossPointSettings::LEFT_BACK_CONFIRM_RIGHT: - default: - return InputManager::BTN_RIGHT; - } + return (gpio.*fn)(front.right); case Button::Up: - return InputManager::BTN_UP; + return (gpio.*fn)(HalGPIO::BTN_UP); case Button::Down: - return InputManager::BTN_DOWN; + return (gpio.*fn)(HalGPIO::BTN_DOWN); case Button::Power: - return InputManager::BTN_POWER; + return (gpio.*fn)(HalGPIO::BTN_POWER); case Button::PageBack: - switch (sideLayout) { - case CrossPointSettings::NEXT_PREV: - return InputManager::BTN_DOWN; - case CrossPointSettings::PREV_NEXT: - default: - return InputManager::BTN_UP; - } + return (gpio.*fn)(side.pageBack); case Button::PageForward: - switch (sideLayout) { - case CrossPointSettings::NEXT_PREV: - return InputManager::BTN_UP; - case CrossPointSettings::PREV_NEXT: - default: - return InputManager::BTN_DOWN; - } + return (gpio.*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, &HalGPIO::wasPressed); } -bool MappedInputManager::wasReleased(const Button button) const { return inputManager.wasReleased(mapButton(button)); } +bool MappedInputManager::wasReleased(const Button button) const { return mapButton(button, &HalGPIO::wasReleased); } -bool MappedInputManager::isPressed(const Button button) const { return inputManager.isPressed(mapButton(button)); } +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 { @@ -93,8 +83,10 @@ 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}; } -} +} \ No newline at end of file diff --git a/src/MappedInputManager.h b/src/MappedInputManager.h index 62065fe9..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,6 +24,7 @@ class MappedInputManager { Labels mapLabels(const char* back, const char* confirm, const char* previous, const char* next) const; private: - InputManager& inputManager; - decltype(InputManager::BTN_BACK) mapButton(Button button) const; + HalGPIO& gpio; + + bool mapButton(Button button, bool (HalGPIO::*fn)(uint8_t) const) const; }; 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/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/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 c9af0794..de6b939f 100644 --- a/src/activities/boot_sleep/SleepActivity.cpp +++ b/src/activities/boot_sleep/SleepActivity.cpp @@ -140,7 +140,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"); @@ -149,7 +149,7 @@ void SleepActivity::renderDefaultSleepScreen() const { renderer.invertScreen(); } - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); + renderer.displayBuffer(HalDisplay::HALF_REFRESH); } void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { @@ -195,10 +195,19 @@ void SleepActivity::renderBitmapSleepScreen(const Bitmap& bitmap) const { Serial.printf("[%lu] [SLP] drawing to %d x %d\n", millis(), x, y); renderer.clearScreen(); - renderer.drawBitmap(bitmap, x, y, pageWidth, pageHeight, cropX, cropY); - renderer.displayBuffer(EInkDisplay::HALF_REFRESH); - if (bitmap.hasGreyscale()) { + 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(HalDisplay::HALF_REFRESH); + + if (hasGreyscale) { bitmap.rewindToData(); renderer.clearScreen(0x00); renderer.setRenderMode(GfxRenderer::GRAYSCALE_LSB); @@ -276,6 +285,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; } @@ -286,5 +296,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/browser/OpdsBookBrowserActivity.cpp b/src/activities/browser/OpdsBookBrowserActivity.cpp index 677f9cac..2bde74de 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" @@ -17,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) { @@ -32,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..."; @@ -171,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()); @@ -265,23 +265,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/activities/home/HomeActivity.cpp b/src/activities/home/HomeActivity.cpp index eb11ba95..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(); @@ -502,8 +505,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/home/MyLibraryActivity.cpp b/src/activities/home/MyLibraryActivity.cpp index 9e6f3734..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); } } @@ -120,7 +103,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); } } @@ -175,8 +159,6 @@ void MyLibraryActivity::onExit() { vSemaphoreDelete(renderingMutex); renderingMutex = nullptr; - bookTitles.clear(); - bookPaths.clear(); files.clear(); } @@ -206,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 @@ -332,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"); @@ -342,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/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/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); } diff --git a/src/activities/reader/EpubReaderActivity.cpp b/src/activities/reader/EpubReaderActivity.cpp index 6ff39c5e..58668c68 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) { @@ -56,12 +58,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. @@ -78,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; @@ -163,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; } @@ -188,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; @@ -201,7 +215,7 @@ void EpubReaderActivity::loop() { return; } - if (prevReleased) { + if (prevTriggered) { if (section->currentPage > 0) { section->currentPage--; } else { @@ -270,7 +284,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; @@ -322,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(), @@ -341,6 +364,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 +410,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(); } } @@ -392,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(); @@ -428,11 +464,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; @@ -441,19 +483,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 %.1f%%", 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/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; 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/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) { diff --git a/src/activities/reader/TxtReaderActivity.cpp b/src/activities/reader/TxtReaderActivity.cpp index db725320..e9303de3 100644 --- a/src/activities/reader/TxtReaderActivity.cpp +++ b/src/activities/reader/TxtReaderActivity.cpp @@ -8,12 +8,14 @@ #include "CrossPointSettings.h" #include "CrossPointState.h" #include "MappedInputManager.h" +#include "RecentBooksStore.h" #include "ScreenComponents.h" #include "fontIds.h" 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 @@ -55,9 +57,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; @@ -107,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; } @@ -156,7 +166,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; @@ -237,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 @@ -465,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(); @@ -497,27 +516,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/reader/XtcReaderActivity.cpp b/src/activities/reader/XtcReaderActivity.cpp index 0a58d7b3..f579abcd 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; @@ -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" @@ -269,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(); @@ -349,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/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 a69349bf..8d6fee09 100644 --- a/src/activities/settings/CategorySettingsActivity.cpp +++ b/src/activities/settings/CategorySettingsActivity.cpp @@ -124,7 +124,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/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) { 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/activities/settings/SettingsActivity.cpp b/src/activities/settings/SettingsActivity.cpp index 89e681cd..ff4c4f26 100644 --- a/src/activities/settings/SettingsActivity.cpp +++ b/src/activities/settings/SettingsActivity.cpp @@ -18,13 +18,16 @@ const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"}; namespace { -constexpr int displaySettingsCount = 6; +constexpr int displaySettingsCount = 7; const SettingInfo displaySettings[displaySettingsCount] = { // Should match with SLEEP_SCREEN_MODE SettingInfo::Action("Sleep Screen"), SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), SettingInfo::Action("Select Sleep BMP"), - SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), + 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"}), SettingInfo::Action("Refresh Frequency")}; @@ -44,8 +47,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), @@ -53,8 +57,9 @@ const SettingInfo controlsSettings[controlsSettingsCount] = { constexpr int systemSettingsCount = 5; const SettingInfo systemSettings[systemSettingsCount] = { - SettingInfo::Action("Time to Sleep"), - SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"), + SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, + {"1 min", "5 min", "10 min", "15 min", "30 min"}), + SettingInfo::Action("KOReader Sync"), SettingInfo::Action("OPDS Browser"), SettingInfo::Action("Clear Cache"), SettingInfo::Action("Check for updates")}; } // namespace 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/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; diff --git a/src/main.cpp b/src/main.cpp index c0222e0d..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 @@ -151,8 +138,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; @@ -163,20 +157,20 @@ void verifyWakeupLongPress() { const uint16_t calibratedPressDuration = (calibration < SETTINGS.getPowerButtonDuration()) ? SETTINGS.getPowerButtonDuration() - calibration : 1; - inputManager.update(); - // Verify the user has actually pressed - while (!inputManager.isPressed(InputManager::BTN_POWER) && millis() - start < 1000) { + gpio.update(); + // Needed because inputManager.isPressed() may take up to ~500ms to return the correct state + 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; } @@ -184,16 +178,15 @@ void verifyWakeupLongPress() { 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(); } } @@ -202,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(); @@ -253,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 @@ -276,24 +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 isWakeupAfterFlashing() { - 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); -} - 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(); @@ -302,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()) { @@ -322,9 +294,10 @@ void setup() { SETTINGS.loadFromFile(); KOREADER_STORE.loadFromFile(); - if (!isWakeupAfterFlashing()) { - // For normal wakeups (not immediately after flashing), verify long press - verifyWakeupLongPress(); + if (gpio.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 @@ -358,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(), @@ -368,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 } @@ -381,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; 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 c4de3a05..b7718c2d 100644 --- a/src/network/HttpDownloader.cpp +++ b/src/network/HttpDownloader.cpp @@ -2,14 +2,18 @@ #include #include +#include #include #include +#include +#include #include +#include "CrossPointSettings.h" #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)) { @@ -27,6 +31,13 @@ bool HttpDownloader::fetchUrl(const std::string& url, std::string& 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); @@ -34,10 +45,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; } @@ -61,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); 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 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(); }; 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