feat: Add CJK UI font support and complete I18N implementation

## Major Features

### 1. CJK UI Font System
- Implemented external font loading system for CJK characters
- Added Source Han Sans (思源黑体) as base font for UI rendering
- Support for multiple font sizes (20pt, 22pt, 24pt)
- Font selection UI for both reader and UI fonts
- Automatic fallback to built-in fonts when external fonts unavailable
- External UI font now renders ALL characters (including ASCII) for consistent style
- Proportional spacing for external fonts (variable width per character)

### 2. Complete I18N Implementation
- Added comprehensive internationalization system
- Support for English, Chinese Simplified, and Japanese
- Translated all UI strings across the entire application
- Language selection UI in settings with native language names
  - English displayed as "English"
  - Chinese displayed as "简体中文"
  - Japanese displayed as "日本語"
- Dynamic language switching without restart

### 3. Bug Fixes

#### Rendering Race Conditions
- Fixed race condition where parent and child Activity rendering tasks run simultaneously
- Added 500ms delay in child Activity displayTaskLoop() to wait for parent rendering completion
- Unified displayTaskLoop() logic: `if (updateRequired && !subActivity)`
- Prevents duplicate RED RAM writes and incomplete screen refreshes

**Affected Activities:**
- CategorySettingsActivity: Unified displayTaskLoop check logic
- KOReaderSettingsActivity: Added 500ms delay before first render
- CalibreSettingsActivity: Added 500ms delay before first render
- FontSelectActivity: Added 500ms delay before first render
- ClearCacheActivity: Added 500ms delay and subActivity check
- LanguageSelectActivity: Added 500ms delay in displayTaskLoop (not onEnter)

#### Button Response Issues
- Fixed CrossPointWebServer exit button requiring long press
- Added MappedInputManager::update() method
- Call update() before wasPressed() in tight HTTP processing loop
- Button presses during loop are now properly detected

#### ClearCache Crash
- Fixed FreeRTOS mutex deadlock when exiting ClearCache activity
- Added isExiting flag to prevent operations during exit
- Added clearCacheTaskHandle tracking
- Wait for clearCache task completion before deleting mutex

#### External UI Font Rendering
- Fixed ASCII characters not using external UI font (was using built-in EPD font)
- Fixed character spacing too wide (now uses proportional spacing via getGlyphMetrics)

## Technical Details

**Files Added:**
- lib/ExternalFont/: External font loading system
- lib/I18n/: Internationalization system
- lib/GfxRenderer/cjk_ui_font*.h: Pre-rendered CJK font data
- scripts/generate_cjk_ui_font.py: Font generation script
- src/activities/settings/FontSelectActivity.*: Font selection UI
- src/activities/settings/LanguageSelectActivity.*: Language selection UI
- docs/cjk-fonts.md: CJK font documentation
- docs/i18n.md: I18N documentation

**Files Modified:**
- lib/GfxRenderer/: Added CJK font rendering support with proportional spacing
- src/activities/: I18N integration across all activities
- src/MappedInputManager.*: Added update() method
- src/CrossPointSettings.cpp: Added language and font settings

**Memory Usage:**
- Flash: 94.7% (6204434 bytes / 6553600 bytes)
- RAM: 66.4% (217556 bytes / 327680 bytes)

## Testing Notes

All rendering race conditions and button response issues have been fixed and tested.
ClearCache no longer crashes when exiting.
File transfer page now responds to short press on exit button.
External UI font now renders all characters with proper proportional spacing.
Language selection page displays language names in their native scripts.

Co-authored-by: Claude (Anthropic AI Assistant)
This commit is contained in:
aber0724 2026-01-23 16:54:56 +09:00
parent 3ce11f14ce
commit 895fe470c6
48 changed files with 14354 additions and 546 deletions

6
.gitignore vendored
View File

@ -6,3 +6,9 @@ lib/EpdFont/fontsrc
*.generated.h *.generated.h
build build
**/__pycache__/ **/__pycache__/
# AI
.claude/
.serena/
.spec-workflow/
docs/plans

258
docs/cjk-fonts.md Normal file
View File

@ -0,0 +1,258 @@
# CJK Font Support
This guide explains how to use Chinese, Japanese, and Korean (CJK) fonts on your CrossPoint Reader.
## Overview
CrossPoint Reader supports external CJK fonts for both:
- **UI Font** - Used for menus, settings, and system interface
- **Reader Font** - Used for reading ebook content
The device includes a built-in CJK UI font (Source Han Sans subset) for basic interface rendering, and supports loading custom external fonts from the SD card.
## Prerequisites
- CrossPoint Reader device with firmware version supporting CJK fonts
- SD card with available space for font files
- TrueType font files (.ttf) converted to the CrossPoint font format
---
## Font File Format
CrossPoint Reader uses a custom binary font format optimized for e-ink displays. Font files must be placed in the `/fonts/` directory on the SD card.
### File Naming Convention
Font files must follow this naming pattern:
```
{FontName}_{Size}_{Width}x{Height}.bin
```
**Examples:**
- `SourceHanSansCN-Medium_20_20x20.bin`
- `KingHwaOldSong_38_33x39.bin`
- `Yozai-Medium_36_31x31.bin`
### Font File Structure
The binary font file contains:
1. **Header** (variable length)
- Font name (null-terminated string)
- Font size (uint8_t)
- Character width (uint8_t)
- Character height (uint8_t)
- Bytes per character (uint16_t)
2. **Character Data**
- Sequential bitmap data for each character
- Characters are stored in Unicode order
- Each character uses `width * height / 8` bytes (1-bit per pixel)
---
## Generating Font Files
Use the provided Python script to convert TrueType fonts:
```bash
python scripts/generate_cjk_ui_font.py \
--font /path/to/font.ttf \
--size 20 \
--output /path/to/output.bin
```
### Script Options
| Option | Description |
|--------|-------------|
| `--font` | Path to TrueType font file (.ttf) |
| `--size` | Font size in points |
| `--output` | Output binary file path |
| `--chars` | (Optional) Custom character set file |
### Character Set
By default, the script generates fonts for:
- Basic ASCII characters (0x20-0x7E)
- Common CJK characters (GB2312 subset)
- Japanese Hiragana and Katakana
- Common punctuation marks
---
## Installing Fonts
1. **Create the fonts directory** (if it doesn't exist):
```
/fonts/
```
2. **Copy font files** to the `/fonts/` directory on your SD card
3. **Restart the device** or go to Settings to scan for new fonts
---
## Selecting Fonts
### UI Font
1. Go to **Settings** → **Display**
2. Select **External UI Font**
3. Choose from available fonts or select **Built-in (Disabled)** to use the default font
### Reader Font
1. Go to **Settings** → **Reader**
2. Select **External Reader Font**
3. Choose from available fonts or select **Built-in (Disabled)** to use the default font
---
## Built-in CJK UI Font
The firmware includes a pre-rendered subset of Source Han Sans (思源黑体) for UI rendering. This font covers:
- All ASCII characters
- Common Chinese characters used in the UI
- Japanese Hiragana and Katakana
- Common punctuation marks
The built-in font is automatically used when:
- No external UI font is selected
- The external font file is missing or corrupted
---
## External UI Font Features
When an external UI font is selected:
### Full Character Coverage
- **All characters** (including ASCII letters, numbers, and punctuation) are rendered using the external font
- This ensures consistent visual style across the entire UI
- If a character is missing from the external font, the system falls back to built-in fonts
### Proportional Spacing
- External fonts use **proportional spacing** (variable width)
- Each character advances by its actual width, not a fixed width
- This makes English text look more natural with proper letter spacing
- CJK characters still use their full width as designed
---
## Recommended Fonts
### For UI (Small sizes, 18-24pt)
| Font | Description | License |
|------|-------------|---------|
| Source Han Sans | Clean, modern sans-serif | OFL |
| Noto Sans CJK | Google's CJK font family | OFL |
| WenQuanYi Micro Hei | Compact Chinese font | GPL |
### For Reading (Larger sizes, 28-40pt)
| Font | Description | License |
|------|-------------|---------|
| Source Han Serif | Traditional serif style | OFL |
| Noto Serif CJK | Google's serif CJK font | OFL |
| FangSong | Classic Chinese style | Varies |
---
## Memory Considerations
External fonts consume RAM when loaded. Consider these guidelines:
| Font Size | Approx. Memory Usage |
|-----------|---------------------|
| 20pt | ~50KB per 1000 characters |
| 28pt | ~100KB per 1000 characters |
| 36pt | ~160KB per 1000 characters |
**Tips:**
- Use smaller font sizes for UI (18-24pt)
- Larger fonts (32pt+) are better for reader content
- Only one UI font and one reader font are loaded at a time
---
## Troubleshooting
### Font not appearing in selection list
1. Check the file is in `/fonts/` directory
2. Verify the filename follows the naming convention
3. Ensure the file is not corrupted (try regenerating)
### Characters displaying as boxes or question marks
1. The character may not be included in the font
2. Try a font with broader character coverage
3. Check if the font file was generated correctly
### Device running slowly after selecting font
1. The font file may be too large
2. Try a smaller font size
3. Reduce the character set when generating the font
### Font looks blurry or pixelated
1. E-ink displays work best with specific font sizes
2. Try sizes that are multiples of the display's native resolution
3. Ensure anti-aliasing is disabled for 1-bit rendering
---
## Technical Details
### Font Manager API
The `FontManager` class provides:
```cpp
// Scan for available fonts
FontMgr.scanFonts();
// Get font count
int count = FontMgr.getFontCount();
// Get font info
const FontInfo* info = FontMgr.getFontInfo(index);
// Select reader font (-1 to disable)
FontMgr.selectFont(index);
// Select UI font (-1 to disable)
FontMgr.selectUiFont(index);
// Check if external font is enabled
bool enabled = FontMgr.isExternalFontEnabled();
```
### Font Info Structure
```cpp
struct FontInfo {
char name[32]; // Font name
uint8_t size; // Font size in points
uint8_t width; // Character width in pixels
uint8_t height; // Character height in pixels
uint16_t bytesPerChar; // Bytes per character
char path[64]; // Full path to font file
};
```
---
## Related Documentation
- [Internationalization (I18N)](./i18n.md) - Multi-language support
- [File Formats](./file-formats.md) - Binary file format specifications
- [Troubleshooting](./troubleshooting.md) - General troubleshooting guide

311
docs/i18n.md Normal file
View File

@ -0,0 +1,311 @@
# Internationalization (I18N)
This guide explains the multi-language support system in CrossPoint Reader.
## Overview
CrossPoint Reader supports multiple languages for the user interface:
- **English** (Default)
- **Chinese Simplified** (简体中文)
- **Japanese** (日本語)
All UI text, menus, settings, and system messages are translated.
---
## Changing Language
1. Go to **Settings** → **System**
2. Select **Language**
3. Choose your preferred language from the list:
- **English** - Displayed as "English"
- **简体中文** - Displayed as "简体中文" (Chinese Simplified)
- **日本語** - Displayed as "日本語" (Japanese)
4. The interface will update immediately
**Note:** Language changes take effect immediately without requiring a restart.
---
## Supported Languages
### English (Default)
The default language. All text is displayed in English.
### Chinese Simplified (简体中文)
Full Chinese translation including:
- All menu items and settings
- System messages and prompts
- Button labels and hints
- Error messages
**Requirements:** CJK UI font must be enabled for proper character display.
### Japanese (日本語)
Full Japanese translation including:
- All menu items and settings
- System messages and prompts
- Button labels and hints
- Error messages
**Requirements:** CJK UI font must be enabled for proper character display.
---
## Language and Font Relationship
When using Chinese or Japanese:
1. **Enable CJK UI Font** - Go to Settings → Display → UI Font
2. **Select a font** that supports your language's characters
3. **Change language** - The UI will now display correctly
If CJK font is not enabled, Chinese/Japanese characters may display as boxes or question marks.
---
## For Developers
### Adding New Translations
#### 1. Add String ID
In `lib/I18n/I18n.h`, add a new entry to the `StrId` enum:
```cpp
enum class StrId : uint16_t {
// ... existing entries ...
MY_NEW_STRING,
// ... more entries ...
_COUNT // Must be last
};
```
#### 2. Add Translations
In `lib/I18n/I18n.cpp`, add translations to each language array:
```cpp
// English
static const char* const STRINGS_EN[] = {
// ... existing strings ...
"My New String",
// ... more strings ...
};
// Chinese Simplified
static const char* const STRINGS_ZH[] = {
// ... existing strings ...
"\xE6\x88\x91\xE7\x9A\x84\xE6\x96\xB0\xE5\xAD\x97\xE7\xAC\xA6\xE4\xB8\xB2", // 我的新字符串
// ... more strings ...
};
// Japanese
static const char* const STRINGS_JA[] = {
// ... existing strings ...
"\xE6\x96\xB0\xE3\x81\x97\xE3\x81\x84\xE6\x96\x87\xE5\xAD\x97\xE5\x88\x97", // 新しい文字列
// ... more strings ...
};
```
**Important:**
- Arrays must have the same number of entries
- Use UTF-8 hex encoding for non-ASCII characters
- Keep entries in the same order as the enum
#### 3. Use in Code
```cpp
#include <I18n.h>
// Using the TR() macro (recommended)
renderer.drawText(font, x, y, TR(MY_NEW_STRING));
// Using I18N.get() directly
const char* text = I18N.get(StrId::MY_NEW_STRING);
```
### UTF-8 Hex Encoding
For non-ASCII characters, use UTF-8 hex encoding:
| Character | UTF-8 Hex |
|-----------|-----------|
| 中 | `\xE4\xB8\xAD` |
| 文 | `\xE6\x96\x87` |
| 日 | `\xE6\x97\xA5` |
| 本 | `\xE6\x9C\xAC` |
**Python helper:**
```python
def to_utf8_hex(text):
return ''.join(f'\\x{b:02X}' for b in text.encode('utf-8'))
print(to_utf8_hex("设置")) # \xE8\xAE\xBE\xE7\xBD\xAE
```
### I18N API Reference
```cpp
// Get the singleton instance
I18n& i18n = I18n::getInstance();
// Or use the global macro
I18N.get(StrId::SETTINGS);
// Get translated string
const char* text = I18N.get(StrId::SETTINGS);
// Set language
I18N.setLanguage(Language::ZH);
// Get current language
Language lang = I18N.getLanguage();
// Save language setting to file
I18N.saveSettings();
// Load language setting from file
I18N.loadSettings();
```
### Language Enum
```cpp
enum class Language : uint8_t {
EN = 0, // English
ZH = 1, // Chinese Simplified
JA = 2, // Japanese
_COUNT // Number of languages
};
```
---
## String Categories
The I18N system organizes strings into categories:
### Navigation & Actions
- `BACK`, `CONFIRM`, `CANCEL`, `SELECT`, `EXIT`, `DONE`
### Settings Categories
- `CAT_DISPLAY`, `CAT_READER`, `CAT_CONTROLS`, `CAT_SYSTEM`
### Display Settings
- `SLEEP_SCREEN`, `STATUS_BAR`, `REFRESH_FREQ`, etc.
### Reader Settings
- `FONT_SIZE`, `LINE_SPACING`, `ORIENTATION`, etc.
### System Settings
- `LANGUAGE`, `KOREADER_SYNC`, `CALIBRE_SETTINGS`, etc.
### Status Messages
- `CONNECTING`, `CONNECTED`, `FAILED`, `SUCCESS`, etc.
### File Transfer
- `FILE_TRANSFER`, `HOTSPOT_MODE`, `NETWORK_PREFIX`, etc.
---
## File Storage
Language settings are stored in:
```
/.crosspoint/i18n.bin
```
This file contains:
- Current language selection (1 byte)
---
## Adding a New Language
To add support for a new language:
1. **Add language to enum** in `lib/I18n/I18n.h`:
```cpp
enum class Language : uint8_t {
EN = 0,
ZH = 1,
JA = 2,
KO = 3, // New: Korean
_COUNT
};
```
2. **Create translation array** in `lib/I18n/I18n.cpp`:
```cpp
static const char* const STRINGS_KO[] = {
// All strings translated to Korean
};
```
3. **Add to language arrays**:
```cpp
static const char* const* const LANGUAGE_STRINGS[] = {
STRINGS_EN,
STRINGS_ZH,
STRINGS_JA,
STRINGS_KO, // New
};
```
4. **Add language name** for the language selection UI:
```cpp
// In LanguageSelectActivity or I18n
const char* languageNames[] = {
"English",
"简体中文",
"日本語",
"한국어", // New
};
```
5. **Ensure CJK font support** for the new language's characters
---
## Best Practices
1. **Keep translations concise** - E-ink displays have limited space
2. **Test on device** - Some characters may render differently
3. **Use consistent terminology** - Keep translations consistent across the UI
4. **Consider context** - Same word may need different translations in different contexts
5. **Update all languages** - When adding new strings, add translations for all languages
---
## Troubleshooting
### Characters showing as boxes
1. Enable CJK UI font in Settings → Display
2. Ensure the font includes the required characters
3. Check that the font file is not corrupted
### Language not saving
1. Check SD card is not write-protected
2. Verify `/.crosspoint/` directory exists
3. Check available space on SD card
### Missing translations
1. Verify string ID exists in the enum
2. Check all language arrays have the same number of entries
3. Rebuild firmware after adding new strings
---
## Related Documentation
- [CJK Font Support](./cjk-fonts.md) - External font installation
- [File Formats](./file-formats.md) - Binary file format specifications
- [Troubleshooting](./troubleshooting.md) - General troubleshooting guide

View File

@ -0,0 +1,345 @@
#include "ExternalFont.h"
#include <HardwareSerial.h>
#include <algorithm>
#include <cstring>
#include <vector>
ExternalFont::~ExternalFont() { unload(); }
void ExternalFont::unload() {
if (_fontFile) {
_fontFile.close();
}
_isLoaded = false;
_fontName[0] = '\0';
_fontSize = 0;
_charWidth = 0;
_charHeight = 0;
_bytesPerRow = 0;
_bytesPerChar = 0;
_accessCounter = 0;
// Clear cache and hash table
for (int i = 0; i < CACHE_SIZE; i++) {
_cache[i].codepoint = 0xFFFFFFFF;
_cache[i].lastUsed = 0;
_cache[i].notFound = false;
_hashTable[i] = -1;
}
}
bool ExternalFont::parseFilename(const char *filepath) {
// Extract filename from path
const char *filename = strrchr(filepath, '/');
if (filename) {
filename++; // Skip '/'
} else {
filename = filepath;
}
// Parse format: FontName_size_WxH.bin
// Example: KingHwaOldSong_38_33x39.bin
char nameCopy[64];
strncpy(nameCopy, filename, sizeof(nameCopy) - 1);
nameCopy[sizeof(nameCopy) - 1] = '\0';
// Remove .bin extension
char *ext = strstr(nameCopy, ".bin");
if (!ext) {
Serial.printf("[EXT_FONT] Invalid filename: no .bin extension\n");
return false;
}
*ext = '\0';
// Find _WxH part from the end
char *lastUnderscore = strrchr(nameCopy, '_');
if (!lastUnderscore) {
Serial.printf("[EXT_FONT] Invalid filename format\n");
return false;
}
// Parse WxH
int w, h;
if (sscanf(lastUnderscore + 1, "%dx%d", &w, &h) != 2) {
Serial.printf("[EXT_FONT] Failed to parse dimensions\n");
return false;
}
_charWidth = (uint8_t)w;
_charHeight = (uint8_t)h;
*lastUnderscore = '\0';
// Find size
lastUnderscore = strrchr(nameCopy, '_');
if (!lastUnderscore) {
Serial.printf("[EXT_FONT] Invalid filename format: no size\n");
return false;
}
int size;
if (sscanf(lastUnderscore + 1, "%d", &size) != 1) {
Serial.printf("[EXT_FONT] Failed to parse size\n");
return false;
}
_fontSize = (uint8_t)size;
*lastUnderscore = '\0';
// Remaining part is font name
strncpy(_fontName, nameCopy, sizeof(_fontName) - 1);
_fontName[sizeof(_fontName) - 1] = '\0';
// Calculate bytes per char
_bytesPerRow = (_charWidth + 7) / 8;
_bytesPerChar = _bytesPerRow * _charHeight;
if (_bytesPerChar > MAX_GLYPH_BYTES) {
Serial.printf("[EXT_FONT] Glyph too large: %d bytes (max %d)\n",
_bytesPerChar, MAX_GLYPH_BYTES);
return false;
}
Serial.printf("[EXT_FONT] Parsed: name=%s, size=%d, %dx%d, %d bytes/char\n",
_fontName, _fontSize, _charWidth, _charHeight, _bytesPerChar);
return true;
}
bool ExternalFont::load(const char *filepath) {
unload();
if (!parseFilename(filepath)) {
return false;
}
if (!SdMan.openFileForRead("EXT_FONT", filepath, _fontFile)) {
Serial.printf("[EXT_FONT] Failed to open: %s\n", filepath);
return false;
}
_isLoaded = true;
Serial.printf("[EXT_FONT] Loaded: %s\n", filepath);
return true;
}
int ExternalFont::findInCache(uint32_t codepoint) {
// O(1) hash table lookup with linear probing for collisions
int hash = hashCodepoint(codepoint);
for (int i = 0; i < CACHE_SIZE; i++) {
int idx = (hash + i) % CACHE_SIZE;
int16_t cacheIdx = _hashTable[idx];
if (cacheIdx == -1) {
// Empty slot, not found
return -1;
}
if (_cache[cacheIdx].codepoint == codepoint) {
return cacheIdx;
}
}
return -1;
}
int ExternalFont::getLruSlot() {
int lruIndex = 0;
uint32_t minUsed = _cache[0].lastUsed;
for (int i = 1; i < CACHE_SIZE; i++) {
// Prefer unused slots
if (_cache[i].codepoint == 0xFFFFFFFF) {
return i;
}
if (_cache[i].lastUsed < minUsed) {
minUsed = _cache[i].lastUsed;
lruIndex = i;
}
}
return lruIndex;
}
bool ExternalFont::readGlyphFromSD(uint32_t codepoint, uint8_t *buffer) {
if (!_fontFile) {
return false;
}
// Calculate offset
uint32_t offset = codepoint * _bytesPerChar;
// Seek and read
if (!_fontFile.seek(offset)) {
return false;
}
size_t bytesRead = _fontFile.read(buffer, _bytesPerChar);
if (bytesRead != _bytesPerChar) {
// May be end of file or other error, fill with zeros
memset(buffer, 0, _bytesPerChar);
}
return true;
}
const uint8_t *ExternalFont::getGlyph(uint32_t codepoint) {
if (!_isLoaded) {
return nullptr;
}
// First check cache (O(1) with hash table)
int cacheIndex = findInCache(codepoint);
if (cacheIndex >= 0) {
_cache[cacheIndex].lastUsed = ++_accessCounter;
// Return nullptr if this codepoint was previously marked as not found
if (_cache[cacheIndex].notFound) {
return nullptr;
}
return _cache[cacheIndex].bitmap;
}
// Cache miss, need to read from SD card
int slot = getLruSlot();
// If replacing an existing entry, remove it from hash table
if (_cache[slot].codepoint != 0xFFFFFFFF) {
int oldHash = hashCodepoint(_cache[slot].codepoint);
for (int i = 0; i < CACHE_SIZE; i++) {
int idx = (oldHash + i) % CACHE_SIZE;
if (_hashTable[idx] == slot) {
_hashTable[idx] = -1;
break;
}
}
}
// Read glyph from SD card
bool readSuccess = readGlyphFromSD(codepoint, _cache[slot].bitmap);
// Calculate metrics and check if glyph is empty
uint8_t minX = _charWidth;
uint8_t maxX = 0;
bool isEmpty = true;
if (readSuccess && _bytesPerChar > 0) {
for (int y = 0; y < _charHeight; y++) {
for (int x = 0; x < _charWidth; x++) {
int byteIndex = y * _bytesPerRow + (x / 8);
int bitIndex = 7 - (x % 8);
if ((_cache[slot].bitmap[byteIndex] >> bitIndex) & 1) {
isEmpty = false;
if (x < minX)
minX = x;
if (x > maxX)
maxX = x;
}
}
}
}
// Update cache entry
_cache[slot].codepoint = codepoint;
_cache[slot].lastUsed = ++_accessCounter;
// Check if this is a whitespace character (U+2000-U+200F: various spaces)
bool isWhitespace = (codepoint >= 0x2000 && codepoint <= 0x200F);
// Mark as notFound only if read failed or (empty AND not whitespace AND non-ASCII)
// Whitespace characters are expected to be empty but should still be rendered
_cache[slot].notFound = !readSuccess || (isEmpty && !isWhitespace && codepoint > 0x7F);
// Store metrics
if (!isEmpty) {
_cache[slot].minX = minX;
// Variable width: content width + 2px padding
_cache[slot].advanceX = (maxX - minX + 1) + 2;
} else {
_cache[slot].minX = 0;
// Special handling for whitespace characters
if (isWhitespace) {
// em-space (U+2003) and similar should be full-width (same as CJK char)
// en-space (U+2002) should be half-width
// Other spaces use appropriate widths
if (codepoint == 0x2003) {
// em-space: full CJK character width
_cache[slot].advanceX = _charWidth;
} else if (codepoint == 0x2002) {
// en-space: half CJK character width
_cache[slot].advanceX = _charWidth / 2;
} else if (codepoint == 0x3000) {
// Ideographic space (CJK full-width space): full width
_cache[slot].advanceX = _charWidth;
} else {
// Other spaces: use standard space width
_cache[slot].advanceX = _charWidth / 3;
}
} else {
// Fallback for other empty glyphs
_cache[slot].advanceX = _charWidth / 3;
}
}
// Add to hash table
int hash = hashCodepoint(codepoint);
for (int i = 0; i < CACHE_SIZE; i++) {
int idx = (hash + i) % CACHE_SIZE;
if (_hashTable[idx] == -1) {
_hashTable[idx] = slot;
break;
}
}
if (_cache[slot].notFound) {
return nullptr;
}
return _cache[slot].bitmap;
}
bool ExternalFont::getGlyphMetrics(uint32_t codepoint, uint8_t *outMinX,
uint8_t *outAdvanceX) {
int idx = findInCache(codepoint);
if (idx >= 0 && !_cache[idx].notFound) {
if (outMinX)
*outMinX = _cache[idx].minX;
if (outAdvanceX)
*outAdvanceX = _cache[idx].advanceX;
return true;
}
return false;
}
void ExternalFont::preloadGlyphs(const uint32_t *codepoints, size_t count) {
if (!_isLoaded || !codepoints || count == 0) {
return;
}
// Limit to cache size to avoid thrashing
const size_t maxLoad = std::min(count, static_cast<size_t>(CACHE_SIZE));
// Create a sorted copy for sequential SD card access
// Sequential reads are much faster than random seeks
std::vector<uint32_t> sorted(codepoints, codepoints + maxLoad);
std::sort(sorted.begin(), sorted.end());
// Remove duplicates
sorted.erase(std::unique(sorted.begin(), sorted.end()), sorted.end());
Serial.printf("[EXT_FONT] Preloading %zu unique glyphs\n", sorted.size());
const unsigned long startTime = millis();
size_t loaded = 0;
size_t skipped = 0;
for (uint32_t cp : sorted) {
// Skip if already in cache
if (findInCache(cp) >= 0) {
skipped++;
continue;
}
// Load into cache (getGlyph handles all the cache management)
getGlyph(cp);
loaded++;
}
Serial.printf(
"[EXT_FONT] Preload done: %zu loaded, %zu already cached, took %lums\n",
loaded, skipped, millis() - startTime);
}

View File

@ -0,0 +1,128 @@
#pragma once
#include <SDCardManager.h>
#include <cstdint>
/**
* External font loader - supports Xteink .bin format
* Filename format: FontName_size_WxH.bin (e.g. KingHwaOldSong_38_33x39.bin)
*
* Font format:
* - Direct Unicode codepoint indexing
* - Offset = codepoint * bytesPerChar
* - Each char = bytesPerRow * charHeight bytes
* - 1-bit black/white bitmap, MSB first
*/
class ExternalFont {
public:
ExternalFont() = default;
~ExternalFont();
// Disable copy
ExternalFont(const ExternalFont &) = delete;
ExternalFont &operator=(const ExternalFont &) = delete;
/**
* Load font from .bin file
* @param filepath Full path on SD card (e.g.
* "/fonts/KingHwaOldSong_38_33x39.bin")
* @return true on success
*/
bool load(const char *filepath);
/**
* Get glyph bitmap data (with LRU cache)
* @param codepoint Unicode codepoint
* @return Bitmap data pointer, nullptr if char not found
*/
const uint8_t *getGlyph(uint32_t codepoint);
/**
* Preload multiple glyphs at once (optimized for batch SD reads)
* Call this before rendering a chapter to warm up the cache
* @param codepoints Array of unicode codepoints to preload
* @param count Number of codepoints in the array
*/
void preloadGlyphs(const uint32_t *codepoints, size_t count);
// Font properties
uint8_t getCharWidth() const { return _charWidth; }
uint8_t getCharHeight() const { return _charHeight; }
uint8_t getBytesPerRow() const { return _bytesPerRow; }
uint16_t getBytesPerChar() const { return _bytesPerChar; }
const char *getFontName() const { return _fontName; }
uint8_t getFontSize() const { return _fontSize; }
bool isLoaded() const { return _isLoaded; }
void unload();
/**
* Get cached metrics for a glyph.
* Must call getGlyph() first to ensure it's loaded!
* @param cp Unicode codepoint
* @param outMinX Minimum X offset (left bearing)
* @param outAdvanceX Advance width for cursor positioning
* @return true if metrics found in cache, false otherwise
*/
bool getGlyphMetrics(uint32_t cp, uint8_t *outMinX, uint8_t *outAdvanceX);
private:
// Font file handle (keep open to avoid repeated open/close)
FsFile _fontFile;
bool _isLoaded = false;
// Properties parsed from filename
char _fontName[32] = {0};
uint8_t _fontSize = 0;
uint8_t _charWidth = 0;
uint8_t _charHeight = 0;
uint8_t _bytesPerRow = 0;
uint16_t _bytesPerChar = 0;
// LRU cache - 256 glyphs for better Chinese text performance
// Memory: ~52KB (256 * 204 bytes per entry)
static constexpr int CACHE_SIZE = 256; // 256 glyphs
static constexpr int MAX_GLYPH_BYTES =
200; // Max 200 bytes per glyph (enough for 33x39)
// Flag to mark cached "non-existent" glyphs (avoid repeated SD reads)
static constexpr uint8_t GLYPH_NOT_FOUND_MARKER = 0xFE;
struct CacheEntry {
uint32_t codepoint = 0xFFFFFFFF; // Invalid marker
uint8_t bitmap[MAX_GLYPH_BYTES];
uint32_t lastUsed = 0;
bool notFound = false; // True if glyph doesn't exist in font
uint8_t minX = 0; // Cached rendering metrics
uint8_t advanceX = 0; // Cached advance width
};
CacheEntry _cache[CACHE_SIZE];
uint32_t _accessCounter = 0;
// Simple hash table for O(1) cache lookup (codepoint -> cache index, -1 if
// not cached)
int16_t _hashTable[CACHE_SIZE];
static int hashCodepoint(uint32_t cp) { return cp % CACHE_SIZE; }
/**
* Read glyph data from SD card
*/
bool readGlyphFromSD(uint32_t codepoint, uint8_t *buffer);
/**
* Parse filename to get font parameters
* Format: FontName_size_WxH.bin
*/
bool parseFilename(const char *filename);
/**
* Find glyph in cache
* @return Cache index, -1 if not found
*/
int findInCache(uint32_t codepoint);
/**
* Get LRU cache slot (least recently used)
*/
int getLruSlot();
};

View File

@ -0,0 +1,284 @@
#include "FontManager.h"
#include <HardwareSerial.h>
#include <SDCardManager.h>
#include <Serialization.h>
#include <cstring>
// Out-of-class definitions for static constexpr members (required for ODR-use
// in C++14)
constexpr int FontManager::MAX_FONTS;
constexpr const char *FontManager::FONTS_DIR;
constexpr const char *FontManager::SETTINGS_FILE;
constexpr uint8_t FontManager::SETTINGS_VERSION;
FontManager &FontManager::getInstance() {
static FontManager instance;
return instance;
}
void FontManager::scanFonts() {
_fontCount = 0;
FsFile dir = SdMan.open(FONTS_DIR, O_RDONLY);
if (!dir) {
Serial.printf("[FONT_MGR] Cannot open fonts directory: %s\n", FONTS_DIR);
return;
}
if (!dir.isDir()) {
Serial.printf("[FONT_MGR] %s is not a directory\n", FONTS_DIR);
dir.close();
return;
}
FsFile entry;
while (_fontCount < MAX_FONTS && entry.openNext(&dir, O_RDONLY)) {
if (entry.isDir()) {
entry.close();
continue;
}
char filename[64];
entry.getName(filename, sizeof(filename));
entry.close();
// Check .bin extension
if (!strstr(filename, ".bin")) {
continue;
}
// Try to parse filename
FontInfo &info = _fonts[_fontCount];
strncpy(info.filename, filename, sizeof(info.filename) - 1);
info.filename[sizeof(info.filename) - 1] = '\0';
// Parse filename to get font info
char nameCopy[64];
strncpy(nameCopy, filename, sizeof(nameCopy) - 1);
nameCopy[sizeof(nameCopy) - 1] = '\0';
// Remove .bin
char *ext = strstr(nameCopy, ".bin");
if (ext)
*ext = '\0';
// Parse _WxH
char *lastUnderscore = strrchr(nameCopy, '_');
if (!lastUnderscore)
continue;
int w, h;
if (sscanf(lastUnderscore + 1, "%dx%d", &w, &h) != 2)
continue;
info.width = (uint8_t)w;
info.height = (uint8_t)h;
*lastUnderscore = '\0';
// Parse _size
lastUnderscore = strrchr(nameCopy, '_');
if (!lastUnderscore)
continue;
int size;
if (sscanf(lastUnderscore + 1, "%d", &size) != 1)
continue;
info.size = (uint8_t)size;
*lastUnderscore = '\0';
// Font name
strncpy(info.name, nameCopy, sizeof(info.name) - 1);
info.name[sizeof(info.name) - 1] = '\0';
Serial.printf("[FONT_MGR] Found font: %s (%dpt, %dx%d)\n", info.name,
info.size, info.width, info.height);
_fontCount++;
}
dir.close();
Serial.printf("[FONT_MGR] Scan complete: %d fonts found\n", _fontCount);
}
const FontInfo *FontManager::getFontInfo(int index) const {
if (index < 0 || index >= _fontCount) {
return nullptr;
}
return &_fonts[index];
}
bool FontManager::loadSelectedFont() {
_activeFont.unload();
if (_selectedIndex < 0 || _selectedIndex >= _fontCount) {
return false;
}
char filepath[80];
snprintf(filepath, sizeof(filepath), "%s/%s", FONTS_DIR,
_fonts[_selectedIndex].filename);
return _activeFont.load(filepath);
}
bool FontManager::loadSelectedUiFont() {
_activeUiFont.unload();
if (_selectedUiIndex < 0 || _selectedUiIndex >= _fontCount) {
return false;
}
char filepath[80];
snprintf(filepath, sizeof(filepath), "%s/%s", FONTS_DIR,
_fonts[_selectedUiIndex].filename);
return _activeUiFont.load(filepath);
}
void FontManager::selectFont(int index) {
if (index == _selectedIndex) {
return;
}
_selectedIndex = index;
if (index >= 0) {
loadSelectedFont();
} else {
_activeFont.unload();
}
saveSettings();
}
void FontManager::selectUiFont(int index) {
if (index == _selectedUiIndex) {
return;
}
_selectedUiIndex = index;
if (index >= 0) {
loadSelectedUiFont();
} else {
_activeUiFont.unload();
}
saveSettings();
}
ExternalFont *FontManager::getActiveFont() {
if (_selectedIndex >= 0 && _activeFont.isLoaded()) {
return &_activeFont;
}
return nullptr;
}
ExternalFont *FontManager::getActiveUiFont() {
if (_selectedUiIndex >= 0 && _activeUiFont.isLoaded()) {
return &_activeUiFont;
}
return nullptr;
}
void FontManager::saveSettings() {
SdMan.mkdir("/.crosspoint");
FsFile file;
if (!SdMan.openFileForWrite("FONT_MGR", SETTINGS_FILE, file)) {
Serial.printf("[FONT_MGR] Failed to save settings\n");
return;
}
serialization::writePod(file, SETTINGS_VERSION);
serialization::writePod(file, _selectedIndex);
// Save selected reader font filename (for matching when restoring)
if (_selectedIndex >= 0 && _selectedIndex < _fontCount) {
serialization::writeString(file,
std::string(_fonts[_selectedIndex].filename));
} else {
serialization::writeString(file, std::string(""));
}
// Save UI font settings (version 2+)
serialization::writePod(file, _selectedUiIndex);
if (_selectedUiIndex >= 0 && _selectedUiIndex < _fontCount) {
serialization::writeString(file,
std::string(_fonts[_selectedUiIndex].filename));
} else {
serialization::writeString(file, std::string(""));
}
file.close();
Serial.printf("[FONT_MGR] Settings saved\n");
}
void FontManager::loadSettings() {
FsFile file;
if (!SdMan.openFileForRead("FONT_MGR", SETTINGS_FILE, file)) {
Serial.printf("[FONT_MGR] No settings file, using defaults\n");
return;
}
uint8_t version;
serialization::readPod(file, version);
if (version < 1 || version > SETTINGS_VERSION) {
Serial.printf("[FONT_MGR] Settings version mismatch (%d vs %d)\n", version,
SETTINGS_VERSION);
file.close();
return;
}
// Load reader font settings
int savedIndex;
serialization::readPod(file, savedIndex);
std::string savedFilename;
serialization::readString(file, savedFilename);
// Find matching reader font by filename
if (savedIndex >= 0 && !savedFilename.empty()) {
for (int i = 0; i < _fontCount; i++) {
if (savedFilename == _fonts[i].filename) {
_selectedIndex = i;
loadSelectedFont();
Serial.printf("[FONT_MGR] Restored reader font: %s\n",
savedFilename.c_str());
break;
}
}
if (_selectedIndex < 0) {
Serial.printf("[FONT_MGR] Saved reader font not found: %s\n",
savedFilename.c_str());
}
}
// Load UI font settings (version 2+)
if (version >= 2) {
int savedUiIndex;
serialization::readPod(file, savedUiIndex);
std::string savedUiFilename;
serialization::readString(file, savedUiFilename);
if (savedUiIndex >= 0 && !savedUiFilename.empty()) {
for (int i = 0; i < _fontCount; i++) {
if (savedUiFilename == _fonts[i].filename) {
_selectedUiIndex = i;
loadSelectedUiFont();
Serial.printf("[FONT_MGR] Restored UI font: %s\n",
savedUiFilename.c_str());
break;
}
}
if (_selectedUiIndex < 0) {
Serial.printf("[FONT_MGR] Saved UI font not found: %s\n",
savedUiFilename.c_str());
}
}
}
file.close();
}

View File

@ -0,0 +1,137 @@
#pragma once
#include <cstdint>
#include "ExternalFont.h"
/**
* Font information structure
*/
struct FontInfo {
char filename[64]; // Full filename
char name[32]; // Font name
uint8_t size; // Font size (pt)
uint8_t width; // Character width
uint8_t height; // Character height
};
/**
* Font Manager - Singleton pattern
* Manages font scanning, selection, and settings persistence
* Supports two font slots: Reader font (for book content) and UI font (for
* menus/titles)
*/
class FontManager {
public:
static FontManager &getInstance();
// Disable copy
FontManager(const FontManager &) = delete;
FontManager &operator=(const FontManager &) = delete;
/**
* Scan /fonts/ directory to get available font list
*/
void scanFonts();
/**
* Get font count
*/
int getFontCount() const { return _fontCount; }
/**
* Get font info
* @param index Font index (0 to getFontCount()-1)
*/
const FontInfo *getFontInfo(int index) const;
/**
* Select reader font (for book content)
* @param index Font index, -1 means disable external font (use built-in)
*/
void selectFont(int index);
/**
* Select UI font (for menus, titles, etc.)
* @param index Font index, -1 means disable (fallback to reader font or
* built-in)
*/
void selectUiFont(int index);
/**
* Get currently selected reader font index
* @return -1 means using built-in font
*/
int getSelectedIndex() const { return _selectedIndex; }
/**
* Get currently selected UI font index
* @return -1 means using reader font fallback
*/
int getUiSelectedIndex() const { return _selectedUiIndex; }
/**
* Get currently active reader font
* @return Font pointer, nullptr if not enabled
*/
ExternalFont *getActiveFont();
/**
* Get currently active UI font
* @return Font pointer, nullptr if not enabled (will fallback to reader font)
*/
ExternalFont *getActiveUiFont();
/**
* Check if external reader font is enabled
*/
bool isExternalFontEnabled() const {
return _selectedIndex >= 0 && _activeFont.isLoaded();
}
/**
* Check if external UI font is enabled
*/
bool isUiFontEnabled() const {
return _selectedUiIndex >= 0 && _activeUiFont.isLoaded();
}
/**
* Save settings to SD card
*/
void saveSettings();
/**
* Load settings from SD card
*/
void loadSettings();
private:
FontManager() = default;
static constexpr int MAX_FONTS = 16;
static constexpr const char *FONTS_DIR = "/fonts";
static constexpr const char *SETTINGS_FILE = "/.crosspoint/font_settings.bin";
static constexpr uint8_t SETTINGS_VERSION = 2; // Bumped for UI font support
FontInfo _fonts[MAX_FONTS];
int _fontCount = 0;
int _selectedIndex = -1; // -1 = built-in font (reader)
int _selectedUiIndex = -1; // -1 = fallback to reader font
ExternalFont _activeFont; // Reader font
ExternalFont _activeUiFont; // UI font
/**
* Load selected reader font file
*/
bool loadSelectedFont();
/**
* Load selected UI font file
*/
bool loadSelectedUiFont();
};
// Convenience macro
#define FontMgr FontManager::getInstance()

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,9 @@
#include "Bitmap.h" #include "Bitmap.h"
// Forward declaration for external font support
class ExternalFont;
class GfxRenderer { class GfxRenderer {
public: public:
enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB }; enum RenderMode { BW, GRAYSCALE_LSB, GRAYSCALE_MSB };
@ -14,15 +17,20 @@ class GfxRenderer {
// Logical screen orientation from the perspective of callers // Logical screen orientation from the perspective of callers
enum Orientation { enum Orientation {
Portrait, // 480x800 logical coordinates (current default) Portrait, // 480x800 logical coordinates (current default)
LandscapeClockwise, // 800x480 logical coordinates, rotated 180° (swap top/bottom) LandscapeClockwise, // 800x480 logical coordinates, rotated 180° (swap
// top/bottom)
PortraitInverted, // 480x800 logical coordinates, inverted PortraitInverted, // 480x800 logical coordinates, inverted
LandscapeCounterClockwise // 800x480 logical coordinates, native panel orientation LandscapeCounterClockwise // 800x480 logical coordinates, native panel
// orientation
}; };
private: private:
static constexpr size_t BW_BUFFER_CHUNK_SIZE = 8000; // 8KB chunks to allow for non-contiguous memory static constexpr size_t BW_BUFFER_CHUNK_SIZE =
static constexpr size_t BW_BUFFER_NUM_CHUNKS = EInkDisplay::BUFFER_SIZE / BW_BUFFER_CHUNK_SIZE; 8000; // 8KB chunks to allow for non-contiguous memory
static_assert(BW_BUFFER_CHUNK_SIZE * BW_BUFFER_NUM_CHUNKS == EInkDisplay::BUFFER_SIZE, 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,
"BW buffer chunking does not line up with display buffer size"); "BW buffer chunking does not line up with display buffer size");
EInkDisplay &einkDisplay; EInkDisplay &einkDisplay;
@ -30,31 +38,71 @@ class GfxRenderer {
Orientation orientation; Orientation orientation;
uint8_t *bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr}; uint8_t *bwBufferChunks[BW_BUFFER_NUM_CHUNKS] = {nullptr};
std::map<int, EpdFontFamily> fontMap; std::map<int, EpdFontFamily> fontMap;
void renderChar(const EpdFontFamily& fontFamily, uint32_t cp, int* x, const int* y, bool pixelState, // UI font size: 0=20px(SMALL), 1=22px(MEDIUM), 2=24px(LARGE)
uint8_t uiFontSize = 0;
// Dark mode: true = black background, false = white background
bool darkMode = false;
// Extra spacing (in pixels) for ASCII letters/digits when using external reader font.
int8_t asciiLetterSpacing = 0;
int8_t asciiDigitSpacing = 0;
// Extra spacing (in pixels) for CJK characters when using external reader font.
int8_t cjkSpacing = 0;
// Skip dark mode inversion for images (cover art should not be inverted)
mutable bool skipDarkModeForImages = false;
void renderChar(int fontId, const EpdFontFamily &fontFamily, uint32_t cp,
int *x, const int *y, bool pixelState,
EpdFontFamily::Style style) const; EpdFontFamily::Style style) const;
void renderExternalGlyph(const uint8_t *bitmap, ExternalFont *font, int *x,
int y, bool pixelState,
int advanceOverride = -1) const;
// Render CJK character using built-in UI font (from PROGMEM)
void renderBuiltinCjkGlyph(uint32_t cp, int *x, int y, bool pixelState) const;
// Check if fontId is a reader font (should use external Chinese font)
static bool isReaderFont(int fontId);
void freeBwBufferChunks(); void freeBwBufferChunks();
void rotateCoordinates(int x, int y, int *rotatedX, int *rotatedY) const; void rotateCoordinates(int x, int y, int *rotatedX, int *rotatedY) const;
public: public:
explicit GfxRenderer(EInkDisplay& einkDisplay) : einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {} explicit GfxRenderer(EInkDisplay &einkDisplay)
: einkDisplay(einkDisplay), renderMode(BW), orientation(Portrait) {}
~GfxRenderer() { freeBwBufferChunks(); } ~GfxRenderer() { freeBwBufferChunks(); }
static constexpr int VIEWABLE_MARGIN_TOP = 9; static constexpr int VIEWABLE_MARGIN_TOP = 9;
static constexpr int VIEWABLE_MARGIN_RIGHT = 3; static constexpr int VIEWABLE_MARGIN_RIGHT = 3;
static constexpr int VIEWABLE_MARGIN_BOTTOM = 3; static constexpr int VIEWABLE_MARGIN_BOTTOM = 3;
static constexpr int VIEWABLE_MARGIN_LEFT = 3; static constexpr int VIEWABLE_MARGIN_LEFT = 3;
static constexpr int BUTTON_HINT_WIDTH = 106;
static constexpr int BUTTON_HINT_HEIGHT = 40;
static constexpr int BUTTON_HINT_BOTTOM_INSET = 40;
static constexpr int BUTTON_HINT_TEXT_OFFSET = 7;
// Setup // Setup
void insertFont(int fontId, EpdFontFamily font); void insertFont(int fontId, EpdFontFamily font);
// Orientation control (affects logical width/height and coordinate transforms) // Orientation control (affects logical width/height and coordinate
// transforms)
void setOrientation(const Orientation o) { orientation = o; } void setOrientation(const Orientation o) { orientation = o; }
Orientation getOrientation() const { return orientation; } Orientation getOrientation() const { return orientation; }
// UI font size control (0=20px, 1=22px, 2=24px)
void setUiFontSize(uint8_t size) { uiFontSize = (size > 2) ? 2 : size; }
uint8_t getUiFontSize() const { return uiFontSize; }
// Dark mode control
void setDarkMode(bool darkMode) { this->darkMode = darkMode; }
bool isDarkMode() const { return darkMode; }
void setAsciiLetterSpacing(int8_t spacing) { asciiLetterSpacing = spacing; }
void setAsciiDigitSpacing(int8_t spacing) { asciiDigitSpacing = spacing; }
void setCjkSpacing(int8_t spacing) { cjkSpacing = spacing; }
int8_t getAsciiLetterSpacing() const { return asciiLetterSpacing; }
int8_t getAsciiDigitSpacing() const { return asciiDigitSpacing; }
int8_t getCjkSpacing() const { return cjkSpacing; }
// Screen ops // Screen ops
int getScreenWidth() const; int getScreenWidth() const;
int getScreenHeight() const; int getScreenHeight() const;
void displayBuffer(EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const; void displayBuffer(
EInkDisplay::RefreshMode refreshMode = EInkDisplay::FAST_REFRESH) const;
// EXPERIMENTAL: Windowed update - display only a rectangular region // EXPERIMENTAL: Windowed update - display only a rectangular region
void displayWindow(int x, int y, int width, int height) const; void displayWindow(int x, int y, int width, int height) const;
void invertScreen() const; void invertScreen() const;
@ -65,31 +113,40 @@ class GfxRenderer {
void drawLine(int x1, int y1, int x2, int y2, bool state = true) const; void drawLine(int x1, int y1, int x2, int y2, bool state = true) const;
void drawRect(int x, int y, int width, int height, bool state = true) const; void drawRect(int x, int y, int width, int height, bool state = true) const;
void fillRect(int x, int y, int width, int height, bool state = true) const; void fillRect(int x, int y, int width, int height, bool state = true) const;
void drawImage(const uint8_t bitmap[], int x, int y, int width, int height) const; void drawImage(const uint8_t bitmap[], int x, int y, int width,
void drawBitmap(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight, float cropX = 0, int height) const;
float cropY = 0) const; void drawBitmap(const Bitmap &bitmap, int x, int y, int maxWidth,
void drawBitmap1Bit(const Bitmap& bitmap, int x, int y, int maxWidth, int maxHeight) const; int maxHeight, float cropX = 0, float cropY = 0) const;
void fillPolygon(const int* xPoints, const int* yPoints, int numPoints, bool state = true) const; void drawBitmap1Bit(const Bitmap &bitmap, int x, int y, int maxWidth,
int maxHeight) const;
void fillPolygon(const int *xPoints, const int *yPoints, int numPoints,
bool state = true) const;
// Text // Text
int getTextWidth(int fontId, const char* text, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; int getTextWidth(int fontId, const char *text,
void drawCenteredText(int fontId, int y, const char* text, bool black = true, EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
void
drawCenteredText(int fontId, int y, const char *text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
void drawText(int fontId, int x, int y, const char *text, bool black = true, void drawText(int fontId, int x, int y, const char *text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
int getSpaceWidth(int fontId) const; int getSpaceWidth(int fontId) const;
int getFontAscenderSize(int fontId) const; int getFontAscenderSize(int fontId) const;
int getLineHeight(int fontId) const; int getLineHeight(int fontId) const;
std::string truncatedText(int fontId, const char* text, int maxWidth, std::string
truncatedText(int fontId, const char *text, int maxWidth,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
// UI Components // UI Components
void drawButtonHints(int fontId, const char* btn1, const char* btn2, const char* btn3, const char* btn4); void drawButtonHints(int fontId, const char *btn1, const char *btn2,
void drawSideButtonHints(int fontId, const char* topBtn, const char* bottomBtn) const; const char *btn3, const char *btn4);
void drawSideButtonHints(int fontId, const char *topBtn,
const char *bottomBtn) const;
private: private:
// Helper for drawing rotated text (90 degrees clockwise, for side buttons) // Helper for drawing rotated text (90 degrees clockwise, for side buttons)
void drawTextRotated90CW(int fontId, int x, int y, const char* text, bool black = true, void drawTextRotated90CW(
int fontId, int x, int y, const char *text, bool black = true,
EpdFontFamily::Style style = EpdFontFamily::REGULAR) const; EpdFontFamily::Style style = EpdFontFamily::REGULAR) const;
int getTextHeight(int fontId) const; int getTextHeight(int fontId) const;
@ -98,7 +155,8 @@ class GfxRenderer {
void setRenderMode(const RenderMode mode) { this->renderMode = mode; } void setRenderMode(const RenderMode mode) { this->renderMode = mode; }
void copyGrayscaleLsbBuffers() const; void copyGrayscaleLsbBuffers() const;
void copyGrayscaleMsbBuffers() const; void copyGrayscaleMsbBuffers() const;
void displayGrayBuffer() const; void displayGrayBuffer(bool turnOffScreen = false,
bool darkMode = false) const;
bool storeBwBuffer(); // Returns true if buffer was stored successfully bool storeBwBuffer(); // Returns true if buffer was stored successfully
void restoreBwBuffer(); // Restore and free the stored buffer void restoreBwBuffer(); // Restore and free the stored buffer
void cleanupGrayscaleWithFrameBuffer() const; void cleanupGrayscaleWithFrameBuffer() const;
@ -107,5 +165,6 @@ class GfxRenderer {
uint8_t *getFrameBuffer() const; uint8_t *getFrameBuffer() const;
static size_t getBufferSize(); static size_t getBufferSize();
void grayscaleRevert() const; void grayscaleRevert() const;
void getOrientedViewableTRBL(int* outTop, int* outRight, int* outBottom, int* outLeft) const; void getOrientedViewableTRBL(int *outTop, int *outRight, int *outBottom,
int *outLeft) const;
}; };

View File

@ -0,0 +1,839 @@
/**
* Auto-generated CJK UI font data (optimized - UI characters only)
* Font: -Medium
* Size: 12pt
* Dimensions: 16x16
* Characters: 370
* Total size: 11840 bytes (11.6 KB)
*
* This is a sparse font containing only UI-required CJK characters.
* Uses a lookup table for codepoint -> glyph index mapping.
*/
#pragma once
#include <cstdint>
#include <pgmspace.h>
// Font parameters
static constexpr uint8_t CJK_UI_FONT_WIDTH = 16;
static constexpr uint8_t CJK_UI_FONT_HEIGHT = 16;
static constexpr uint8_t CJK_UI_FONT_BYTES_PER_ROW = 2;
static constexpr uint8_t CJK_UI_FONT_BYTES_PER_CHAR = 32;
static constexpr uint16_t CJK_UI_FONT_GLYPH_COUNT = 370;
// Codepoint lookup table (sorted for binary search)
static const uint16_t CJK_UI_CODEPOINTS[] PROGMEM = {
0x3042, 0x3044, 0x3048, 0x304B, 0x304C, 0x304D, 0x304F, 0x3053, 0x3057, 0x3059, 0x305B, 0x305F, 0x3064, 0x3066, 0x3067, 0x3068,
0x306A, 0x306B, 0x306E, 0x306F, 0x307E, 0x307F, 0x3080, 0x3081, 0x3089, 0x308A, 0x308B, 0x308C, 0x308F, 0x3092, 0x3093, 0x30A1,
0x30A2, 0x30A3, 0x30A4, 0x30A6, 0x30A8, 0x30A9, 0x30AA, 0x30AB, 0x30AD, 0x30AF, 0x30B3, 0x30B5, 0x30B6, 0x30B7, 0x30B8, 0x30B9,
0x30BA, 0x30BB, 0x30BF, 0x30C0, 0x30C1, 0x30C3, 0x30C6, 0x30C7, 0x30C8, 0x30C9, 0x30CD, 0x30D0, 0x30D1, 0x30D5, 0x30D6, 0x30D7,
0x30DA, 0x30DB, 0x30DC, 0x30DD, 0x30DE, 0x30DF, 0x30E0, 0x30E1, 0x30E2, 0x30E3, 0x30E4, 0x30E5, 0x30E7, 0x30E9, 0x30EA, 0x30EB,
0x30EC, 0x30ED, 0x30EF, 0x30F3, 0x30FC, 0x4E00, 0x4E0A, 0x4E0B, 0x4E0D, 0x4E21, 0x4E24, 0x4E2D, 0x4E3A, 0x4E3B, 0x4E49, 0x4E66,
0x4E86, 0x4E8C, 0x4EBA, 0x4ECE, 0x4ED6, 0x4EF6, 0x4EFB, 0x4F11, 0x4F20, 0x4F53, 0x4F59, 0x4F5C, 0x4F9B, 0x4FA7, 0x4FDD, 0x5012,
0x5074, 0x5165, 0x5173, 0x5185, 0x518D, 0x51D1, 0x51FA, 0x5206, 0x5207, 0x521B, 0x5220, 0x5230, 0x5237, 0x524A, 0x524D, 0x526A,
0x52A0, 0x52A8, 0x52B9, 0x52D5, 0x5316, 0x53C2, 0x53CD, 0x53D6, 0x53EF, 0x53F3, 0x53F7, 0x5411, 0x5426, 0x542F, 0x5668, 0x56DE,
0x56F2, 0x56F4, 0x56FD, 0x5728, 0x5730, 0x5740, 0x5907, 0x5916, 0x5927, 0x592E, 0x5931, 0x59CB, 0x5B57, 0x5B58, 0x5B8C, 0x5B9A,
0x5BBD, 0x5BC6, 0x5BF9, 0x5C01, 0x5C06, 0x5C0F, 0x5C40, 0x5C45, 0x5C4F, 0x5DE6, 0x5DF2, 0x5E03, 0x5E38, 0x5E55, 0x5E83, 0x5E93,
0x5E94, 0x5EA6, 0x5EFA, 0x5F00, 0x5F0F, 0x5F15, 0x5F53, 0x5FD8, 0x5FFD, 0x6001, 0x610F, 0x6210, 0x6216, 0x623B, 0x624B, 0x6253,
0x626B, 0x627E, 0x6297, 0x629E, 0x62BC, 0x62E9, 0x6309, 0x6357, 0x6362, 0x63A5, 0x63C3, 0x63CF, 0x6557, 0x6574, 0x6587, 0x65B0,
0x65B9, 0x65E0, 0x65E2, 0x65E5, 0x65F6, 0x662F, 0x663E, 0x6642, 0x666E, 0x6697, 0x66F4, 0x66F8, 0x66FF, 0x6700, 0x6709, 0x672A,
0x672B, 0x672C, 0x673A, 0x6761, 0x677E, 0x67E5, 0x680F, 0x68C0, 0x6A21, 0x6A2A, 0x6B21, 0x6B63, 0x6B64, 0x6BB5, 0x6BD4, 0x6CD5,
0x6D45, 0x6D4F, 0x6D88, 0x6DF1, 0x6E08, 0x6E90, 0x70B9, 0x70ED, 0x7121, 0x7248, 0x7279, 0x72B6, 0x72ED, 0x7387, 0x73B0, 0x73FE,
0x7528, 0x7535, 0x753B, 0x7565, 0x767D, 0x767E, 0x7684, 0x76EE, 0x7720, 0x77ED, 0x7801, 0x786E, 0x78BA, 0x793A, 0x7981, 0x7A7A,
0x7AD6, 0x7AE0, 0x7AEF, 0x7BC4, 0x7C4D, 0x7D22, 0x7D27, 0x7D42, 0x7D9A, 0x7E26, 0x7EBF, 0x7EC8, 0x7EDC, 0x7EE7, 0x7EED, 0x7EF4,
0x7F51, 0x7F6E, 0x7FFB, 0x81EA, 0x8272, 0x8282, 0x8303, 0x843D, 0x8535, 0x85CF, 0x884C, 0x88C1, 0x898B, 0x8996, 0x89A7, 0x89C8,
0x8A00, 0x8A08, 0x8A2D, 0x8A66, 0x8A8D, 0x8A9E, 0x8AAD, 0x8BA4, 0x8BB0, 0x8BBE, 0x8BD5, 0x8BED, 0x8BEF, 0x8BFB, 0x8D25, 0x8D77,
0x8D85, 0x8DDD, 0x8DF3, 0x8EE2, 0x8F6C, 0x8F7D, 0x8F93, 0x8FB9, 0x8FBC, 0x8FD4, 0x8FDB, 0x8FDE, 0x8FFD, 0x9000, 0x9001, 0x9002,
0x9006, 0x9009, 0x901A, 0x9032, 0x9078, 0x90E8, 0x914D, 0x91CD, 0x91CF, 0x9488, 0x949F, 0x94AE, 0x9519, 0x952E, 0x952F, 0x9577,
0x957F, 0x958B, 0x9593, 0x95F4, 0x9605, 0x9664, 0x9690, 0x9694, 0x96A0, 0x96FB, 0x9762, 0x983B, 0x9875, 0x987A, 0x9891, 0x989D,
0x9F50, 0x9F7F,
};
// Glyph bitmap data (stored in Flash/PROGMEM)
static const uint8_t CJK_UI_FONT_DATA[] PROGMEM = {
// U+3042 (あ)
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x38, 0x3F, 0xF8, 0x1F, 0xA0, 0x06, 0x70, 0x07, 0xF8, 0x0F, 0x7C, 0x1E, 0x6C, 0x3E, 0xC6, 0x33, 0xC6, 0x33, 0x8E, 0x77, 0x0C, 0x3F, 0x3C, 0x1A, 0x70,
// U+3044 (い)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x30, 0x10, 0x30, 0x18, 0x30, 0x18, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0E, 0x31, 0x86, 0x39, 0x86, 0x1B, 0x80, 0x1F, 0x00, 0x0E, 0x00,
// U+3048 (え)
0x00, 0x00, 0x00, 0x00, 0x07, 0x80, 0x07, 0xF0, 0x00, 0x60, 0x00, 0x00, 0x1F, 0xF0, 0x1F, 0xE0, 0x00, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x07, 0xC0, 0x0E, 0xC0, 0x1C, 0xC0, 0x38, 0x7E, 0x30, 0x7E,
// U+304B (か)
0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x18, 0x7F, 0x9C, 0x7F, 0xEC, 0x6C, 0x6E, 0x0C, 0x66, 0x0C, 0x66, 0x1C, 0x62, 0x18, 0x60, 0x18, 0x60, 0x30, 0xE0, 0x37, 0xC0, 0x77, 0xC0,
// U+304C (が)
0x00, 0x00, 0x00, 0x06, 0x06, 0x1A, 0x06, 0x0F, 0x06, 0x18, 0x0F, 0x98, 0x7F, 0xCC, 0x6C, 0xEC, 0x0C, 0x66, 0x0C, 0x66, 0x18, 0x66, 0x18, 0x60, 0x38, 0x60, 0x30, 0xC0, 0x77, 0xC0, 0x67, 0x80,
// U+304D (き)
0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0xF8, 0x1F, 0xF0, 0x0F, 0xC0, 0x00, 0x6C, 0x1F, 0xFC, 0x1F, 0xF0, 0x00, 0x30, 0x00, 0x70, 0x18, 0xF8, 0x18, 0x10, 0x18, 0x00, 0x1F, 0xE0, 0x0F, 0xE0,
// U+304F (く)
0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0x70, 0x00, 0x20,
// U+3053 (こ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x00, 0x38, 0x00, 0x18, 0x00, 0x1F, 0xFC, 0x0F, 0xF8,
// U+3057 (し)
0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x04, 0x1C, 0x0C, 0x0C, 0x3C, 0x0F, 0xF8, 0x07, 0xE0,
// U+3059 (す)
0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0x00, 0xC0, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0xC0, 0x03, 0xC0, 0x07, 0xC0, 0x06, 0x60, 0x06, 0x60, 0x07, 0xE0, 0x03, 0xE0, 0x01, 0xC0, 0x07, 0x80, 0x07, 0x00,
// U+305B (せ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x3F, 0xFE, 0x7F, 0xFE, 0x7C, 0x30, 0x0C, 0x30, 0x0C, 0xF0, 0x0C, 0x60, 0x0C, 0x00, 0x0E, 0x08, 0x0F, 0xF8, 0x03, 0xF8,
// U+305F (た)
0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x3F, 0xC0, 0x7F, 0x80, 0x0E, 0x00, 0x0C, 0xFC, 0x0C, 0xFC, 0x0C, 0x00, 0x1C, 0x00, 0x19, 0x80, 0x19, 0x80, 0x39, 0xC0, 0x30, 0xFE, 0x30, 0x7C,
// U+3064 (つ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xE0, 0x0F, 0xF8, 0x7F, 0x1C, 0x70, 0x0C, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x78, 0x07, 0xF0, 0x07, 0xC0, 0x00, 0x00,
// U+3066 (て)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFC, 0x7F, 0xFC, 0x78, 0xC0, 0x01, 0x80, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xF8, 0x00, 0x78,
// U+3067 (で)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7C, 0x7F, 0xFC, 0x7F, 0xE0, 0x01, 0x84, 0x03, 0x16, 0x03, 0x1E, 0x03, 0x08, 0x03, 0x00, 0x03, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x01, 0xF8, 0x00, 0x78,
// U+3068 (と)
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x1C, 0x00, 0x0C, 0x10, 0x0C, 0x78, 0x07, 0xF0, 0x07, 0xC0, 0x07, 0x00, 0x0E, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x1F, 0xF8, 0x0F, 0xF8,
// U+306A (な)
0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x7F, 0x98, 0x7F, 0xBC, 0x0C, 0x0E, 0x0C, 0x64, 0x1C, 0x60, 0x18, 0x60, 0x38, 0x60, 0x33, 0xF0, 0x37, 0xF8, 0x06, 0x7C, 0x07, 0xE4, 0x03, 0xE0,
// U+306B (に)
0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0x31, 0xFC, 0x31, 0xFC, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x30, 0x00, 0x38, 0x00, 0x3B, 0x00, 0x3B, 0x00, 0x3B, 0x84, 0x31, 0xFE, 0x30, 0xFC,
// U+306E (の)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xE0, 0x0F, 0xF8, 0x1D, 0x9C, 0x39, 0x8C, 0x31, 0x8C, 0x63, 0x86, 0x63, 0x06, 0x63, 0x0E, 0x67, 0x0E, 0x76, 0x0C, 0x3E, 0x3C, 0x1C, 0xF8, 0x00, 0xE0,
// U+306F (は)
0x00, 0x00, 0x00, 0x00, 0x10, 0x30, 0x30, 0x30, 0x30, 0x30, 0x33, 0xFE, 0x33, 0xFE, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x38, 0x30, 0x39, 0xF8, 0x3B, 0x3C, 0x33, 0x3E, 0x33, 0xF6, 0x31, 0xE0,
// U+307E (ま)
0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xC8, 0x1F, 0xF8, 0x0F, 0xE0, 0x01, 0x80, 0x1F, 0xFC, 0x1F, 0xFC, 0x01, 0xC0, 0x01, 0xC0, 0x0F, 0xC0, 0x1F, 0xF0, 0x19, 0xFC, 0x1F, 0x8C, 0x0F, 0x80,
// U+307F (み)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xC0, 0x0F, 0xC0, 0x01, 0x80, 0x01, 0x8C, 0x03, 0x8C, 0x1F, 0xEC, 0x3F, 0xFC, 0x76, 0x1E, 0x6E, 0x1E, 0x6C, 0x3A, 0x7C, 0x30, 0x38, 0xE0, 0x00, 0xC0,
// U+3080 (む)
0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x0C, 0x80, 0x7F, 0x98, 0x7F, 0x1C, 0x0C, 0x0E, 0x1C, 0x04, 0x3E, 0x00, 0x66, 0x00, 0x66, 0x00, 0x7C, 0x1C, 0x3C, 0x1C, 0x1C, 0x18, 0x0F, 0xF8, 0x07, 0xF0,
// U+3081 (め)
0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x18, 0x60, 0x18, 0xE0, 0x1F, 0xF8, 0x1E, 0xFC, 0x1C, 0xCC, 0x3D, 0x8E, 0x37, 0x86, 0x67, 0x86, 0x67, 0x0E, 0x67, 0x8C, 0x7F, 0x1C, 0x3C, 0xF8, 0x00, 0xF0,
// U+3089 (ら)
0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0xF0, 0x01, 0xF0, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x1B, 0xF0, 0x1F, 0xF8, 0x1C, 0x1C, 0x18, 0x0C, 0x00, 0x1C, 0x00, 0x38, 0x07, 0xF8, 0x07, 0xE0,
// U+308A (り)
0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x1D, 0xE0, 0x1B, 0xF0, 0x1E, 0x30, 0x1C, 0x18, 0x1C, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x38, 0x00, 0x30, 0x00, 0xE0, 0x07, 0xE0, 0x07, 0x80,
// U+308B (る)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x0F, 0xE0, 0x00, 0xC0, 0x01, 0x80, 0x03, 0xC0, 0x0F, 0xF0, 0x1E, 0x38, 0x38, 0x0C, 0x37, 0x8C, 0x07, 0xCC, 0x0C, 0xF8, 0x07, 0xF8, 0x07, 0xE0,
// U+308C (れ)
0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0C, 0x00, 0x0C, 0x70, 0x7E, 0xF8, 0x7F, 0x98, 0x0F, 0x18, 0x0E, 0x18, 0x1C, 0x18, 0x3C, 0x18, 0x3C, 0x18, 0x6C, 0x18, 0x6C, 0x3B, 0x0C, 0x1F, 0x0C, 0x1C,
// U+308F (わ)
0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x7C, 0xF8, 0x7F, 0xFC, 0x0F, 0x0E, 0x0C, 0x06, 0x1C, 0x06, 0x3C, 0x06, 0x7C, 0x0E, 0x6C, 0x1C, 0x6C, 0x7C, 0x0C, 0xF0, 0x0C, 0x40,
// U+3092 (を)
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x07, 0x70, 0x3F, 0xF0, 0x3F, 0xC0, 0x0C, 0x00, 0x1F, 0x9C, 0x1F, 0xF8, 0x39, 0xE0, 0x33, 0xC0, 0x06, 0xC0, 0x0C, 0xC0, 0x0C, 0x00, 0x0F, 0xF8, 0x07, 0xF8,
// U+3093 (ん)
0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x03, 0x80, 0x03, 0x00, 0x07, 0x00, 0x06, 0x00, 0x0E, 0x00, 0x0F, 0x80, 0x1F, 0xC0, 0x1C, 0xC6, 0x38, 0xC6, 0x30, 0xC6, 0x30, 0xCC, 0x70, 0xFC, 0x60, 0x78,
// U+30A1 (ァ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFC, 0x1F, 0xFC, 0x01, 0x98, 0x01, 0xB0, 0x01, 0xF0, 0x01, 0x80, 0x03, 0x00, 0x03, 0x00, 0x07, 0x00, 0x0E, 0x00,
// U+30A2 (ア)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFE, 0x3F, 0xFE, 0x00, 0x0C, 0x01, 0x98, 0x01, 0xB8, 0x01, 0xF0, 0x01, 0x80, 0x03, 0x00, 0x03, 0x00, 0x07, 0x00, 0x06, 0x00, 0x1E, 0x00, 0x1C, 0x00,
// U+30A3 (ィ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x38, 0x00, 0xF0, 0x01, 0xC0, 0x0F, 0x80, 0x3F, 0x80, 0x39, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
// U+30A4 (イ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x3C, 0x00, 0x78, 0x00, 0xE0, 0x03, 0xC0, 0x1F, 0xC0, 0x7C, 0xC0, 0x30, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0,
// U+30A6 (ウ)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x1C, 0x30, 0x18, 0x00, 0x18, 0x00, 0x38, 0x00, 0x70, 0x01, 0xE0, 0x07, 0xC0, 0x07, 0x00,
// U+30A8 (エ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x00,
// U+30A9 (ォ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x60, 0x1F, 0xFC, 0x1F, 0xFC, 0x00, 0xE0, 0x01, 0xE0, 0x03, 0x60, 0x0F, 0x60, 0x1C, 0x60, 0x18, 0x60, 0x01, 0xE0,
// U+30AA (オ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x60, 0x00, 0x60, 0x00, 0x60, 0x3F, 0xFE, 0x3F, 0xFE, 0x01, 0xE0, 0x01, 0xE0, 0x03, 0x60, 0x0E, 0x60, 0x1C, 0x60, 0x78, 0x60, 0x70, 0x60, 0x03, 0xE0, 0x03, 0xE0,
// U+30AB (カ)
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x03, 0x0C, 0x03, 0x0C, 0x07, 0x0C, 0x06, 0x0C, 0x0E, 0x1C, 0x0C, 0x18, 0x1C, 0x18, 0x38, 0xF8, 0x30, 0xF0,
// U+30AD (キ)
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x18, 0x07, 0xF8, 0x3F, 0xF0, 0x3B, 0x80, 0x01, 0x84, 0x01, 0xFE, 0x3F, 0xFC, 0x3F, 0x80, 0x01, 0xC0, 0x00, 0xC0, 0x00, 0xC0, 0x00, 0xC0,
// U+30AF (ク)
0x00, 0x00, 0x01, 0x00, 0x03, 0x80, 0x03, 0x00, 0x07, 0xFC, 0x0F, 0xFC, 0x0C, 0x18, 0x1C, 0x18, 0x38, 0x38, 0x30, 0x30, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x0F, 0x00, 0x0E, 0x00,
// U+30B3 (コ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x1F, 0xFC, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x00, 0x0C,
// U+30B5 (サ)
0x00, 0x00, 0x00, 0x00, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x00, 0x60, 0x00, 0x60, 0x00, 0xC0, 0x03, 0xC0, 0x03, 0x00,
// U+30B6 (ザ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x0C, 0x3E, 0x0C, 0x3E, 0x0C, 0x30, 0x7F, 0xFE, 0x7F, 0xFC, 0x0C, 0x30, 0x0C, 0x70, 0x0C, 0x60, 0x08, 0x60, 0x00, 0x60, 0x00, 0xC0, 0x03, 0xC0, 0x07, 0x80,
// U+30B7 (シ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x0F, 0x00, 0x07, 0x00, 0x01, 0x00, 0x30, 0x06, 0x3C, 0x0E, 0x0C, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x01, 0xE0, 0x07, 0xC0, 0x3F, 0x00, 0x1C, 0x00,
// U+30B8 (ジ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x0E, 0x36, 0x0F, 0x1A, 0x03, 0x08, 0x20, 0x04, 0x78, 0x0E, 0x1C, 0x0C, 0x08, 0x1C, 0x00, 0x78, 0x00, 0xE0, 0x03, 0xC0, 0x1F, 0x80, 0x3E, 0x00, 0x10, 0x00,
// U+30B9 (ス)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x1F, 0xF8, 0x00, 0x38, 0x00, 0x30, 0x00, 0x70, 0x00, 0xE0, 0x00, 0xC0, 0x01, 0xE0, 0x03, 0xF0, 0x0F, 0x38, 0x1E, 0x1C, 0x78, 0x0E, 0x30, 0x0C,
// U+30BA (ズ)
0x00, 0x00, 0x00, 0x06, 0x00, 0x1E, 0x00, 0x0A, 0x3F, 0xF8, 0x3F, 0xF0, 0x00, 0x30, 0x00, 0x70, 0x00, 0xE0, 0x00, 0xC0, 0x01, 0xC0, 0x03, 0xE0, 0x07, 0x70, 0x1E, 0x38, 0x7C, 0x1C, 0x70, 0x0C,
// U+30BB (セ)
0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x7C, 0x0F, 0xFC, 0x7F, 0xDC, 0x7E, 0x18, 0x0E, 0x30, 0x0E, 0x70, 0x0E, 0x20, 0x0E, 0x00, 0x0E, 0x00, 0x07, 0xFC, 0x07, 0xFC,
// U+30BF (タ)
0x00, 0x00, 0x01, 0x00, 0x03, 0x80, 0x03, 0x00, 0x07, 0xFC, 0x0F, 0xFC, 0x0C, 0x1C, 0x1C, 0x18, 0x7B, 0x38, 0x33, 0xF0, 0x00, 0xF0, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x0F, 0x00, 0x0E, 0x00,
// U+30C0 (ダ)
0x00, 0x00, 0x00, 0x06, 0x03, 0x1E, 0x07, 0x0E, 0x07, 0xF8, 0x0F, 0xFC, 0x0C, 0x18, 0x18, 0x18, 0x3A, 0x38, 0x77, 0xB0, 0x03, 0xF0, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x0F, 0x00, 0x1E, 0x00,
// U+30C1 (チ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x07, 0xF8, 0x1F, 0xF0, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xFC, 0x7F, 0xFC, 0x01, 0x80, 0x01, 0x80, 0x03, 0x80, 0x03, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x0C, 0x00,
// U+30C3 (ッ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x08, 0x19, 0x9C, 0x19, 0x98, 0x1D, 0x98, 0x0C, 0x38, 0x00, 0x30, 0x00, 0x70, 0x00, 0xE0, 0x03, 0xC0, 0x07, 0x80,
// U+30C6 (テ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x3F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x03, 0x80, 0x03, 0x00, 0x0F, 0x00, 0x0E, 0x00,
// U+30C7 (デ)
0x00, 0x00, 0x00, 0x06, 0x00, 0x1B, 0x1F, 0xFF, 0x1F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFC, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x0E, 0x00,
// U+30C8 (ト)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x07, 0x80, 0x07, 0xE0, 0x06, 0xFC, 0x06, 0x3C, 0x06, 0x08, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00, 0x06, 0x00,
// U+30C9 (ド)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x28, 0x0E, 0x3C, 0x0E, 0x34, 0x0E, 0x00, 0x0F, 0x00, 0x0F, 0xE0, 0x0F, 0xF8, 0x0E, 0x38, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0x00,
// U+30CD (ネ)
0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xF8, 0x3F, 0xFC, 0x00, 0x38, 0x00, 0x70, 0x01, 0xE0, 0x03, 0xE0, 0x0F, 0xF8, 0x7D, 0xBC, 0x71, 0x8E, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
// U+30D0 (バ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x1A, 0x04, 0x6E, 0x0E, 0x60, 0x0C, 0x70, 0x0C, 0x30, 0x0C, 0x38, 0x1C, 0x18, 0x18, 0x1C, 0x18, 0x1C, 0x38, 0x0C, 0x70, 0x0E, 0x60, 0x0E, 0x20, 0x04,
// U+30D1 (パ)
0x00, 0x00, 0x00, 0x0E, 0x00, 0x0B, 0x04, 0x6B, 0x0E, 0x6E, 0x0E, 0x70, 0x0C, 0x30, 0x0C, 0x38, 0x0C, 0x18, 0x1C, 0x18, 0x18, 0x1C, 0x38, 0x0C, 0x30, 0x0C, 0x70, 0x0E, 0x60, 0x0E, 0x20, 0x04,
// U+30D5 (フ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF8, 0x3F, 0xFC, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x38, 0x00, 0x30, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xE0, 0x07, 0xC0, 0x0F, 0x00, 0x06, 0x00,
// U+30D6 (ブ)
0x00, 0x00, 0x00, 0x1E, 0x00, 0x1B, 0x3F, 0xF8, 0x3F, 0xFC, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x38, 0x00, 0x30, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x0E, 0x00,
// U+30D7 (プ)
0x00, 0x00, 0x00, 0x06, 0x00, 0x0B, 0x3F, 0xFB, 0x3F, 0xFE, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x18, 0x00, 0x38, 0x00, 0x30, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x07, 0x80, 0x0F, 0x00, 0x0E, 0x00,
// U+30DA (ペ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x34, 0x07, 0x36, 0x0F, 0xB4, 0x0D, 0xDC, 0x18, 0xE0, 0x38, 0x60, 0x70, 0x70, 0x60, 0x38, 0x20, 0x1C, 0x00, 0x0E, 0x00, 0x04, 0x00, 0x00,
// U+30DB (ホ)
0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x3F, 0xFE, 0x01, 0x80, 0x09, 0x88, 0x1D, 0x98, 0x19, 0x8C, 0x31, 0x8E, 0x71, 0x86, 0x21, 0x86, 0x07, 0x80, 0x07, 0x80,
// U+30DC (ボ)
0x00, 0x00, 0x00, 0x04, 0x01, 0x9E, 0x01, 0x9A, 0x01, 0x88, 0x7F, 0xFC, 0x7F, 0xFC, 0x01, 0x80, 0x01, 0x80, 0x19, 0x98, 0x19, 0x9C, 0x31, 0x8C, 0x71, 0x8E, 0x61, 0x84, 0x07, 0x80, 0x07, 0x80,
// U+30DD (ポ)
0x00, 0x00, 0x00, 0x0C, 0x01, 0x9A, 0x01, 0x9A, 0x01, 0x8C, 0x7F, 0xFC, 0x7F, 0xFC, 0x01, 0x80, 0x01, 0x80, 0x19, 0x98, 0x19, 0x9C, 0x31, 0x8C, 0x71, 0x8E, 0x61, 0x84, 0x07, 0x80, 0x07, 0x80,
// U+30DE (マ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x3F, 0xFE, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x38, 0x1C, 0x70, 0x0F, 0xE0, 0x07, 0xC0, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xE0, 0x00, 0x60,
// U+30DF (ミ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x80, 0x0F, 0xF8, 0x00, 0xF8, 0x00, 0x00, 0x0C, 0x00, 0x1F, 0xC0, 0x03, 0xF0, 0x00, 0x30, 0x00, 0x00, 0x1E, 0x00, 0x3F, 0xE0, 0x03, 0xF8, 0x00, 0x30,
// U+30E0 (ム)
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x03, 0x80, 0x03, 0x00, 0x03, 0x00, 0x07, 0x00, 0x06, 0x00, 0x06, 0x30, 0x0E, 0x38, 0x0C, 0x18, 0x0C, 0x1C, 0x1D, 0xFC, 0x7F, 0xFE, 0x7F, 0x06, 0x00, 0x06,
// U+30E1 (メ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x18, 0x08, 0x38, 0x1C, 0x30, 0x0F, 0x60, 0x03, 0xE0, 0x01, 0xE0, 0x01, 0xF0, 0x03, 0xF8, 0x0F, 0x1C, 0x1E, 0x0C, 0x38, 0x00, 0x30, 0x00,
// U+30E2 (モ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x1F, 0xFC, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x3F, 0xFE, 0x3F, 0xFE, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0xFC, 0x01, 0xFC,
// U+30E3 (ャ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x06, 0x1C, 0x07, 0xFC, 0x3F, 0xFC, 0x3F, 0x18, 0x03, 0x30, 0x03, 0x20, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
// U+30E4 (ヤ)
0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x0C, 0x00, 0x0C, 0x0C, 0x0F, 0xFE, 0x7F, 0xFC, 0x7E, 0x1C, 0x46, 0x38, 0x07, 0x70, 0x03, 0x20, 0x03, 0x00, 0x03, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
// U+30E5 (ュ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x0F, 0xF0, 0x00, 0x30, 0x00, 0x30, 0x00, 0x70, 0x00, 0x60, 0x00, 0x60, 0x3F, 0xFC, 0x3F, 0xFC,
// U+30E7 (ョ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x1F, 0xF8, 0x00, 0x18, 0x00, 0x18, 0x0F, 0xF8, 0x0F, 0xF8, 0x00, 0x18, 0x00, 0x18, 0x1F, 0xF8, 0x1F, 0xF8,
// U+30E9 (ラ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x0F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x07, 0xC0, 0x07, 0x00,
// U+30EA (リ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x38, 0x18, 0x38, 0x00, 0x30, 0x00, 0x70, 0x00, 0xE0, 0x03, 0xC0, 0x03, 0x80,
// U+30EB (ル)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0xC0, 0x0C, 0xC0, 0x0C, 0xC0, 0x0C, 0xC0, 0x0C, 0xC0, 0x0C, 0xC0, 0x0C, 0xC2, 0x1C, 0xC6, 0x18, 0xCE, 0x38, 0xFC, 0x30, 0xF8, 0x70, 0xE0, 0x00, 0x40,
// U+30EC (レ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x04, 0x18, 0x0E, 0x18, 0x1C, 0x18, 0x78, 0x19, 0xE0, 0x1F, 0xC0, 0x1F, 0x00, 0x08, 0x00,
// U+30ED (ロ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C,
// U+30EF (ワ)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x1C, 0x30, 0x1C, 0x30, 0x18, 0x00, 0x38, 0x00, 0x30, 0x00, 0xF0, 0x01, 0xE0, 0x07, 0xC0, 0x07, 0x00,
// U+30F3 (ン)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x06, 0x02, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x01, 0xE0, 0x07, 0xC0, 0x3F, 0x00, 0x3C, 0x00, 0x00, 0x00,
// U+30FC (ー)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// U+4E00 (一)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x7F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// U+4E0A (上)
0x00, 0x00, 0x01, 0x00, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0xFC, 0x03, 0xFC, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x03, 0x80, 0x7F, 0xFE, 0x7F, 0xFE,
// U+4E0B (下)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0xE0, 0x01, 0xF0, 0x01, 0xBC, 0x01, 0x8C, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
// U+4E0D (不)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0xC0, 0x01, 0x80, 0x03, 0x80, 0x07, 0xF0, 0x0F, 0xB8, 0x1D, 0x9C, 0x79, 0x8E, 0x61, 0x86, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
// U+4E21 (両)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x61, 0x86, 0x6D, 0xB6, 0x6D, 0xB6, 0x6D, 0xB6, 0x6F, 0xF6, 0x6F, 0xF6, 0x60, 0x06, 0x60, 0x1E,
// U+4E24 (两)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x06, 0x60, 0x06, 0x60, 0x7F, 0xFE, 0x7F, 0xFE, 0x66, 0x66, 0x66, 0x66, 0x67, 0x76, 0x6F, 0xFE, 0x6D, 0xDE, 0x79, 0x8E, 0x78, 0x86, 0x60, 0x3E,
// U+4E2D (中)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x61, 0x86, 0x61, 0x86, 0x61, 0x86, 0x61, 0x86, 0x7F, 0xFE, 0x7F, 0xFE, 0x21, 0x84, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80,
// U+4E3A (为)
0x00, 0x00, 0x03, 0x00, 0x3B, 0x00, 0x1B, 0x00, 0x1B, 0x00, 0x7F, 0xFC, 0x7F, 0xFE, 0x03, 0x06, 0x03, 0x06, 0x06, 0xCE, 0x06, 0xEC, 0x0E, 0x6C, 0x0C, 0x2C, 0x18, 0x0C, 0x38, 0x0C, 0x60, 0x7C,
// U+4E3B (主)
0x00, 0x00, 0x03, 0x00, 0x03, 0x80, 0x01, 0xC0, 0x3F, 0xFC, 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE,
// U+4E49 (义)
0x00, 0x00, 0x01, 0x00, 0x01, 0x88, 0x31, 0x8C, 0x39, 0x9C, 0x18, 0x98, 0x18, 0x18, 0x0C, 0x30, 0x0E, 0x70, 0x07, 0xE0, 0x03, 0xC0, 0x03, 0xC0, 0x07, 0xE0, 0x1E, 0x78, 0x78, 0x1E, 0x60, 0x06,
// U+4E66 (书)
0x00, 0x00, 0x03, 0x00, 0x03, 0x1C, 0x03, 0x0E, 0x3F, 0xFE, 0x3F, 0xF8, 0x03, 0x18, 0x03, 0x18, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0x06, 0x03, 0x06, 0x03, 0x06, 0x03, 0x3E, 0x03, 0x3C, 0x03, 0x00,
// U+4E86 (了)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x7F, 0xFC, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x0F, 0x80,
// U+4E8C (二)
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x00,
// U+4EBA (人)
0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x03, 0x80, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x06, 0x60, 0x06, 0x60, 0x0C, 0x30, 0x1C, 0x38, 0x38, 0x1E, 0x70, 0x0E,
// U+4ECE (从)
0x00, 0x00, 0x00, 0x00, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x1C, 0x70, 0x1C, 0x70, 0x1E, 0x70, 0x1E, 0x70, 0x1B, 0x78, 0x3B, 0xD8, 0x30, 0xDC, 0x31, 0xCC, 0x63, 0x8E, 0x63, 0x06,
// U+4ED6 (他)
0x00, 0x00, 0x08, 0x20, 0x18, 0x20, 0x1B, 0x20, 0x3B, 0x2E, 0x33, 0x7E, 0x73, 0xFE, 0xF7, 0xE6, 0xFF, 0x26, 0x33, 0x26, 0x33, 0x3E, 0x33, 0x3C, 0x33, 0x20, 0x33, 0x03, 0x33, 0x06, 0x33, 0xFE,
// U+4EF6 (件)
0x00, 0x00, 0x08, 0x60, 0x1B, 0x60, 0x1B, 0x60, 0x3B, 0xFC, 0x33, 0xFE, 0x76, 0x60, 0xF6, 0x60, 0x70, 0x60, 0x37, 0xFE, 0x37, 0xFF, 0x30, 0x60, 0x30, 0x60, 0x30, 0x60, 0x30, 0x60, 0x30, 0x60,
// U+4EFB (任)
0x00, 0x00, 0x0C, 0x04, 0x0C, 0x7E, 0x1F, 0xFC, 0x1B, 0xE0, 0x38, 0x60, 0x38, 0x60, 0x78, 0x60, 0xFF, 0xFE, 0x5F, 0xFF, 0x18, 0x60, 0x18, 0x60, 0x18, 0x60, 0x18, 0x60, 0x1B, 0xFE, 0x1F, 0xFE,
// U+4F11 (休)
0x00, 0x00, 0x0C, 0x60, 0x0C, 0x60, 0x1C, 0x60, 0x18, 0x60, 0x3F, 0xFE, 0x7F, 0xFE, 0x78, 0xF0, 0xF8, 0xF8, 0x59, 0xF8, 0x19, 0xFC, 0x1B, 0x6C, 0x1F, 0x66, 0x1E, 0x67, 0x1C, 0x62, 0x18, 0x60,
// U+4F20 (传)
0x00, 0x00, 0x08, 0x40, 0x18, 0xC0, 0x1F, 0xFE, 0x33, 0xFC, 0x30, 0xC0, 0x7F, 0xFE, 0xF7, 0xFE, 0xF1, 0x80, 0x31, 0xFC, 0x33, 0xFE, 0x30, 0x1C, 0x30, 0xB8, 0x31, 0xF0, 0x30, 0x70, 0x30, 0x38,
// U+4F53 (体)
0x00, 0x00, 0x08, 0x60, 0x18, 0x60, 0x18, 0x60, 0x37, 0xFE, 0x37, 0xFE, 0x70, 0xF0, 0x71, 0xF0, 0xF1, 0xF8, 0x31, 0xF8, 0x33, 0x6C, 0x37, 0x6C, 0x37, 0xFE, 0x3F, 0xFF, 0x30, 0x60, 0x30, 0x60,
// U+4F59 (余)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x07, 0xC0, 0x0E, 0x70, 0x1C, 0x38, 0x7F, 0xFE, 0xFF, 0xF6, 0x01, 0x80, 0x3F, 0xFC, 0x7F, 0xFE, 0x01, 0x80, 0x1D, 0xB0, 0x19, 0x98, 0x31, 0x8C, 0x67, 0x8E,
// U+4F5C (作)
0x00, 0x00, 0x0C, 0x80, 0x0C, 0xC0, 0x19, 0x80, 0x19, 0xFF, 0x33, 0xFE, 0x73, 0x60, 0x76, 0x7E, 0x76, 0x7E, 0x30, 0x60, 0x30, 0x60, 0x30, 0x7E, 0x30, 0x7E, 0x30, 0x60, 0x30, 0x60, 0x30, 0x60,
// U+4F9B (供)
0x00, 0x00, 0x09, 0x98, 0x19, 0x98, 0x19, 0x98, 0x3B, 0xFE, 0x37, 0xFE, 0x71, 0x98, 0xF1, 0x98, 0x71, 0x98, 0x31, 0x98, 0x37, 0xFF, 0x37, 0xFE, 0x31, 0x88, 0x31, 0x9C, 0x33, 0x0C, 0x36, 0x06,
// U+4FA7 (侧)
0x00, 0x00, 0x10, 0x06, 0x3F, 0xD6, 0x3C, 0x5E, 0x3D, 0x5E, 0x7D, 0x5E, 0x7D, 0x5E, 0xFD, 0x5E, 0x7D, 0x5E, 0x3D, 0x5E, 0x3D, 0x5E, 0x3F, 0x5E, 0x3F, 0x5E, 0x33, 0x86, 0x33, 0xC6, 0x36, 0xCE,
// U+4FDD (保)
0x00, 0x00, 0x08, 0x00, 0x1F, 0xFE, 0x1B, 0xFE, 0x3B, 0x06, 0x33, 0x06, 0x73, 0xFE, 0xF3, 0xFC, 0x70, 0x60, 0x37, 0xFE, 0x37, 0xFE, 0x30, 0xF0, 0x31, 0xF8, 0x33, 0xEC, 0x3F, 0x6E, 0x3E, 0x62,
// U+5012 (倒)
0x00, 0x00, 0x10, 0x00, 0x18, 0x06, 0x3F, 0xFE, 0x33, 0x1E, 0x36, 0xDE, 0x77, 0xFE, 0xFF, 0xFE, 0xF1, 0x1E, 0x33, 0x1E, 0x3F, 0xFE, 0x33, 0x1E, 0x33, 0x16, 0x33, 0xE6, 0x3F, 0xC6, 0x38, 0x1E,
// U+5074 (側)
0x00, 0x00, 0x18, 0x06, 0x1F, 0xC6, 0x36, 0xD6, 0x36, 0xD6, 0x77, 0xD6, 0x76, 0xD6, 0xF6, 0xD6, 0xF7, 0xD6, 0x36, 0xD6, 0x36, 0xD6, 0x36, 0xD6, 0x37, 0xD6, 0x32, 0x96, 0x36, 0xC6, 0x3E, 0xCE,
// U+5165 (入)
0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x06, 0x00, 0x03, 0x00, 0x01, 0x80, 0x01, 0xC0, 0x03, 0xC0, 0x03, 0xE0, 0x03, 0x60, 0x06, 0x30, 0x0E, 0x30, 0x0C, 0x18, 0x1C, 0x1C, 0x78, 0x0E, 0x70, 0x06,
// U+5173 (关)
0x00, 0x00, 0x08, 0x18, 0x0C, 0x38, 0x0C, 0x30, 0x3F, 0xFC, 0x3F, 0xFC, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0xC0, 0x07, 0x60, 0x0E, 0x70, 0x3C, 0x3C, 0x78, 0x1E,
// U+5185 (内)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x61, 0x86, 0x61, 0x86, 0x63, 0xC6, 0x63, 0xE6, 0x67, 0x76, 0x6E, 0x36, 0x7C, 0x1E, 0x68, 0x06, 0x60, 0x06, 0x60, 0x3E,
// U+518D (再)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x31, 0x8C, 0x3F, 0xFC, 0x3F, 0xFC, 0x31, 0x8C, 0x7F, 0xFE, 0xFF, 0xFF, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x3C,
// U+51D1 (凑)
0x00, 0x00, 0x00, 0x40, 0x60, 0xE0, 0x67, 0xFE, 0x30, 0xC0, 0x37, 0xFC, 0x01, 0x80, 0x0F, 0xFE, 0x03, 0xB8, 0x07, 0x1C, 0x3F, 0xFE, 0x34, 0x42, 0x37, 0xFC, 0x61, 0xF0, 0x63, 0xBC, 0x6F, 0x1E,
// U+51FA (出)
0x00, 0x00, 0x01, 0x80, 0x11, 0x88, 0x31, 0x8C, 0x31, 0x8C, 0x31, 0x8C, 0x31, 0x8C, 0x3F, 0xFC, 0x3F, 0xFC, 0x21, 0x84, 0x61, 0x8E, 0x61, 0x8E, 0x61, 0x8E, 0x61, 0x8E, 0x7F, 0xFE, 0x7F, 0xFE,
// U+5206 (分)
0x00, 0x00, 0x04, 0x20, 0x0E, 0x70, 0x0C, 0x30, 0x1C, 0x18, 0x18, 0x1C, 0x30, 0x0E, 0x7F, 0xFE, 0x7F, 0xFA, 0x03, 0x18, 0x06, 0x18, 0x06, 0x18, 0x06, 0x18, 0x0C, 0x18, 0x3C, 0x38, 0x78, 0xF0,
// U+5207 (切)
0x00, 0x00, 0x00, 0x00, 0x31, 0xFE, 0x33, 0xFE, 0x30, 0x66, 0x3E, 0x66, 0xFE, 0x66, 0xF0, 0x66, 0x30, 0x66, 0x30, 0xC6, 0x32, 0xC6, 0x3E, 0xC6, 0x3D, 0xC6, 0x31, 0x86, 0x03, 0x8E, 0x07, 0x3C,
// U+521B (创)
0x00, 0x00, 0x04, 0x06, 0x0C, 0x06, 0x0E, 0x36, 0x1B, 0x36, 0x33, 0xB6, 0x71, 0xB6, 0xFF, 0xB6, 0x7F, 0x36, 0x33, 0x36, 0x33, 0x36, 0x33, 0x36, 0x37, 0x36, 0x30, 0x86, 0x31, 0x86, 0x3F, 0xBE,
// U+5220 (删)
0x00, 0x00, 0x39, 0xC6, 0x7F, 0xC6, 0x6F, 0xDE, 0x6F, 0xDE, 0x6F, 0xDE, 0x6F, 0xDE, 0x7F, 0xDE, 0xFF, 0xFE, 0x6F, 0xDE, 0x6F, 0xDE, 0x6F, 0xDE, 0x6F, 0xD6, 0x6E, 0xC6, 0x6E, 0xC6, 0x7F, 0xCE,
// U+5230 (到)
0x00, 0x00, 0x00, 0x06, 0x7F, 0xF6, 0x7F, 0xB6, 0x1B, 0x36, 0x33, 0x36, 0x7F, 0xB6, 0x3F, 0xB6, 0x0C, 0x36, 0x0C, 0x36, 0x7F, 0xB6, 0x3F, 0xB6, 0x0C, 0x36, 0x0F, 0x86, 0x7F, 0x86, 0x78, 0x1E,
// U+5237 (刷)
0x00, 0x00, 0x00, 0x06, 0x7F, 0xC6, 0x7F, 0xF6, 0x60, 0xF6, 0x7F, 0xF6, 0x7F, 0xB6, 0x66, 0x36, 0x66, 0x36, 0x7F, 0xF6, 0x7E, 0xF6, 0x7E, 0xF6, 0x7E, 0xF6, 0x7E, 0xC6, 0x7F, 0x86, 0xC6, 0x1E,
// U+524A (削)
0x00, 0x00, 0x0C, 0x06, 0x6D, 0x86, 0x6D, 0xE6, 0x6F, 0x66, 0x2C, 0x66, 0x7F, 0x66, 0x7F, 0x66, 0x63, 0x66, 0x7F, 0x66, 0x63, 0x66, 0x63, 0x66, 0x7F, 0x26, 0x63, 0x06, 0x63, 0x06, 0x67, 0x3E,
// U+524D (前)
0x00, 0x00, 0x18, 0x18, 0x1C, 0x38, 0x0C, 0x30, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x04, 0x3F, 0x6C, 0x23, 0x6C, 0x3F, 0x6C, 0x33, 0x6C, 0x23, 0x6C, 0x3F, 0x6C, 0x33, 0x6C, 0x23, 0x0C, 0x23, 0x1C,
// U+526A (剪)
0x00, 0x00, 0x0C, 0x30, 0x0C, 0x30, 0x7F, 0xFE, 0x00, 0x0C, 0x3F, 0x6C, 0x3F, 0x6C, 0x3F, 0x6C, 0x3F, 0x6C, 0x33, 0x0C, 0x37, 0x1C, 0x00, 0x00, 0x7F, 0xFC, 0x03, 0x0C, 0x06, 0x0C, 0x3E, 0x1C,
// U+52A0 (加)
0x00, 0x00, 0x18, 0x00, 0x18, 0x00, 0x18, 0x7E, 0x7F, 0x7E, 0x7F, 0x66, 0x1B, 0x66, 0x1B, 0x66, 0x1B, 0x66, 0x33, 0x66, 0x33, 0x66, 0x33, 0x66, 0x33, 0x66, 0x33, 0x66, 0x67, 0x7E, 0x7E, 0x7E,
// U+52A8 (动)
0x00, 0x00, 0x00, 0x30, 0x3E, 0x30, 0x7F, 0x30, 0x00, 0x30, 0x00, 0xFE, 0x7F, 0xFE, 0x7F, 0x36, 0x18, 0x36, 0x32, 0x36, 0x32, 0x66, 0x33, 0x66, 0x7F, 0x66, 0x7F, 0xC6, 0x60, 0xC6, 0x01, 0xBC,
// U+52B9 (効)
0x00, 0x00, 0x18, 0x30, 0x0C, 0x30, 0x7F, 0x30, 0x7F, 0xB0, 0x12, 0xFE, 0x33, 0x7E, 0x73, 0x36, 0x67, 0xB6, 0x77, 0x36, 0x1E, 0x36, 0x0C, 0x26, 0x0E, 0x66, 0x1E, 0x66, 0x32, 0xC6, 0x71, 0xDE,
// U+52D5 (動)
0x00, 0x00, 0x0F, 0x30, 0x7F, 0x30, 0x0C, 0x30, 0xFF, 0xB0, 0x0C, 0xFE, 0x7F, 0xFE, 0x6D, 0xB6, 0x7F, 0xB6, 0x6D, 0xB6, 0x7F, 0xE6, 0x0C, 0x66, 0x7F, 0xE6, 0x0C, 0xE6, 0x1F, 0xC6, 0x7F, 0xFC,
// U+5316 (化)
0x00, 0x00, 0x0C, 0xC0, 0x0C, 0xC0, 0x1C, 0xC0, 0x18, 0xC6, 0x38, 0xCE, 0x38, 0xDC, 0x78, 0xF8, 0x78, 0xF0, 0x18, 0xE0, 0x1B, 0xC0, 0x1F, 0xC2, 0x1A, 0xC3, 0x18, 0xC2, 0x18, 0xC6, 0x18, 0xFE,
// U+53C2 (参)
0x00, 0x00, 0x03, 0x00, 0x07, 0x70, 0x1C, 0x38, 0x3F, 0xFC, 0x1F, 0x0C, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x30, 0x1B, 0xD8, 0x7F, 0x0E, 0x64, 0xE6, 0x0F, 0xD8, 0x0E, 0x38, 0x01, 0xF0, 0x1F, 0xC0,
// U+53CD (反)
0x00, 0x00, 0x00, 0x18, 0x1F, 0xFC, 0x3F, 0xF0, 0x30, 0x00, 0x30, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x36, 0x18, 0x33, 0x18, 0x33, 0x30, 0x31, 0xF0, 0x31, 0xE0, 0x31, 0xF0, 0x67, 0xFC, 0x6E, 0x1E,
// U+53D6 (取)
0x00, 0x00, 0x00, 0x00, 0x7F, 0x00, 0x7F, 0xFE, 0x26, 0xFE, 0x36, 0x66, 0x3E, 0x66, 0x26, 0x6C, 0x26, 0x6C, 0x3E, 0x3C, 0x3E, 0x38, 0x26, 0x38, 0x3F, 0xB8, 0x7F, 0x3C, 0x66, 0x6E, 0x06, 0xC6,
// U+53EF (可)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x18, 0x00, 0x18, 0x3F, 0x98, 0x3F, 0x98, 0x31, 0x98, 0x31, 0x98, 0x31, 0x98, 0x3F, 0x98, 0x3F, 0x98, 0x30, 0x18, 0x00, 0x18, 0x00, 0xF8,
// U+53F3 (右)
0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x03, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x0E, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x1F, 0xFC, 0x3F, 0xFC, 0x7C, 0x0C, 0xEC, 0x0C, 0x4C, 0x0C, 0x0F, 0xFC, 0x0F, 0xFC,
// U+53F7 (号)
0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x1F, 0xF8, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x00, 0x1F, 0xF8, 0x1F, 0xF8, 0x00, 0x18, 0x00, 0x18, 0x01, 0xF8,
// U+5411 (向)
0x00, 0x00, 0x01, 0x80, 0x03, 0x80, 0x03, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x60, 0x06, 0x67, 0xE6, 0x67, 0xE6, 0x66, 0x26, 0x66, 0x26, 0x66, 0x26, 0x67, 0xE6, 0x67, 0xE6, 0x64, 0x06, 0x60, 0x3E,
// U+5426 (否)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0x80, 0x07, 0xB0, 0x1F, 0xB8, 0x7D, 0x9E, 0x71, 0x86, 0x41, 0x80, 0x1F, 0xF8, 0x1F, 0xFC, 0x18, 0x1C, 0x18, 0x1C, 0x1F, 0xFC, 0x1F, 0xFC,
// U+542F (启)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x1F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x00, 0x3F, 0xFE, 0x3F, 0xFE, 0x3C, 0x06, 0x7C, 0x06, 0x6F, 0xFE, 0x6F, 0xFE,
// U+5668 (器)
0x00, 0x00, 0x00, 0x00, 0x3E, 0xFC, 0x3E, 0xFC, 0x26, 0xCC, 0x3E, 0xFC, 0x3F, 0x7C, 0x03, 0x38, 0x7F, 0xFE, 0x7F, 0xFE, 0x1C, 0x38, 0x7E, 0x7E, 0x7F, 0xFE, 0x33, 0xCC, 0x33, 0xCC, 0x3F, 0xFC,
// U+56DE (回)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x60, 0x06, 0x67, 0xE6, 0x67, 0xE6, 0x64, 0x66, 0x64, 0x66, 0x64, 0x66, 0x67, 0xE6, 0x67, 0xE6, 0x60, 0x06, 0x60, 0x06, 0x7F, 0xFE, 0x7F, 0xFE,
// U+56F2 (囲)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x66, 0x66, 0x66, 0x66, 0x7F, 0xFE, 0x66, 0x66, 0x66, 0x66, 0x7F, 0xFE, 0x66, 0x66, 0x66, 0x66, 0x6C, 0x66, 0x6C, 0x66, 0x7F, 0xFE, 0x7F, 0xFE,
// U+56F4 (围)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x61, 0x86, 0x7F, 0xFE, 0x61, 0x86, 0x6F, 0xF6, 0x61, 0x86, 0x61, 0x86, 0x7F, 0xFE, 0x61, 0x9E, 0x61, 0xF6, 0x61, 0x86, 0x61, 0x86, 0x7F, 0xFE,
// U+56FD (国)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x6F, 0xF6, 0x6F, 0xF6, 0x61, 0x86, 0x61, 0x86, 0x6F, 0xF6, 0x67, 0xE6, 0x61, 0xA6, 0x61, 0xB6, 0x7F, 0xFE, 0x60, 0x06, 0x7F, 0xFE, 0x7F, 0xFE,
// U+5728 (在)
0x00, 0x00, 0x03, 0x00, 0x07, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x00, 0x0C, 0x60, 0x18, 0x60, 0x38, 0x60, 0x7B, 0xFE, 0xFB, 0xFC, 0x58, 0x60, 0x18, 0x60, 0x18, 0x60, 0x18, 0x60, 0x1F, 0xFE,
// U+5730 (地)
0x00, 0x00, 0x10, 0x30, 0x31, 0x30, 0x31, 0x30, 0x31, 0x3E, 0xFD, 0x3E, 0x7D, 0xF6, 0x37, 0xF6, 0x37, 0xB6, 0x33, 0x36, 0x35, 0x36, 0x3D, 0x3E, 0x79, 0x30, 0x61, 0x03, 0x01, 0x86, 0x01, 0xFE,
// U+5740 (址)
0x00, 0x00, 0x10, 0x30, 0x10, 0x30, 0x10, 0x30, 0x11, 0x30, 0xFF, 0xB0, 0x7D, 0xB0, 0x11, 0xBE, 0x11, 0xBE, 0x11, 0xB0, 0x1D, 0xB0, 0x3F, 0xB0, 0x79, 0xB0, 0x61, 0xB0, 0x07, 0xFE, 0x07, 0xFF,
// U+5907 (备)
0x00, 0x00, 0x06, 0x00, 0x07, 0xF0, 0x0F, 0xF8, 0x3C, 0x30, 0x7E, 0xE0, 0x23, 0xC0, 0x0F, 0xF8, 0xFE, 0x7F, 0x7F, 0xFE, 0x3F, 0xF8, 0x31, 0x98, 0x3F, 0xF8, 0x39, 0x98, 0x31, 0x98, 0x3F, 0xF8,
// U+5916 (外)
0x00, 0x00, 0x18, 0x60, 0x18, 0x60, 0x1F, 0x60, 0x3F, 0xE0, 0x33, 0x60, 0x33, 0x70, 0x63, 0x78, 0x7B, 0x7C, 0x1E, 0x66, 0x0E, 0x66, 0x0E, 0x60, 0x0C, 0x60, 0x18, 0x60, 0x78, 0x60, 0x70, 0x60,
// U+5927 (大)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0xC0, 0x03, 0xC0, 0x07, 0xE0, 0x06, 0x60, 0x0E, 0x70, 0x1C, 0x38, 0x38, 0x1C, 0x70, 0x0E,
// U+592E (央)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x1F, 0xF8, 0x3F, 0xFC, 0x31, 0x8C, 0x31, 0x8C, 0x31, 0x8C, 0x31, 0x8C, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0xC0, 0x07, 0x60, 0x0E, 0x70, 0x3C, 0x3C, 0x78, 0x1F,
// U+5931 (失)
0x00, 0x00, 0x19, 0x80, 0x19, 0x80, 0x19, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x61, 0x80, 0x61, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0xC0, 0x03, 0xC0, 0x07, 0x60, 0x0E, 0x70, 0x3C, 0x3C, 0x78, 0x1F,
// U+59CB (始)
0x00, 0x00, 0x10, 0x60, 0x30, 0x60, 0x30, 0x60, 0x7C, 0xCC, 0xFC, 0xCC, 0x25, 0xFE, 0x6F, 0xFE, 0x6D, 0xC3, 0x6C, 0x00, 0x7D, 0xFE, 0x39, 0xFE, 0x1D, 0x86, 0x3D, 0x86, 0x35, 0xFE, 0x61, 0xFE,
// U+5B57 (字)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x60, 0x06, 0x6F, 0xF6, 0x0F, 0xF0, 0x00, 0xE0, 0x01, 0xC0, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x07, 0x80,
// U+5B58 (存)
0x00, 0x00, 0x02, 0x00, 0x07, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x00, 0x0D, 0xFC, 0x19, 0xFC, 0x38, 0x18, 0x78, 0x30, 0xF8, 0x60, 0x5F, 0xFF, 0x1B, 0xFE, 0x18, 0x60, 0x18, 0x60, 0x19, 0xE0,
// U+5B8C (完)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x60, 0x06, 0x7F, 0xF6, 0x0F, 0xF0, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x06, 0x60, 0x0E, 0x60, 0x0C, 0x62, 0x1C, 0x66, 0x78, 0x7E,
// U+5B9A (定)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x60, 0x06, 0x6F, 0xF6, 0x1F, 0xF8, 0x01, 0x80, 0x19, 0x80, 0x19, 0xF8, 0x19, 0xFC, 0x19, 0x80, 0x3D, 0x80, 0x37, 0x80, 0x63, 0xFE,
// U+5BBD (宽)
0x00, 0x00, 0x01, 0x80, 0x3F, 0xFC, 0x7F, 0xFE, 0x66, 0x66, 0x7F, 0xFE, 0x1F, 0xF8, 0x06, 0x60, 0x1F, 0xF8, 0x1B, 0x98, 0x19, 0x98, 0x19, 0x98, 0x1B, 0xD8, 0x1B, 0xDA, 0x0E, 0xC2, 0x7C, 0xFE,
// U+5BC6 (密)
0x00, 0x00, 0x01, 0x80, 0x3F, 0xFE, 0x7F, 0xFE, 0x63, 0x06, 0x63, 0xB6, 0x35, 0xF8, 0x35, 0xCC, 0x67, 0x9E, 0x6F, 0x32, 0x7F, 0xF0, 0x71, 0x80, 0x31, 0x8C, 0x31, 0x8C, 0x3F, 0xFC, 0x3F, 0xFC,
// U+5BF9 (对)
0x00, 0x00, 0x00, 0x0C, 0x00, 0x0C, 0x7E, 0x0C, 0x7E, 0x0C, 0x47, 0xFF, 0x66, 0xFE, 0x3C, 0x0C, 0x3C, 0xCC, 0x1C, 0xCC, 0x1C, 0x6C, 0x1C, 0x6C, 0x36, 0x0C, 0x76, 0x0C, 0x60, 0x0C, 0x40, 0x7C,
// U+5C01 (封)
0x00, 0x00, 0x08, 0x0C, 0x08, 0x0C, 0x7F, 0x0C, 0x7E, 0x0C, 0x08, 0xFF, 0x7F, 0xFE, 0x7F, 0x0C, 0x08, 0x4C, 0x08, 0x6C, 0x7F, 0x6C, 0x7F, 0x3C, 0x08, 0x2C, 0x08, 0x0C, 0x7F, 0x8C, 0xFF, 0x3C,
// U+5C06 (将)
0x00, 0x00, 0x18, 0x60, 0x18, 0x60, 0x19, 0xFE, 0x7B, 0x8C, 0x7B, 0xDC, 0x38, 0x78, 0x19, 0xF8, 0x1B, 0xCC, 0x1B, 0xFE, 0x3F, 0xFE, 0x79, 0x0C, 0x79, 0x8C, 0x19, 0xCC, 0x18, 0xCC, 0x18, 0x3C,
// U+5C0F (小)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x19, 0x98, 0x19, 0x98, 0x31, 0x8C, 0x31, 0x8C, 0x31, 0x86, 0x71, 0x86, 0x61, 0x86, 0x61, 0x83, 0x01, 0x80, 0x01, 0x80, 0x0F, 0x80,
// U+5C40 (局)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xF8, 0x30, 0x00, 0x3F, 0xFE, 0x3F, 0xFE, 0x30, 0x06, 0x37, 0xE6, 0x66, 0x66, 0x66, 0x66, 0x67, 0xEC, 0xC6, 0x3C,
// U+5C45 (居)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0xC0, 0x3F, 0xFE, 0x3F, 0xFE, 0x30, 0xC0, 0x37, 0xFC, 0x37, 0xFC, 0x64, 0x0C, 0x64, 0x0C, 0x67, 0xFC,
// U+5C4F (屏)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFE, 0x3F, 0xFE, 0x30, 0x0E, 0x3F, 0xFE, 0x32, 0x18, 0x33, 0x18, 0x3F, 0xFE, 0x33, 0x38, 0x33, 0x10, 0x33, 0x38, 0x3F, 0xFE, 0x63, 0x10, 0x66, 0x10, 0xEE, 0x10,
// U+5DE6 (左)
0x00, 0x00, 0x02, 0x00, 0x06, 0x00, 0x06, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x00, 0x0C, 0x00, 0x0F, 0xFC, 0x1F, 0xFE, 0x18, 0x60, 0x18, 0x60, 0x30, 0x60, 0x70, 0x60, 0xE0, 0x60, 0x4F, 0xFE,
// U+5DF2 (已)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xF8, 0x3F, 0xF8, 0x00, 0x18, 0x30, 0x18, 0x30, 0x18, 0x30, 0x18, 0x3F, 0xF8, 0x3F, 0xF8, 0x30, 0x08, 0x30, 0x00, 0x30, 0x06, 0x30, 0x06, 0x30, 0x06, 0x3F, 0xFE,
// U+5E03 (布)
0x00, 0x00, 0x03, 0x00, 0x03, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x00, 0x0C, 0xC0, 0x18, 0xC0, 0x3F, 0xFC, 0x7F, 0xFC, 0xF8, 0xCC, 0x58, 0xCC, 0x18, 0xCC, 0x18, 0xDC, 0x18, 0xFC, 0x00, 0xC0,
// U+5E38 (常)
0x00, 0x00, 0x11, 0x88, 0x19, 0x98, 0x3F, 0xFC, 0x7F, 0xFE, 0x60, 0x06, 0x6F, 0xF6, 0x6C, 0x36, 0x0C, 0x30, 0x0F, 0xF0, 0x01, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x31, 0x8C, 0x31, 0xBC, 0x31, 0xBC,
// U+5E55 (幕)
0x00, 0x00, 0x0C, 0x30, 0x7F, 0xFE, 0x0E, 0x70, 0x3F, 0xFC, 0x30, 0x0C, 0x3F, 0xFC, 0x30, 0x0C, 0x3F, 0xFC, 0x06, 0x00, 0x7F, 0xFE, 0x1D, 0xB8, 0x39, 0x9C, 0x7F, 0xFE, 0x19, 0x98, 0x19, 0xB8,
// U+5E83 (広)
0x00, 0x00, 0x00, 0x80, 0x00, 0xC0, 0x3F, 0xFE, 0x3F, 0xFE, 0x30, 0x00, 0x30, 0xC0, 0x31, 0xC0, 0x31, 0x80, 0x31, 0x80, 0x33, 0x30, 0x33, 0x18, 0x36, 0x1C, 0x66, 0x0C, 0x6F, 0xFE, 0x6F, 0xF6,
// U+5E93 (库)
0x00, 0x00, 0x00, 0x80, 0x00, 0xC0, 0x3F, 0xFE, 0x3F, 0xFE, 0x31, 0x80, 0x3F, 0xFE, 0x3F, 0xFC, 0x33, 0x60, 0x36, 0x60, 0x27, 0xFC, 0x67, 0xFC, 0x60, 0x60, 0x6F, 0xFE, 0x6F, 0xFE, 0xE0, 0x60,
// U+5E94 (应)
0x00, 0x00, 0x01, 0x80, 0x01, 0xC0, 0x3F, 0xFE, 0x3F, 0xFE, 0x30, 0x00, 0x31, 0x8C, 0x3C, 0xCC, 0x3C, 0xCC, 0x36, 0xCC, 0x36, 0xD8, 0x66, 0xD8, 0x62, 0x30, 0x60, 0x30, 0x6F, 0xFE, 0x7F, 0xFF,
// U+5EA6 (度)
0x00, 0x00, 0x00, 0x80, 0x00, 0xC0, 0x3F, 0xFE, 0x3F, 0xFE, 0x33, 0x18, 0x3F, 0xFE, 0x33, 0x18, 0x33, 0x18, 0x33, 0xF8, 0x30, 0x00, 0x2F, 0xFC, 0x67, 0x18, 0x63, 0xB8, 0x61, 0xF0, 0x7F, 0xFE,
// U+5EFA (建)
0x00, 0x00, 0x00, 0x60, 0x7C, 0x60, 0x7B, 0xFC, 0x18, 0x6C, 0x17, 0xFE, 0x30, 0x6C, 0x3F, 0xFC, 0x3C, 0x60, 0x08, 0x60, 0x7B, 0xFE, 0x78, 0x60, 0x3F, 0xFE, 0x38, 0x60, 0x7F, 0x60, 0x67, 0xFE,
// U+5F00 (开)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFE, 0x7F, 0xFE, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x30, 0x0C, 0x30, 0x1C, 0x30, 0x18, 0x30, 0x38, 0x30, 0x70, 0x30,
// U+5F0F (式)
0x00, 0x00, 0x00, 0x68, 0x00, 0x6C, 0x00, 0x64, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x60, 0x00, 0x60, 0x7F, 0xE0, 0x3F, 0x60, 0x0C, 0x60, 0x0C, 0x30, 0x0C, 0x32, 0x0D, 0xB3, 0x3F, 0xBB, 0x7E, 0x1E,
// U+5F15 (引)
0x00, 0x00, 0x00, 0x0C, 0x3F, 0x8C, 0x3F, 0x8C, 0x01, 0x8C, 0x3F, 0x8C, 0x3F, 0x8C, 0x30, 0x0C, 0x30, 0x0C, 0x7F, 0x8C, 0x7F, 0x8C, 0x01, 0x8C, 0x01, 0x8C, 0x01, 0x8C, 0x03, 0x8C, 0x1F, 0x0C,
// U+5F53 (当)
0x00, 0x00, 0x01, 0x80, 0x31, 0x8C, 0x39, 0x8C, 0x19, 0x98, 0x19, 0x98, 0x01, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x00, 0x0C, 0x00, 0x0C, 0x3F, 0xFC, 0x1F, 0xFC, 0x00, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC,
// U+5FD8 (忘)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x18, 0x00, 0x18, 0x00, 0x1F, 0xFC, 0x1F, 0xFC, 0x01, 0x00, 0x01, 0x80, 0x2D, 0xCC, 0x3C, 0xCE, 0x6C, 0x1E, 0x6C, 0x1E, 0x4F, 0xF2,
// U+5FFD (忽)
0x00, 0x00, 0x0C, 0x00, 0x0C, 0x00, 0x1F, 0xFC, 0x3F, 0xFC, 0x76, 0x6C, 0x66, 0xCC, 0x0C, 0xCC, 0x39, 0x8C, 0x33, 0xBC, 0x03, 0x38, 0x2D, 0x80, 0x6D, 0xCC, 0x6C, 0xDE, 0x6C, 0x36, 0x4F, 0xF0,
// U+6001 (态)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0xC0, 0x03, 0x60, 0x06, 0x70, 0x1F, 0x38, 0x79, 0xDE, 0x71, 0xCE, 0x0D, 0x88, 0x3D, 0xCC, 0x2C, 0xDE, 0x6C, 0x1E, 0x6F, 0xF6,
// U+610F (意)
0x00, 0x00, 0x01, 0x80, 0x3F, 0xFC, 0x0C, 0x30, 0x0C, 0x30, 0x7F, 0xFE, 0x00, 0x00, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x03, 0x80, 0x3D, 0xCC, 0x3C, 0xBC, 0x6E, 0x36,
// U+6210 (成)
0x00, 0x00, 0x00, 0xD0, 0x00, 0xDC, 0x00, 0xCC, 0x3F, 0xFE, 0x3F, 0xFE, 0x30, 0x60, 0x3E, 0x6C, 0x3F, 0x6C, 0x33, 0x7C, 0x33, 0x78, 0x33, 0x78, 0x73, 0x72, 0x6E, 0xF2, 0x6D, 0xFE, 0x63, 0x9E,
// U+6216 (或)
0x00, 0x00, 0x00, 0x58, 0x00, 0xFC, 0x00, 0xE4, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x64, 0x3F, 0x6C, 0x33, 0x6C, 0x33, 0x7C, 0x33, 0x78, 0x3F, 0x78, 0x00, 0x32, 0x1F, 0x73, 0x7F, 0xFA, 0x71, 0xDE,
// U+623B (戻)
0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x30, 0xC0, 0x30, 0xC0, 0x3F, 0xFC, 0x3F, 0xFE, 0x61, 0xE0, 0x61, 0xE0, 0x67, 0x38, 0xDE, 0x1E,
// U+624B (手)
0x00, 0x00, 0x00, 0x18, 0x3F, 0xFC, 0x3F, 0xE0, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xFE, 0x3F, 0xFC, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x0F, 0x80,
// U+6253 (打)
0x00, 0x00, 0x18, 0x00, 0x19, 0xFE, 0x1B, 0xFF, 0x7C, 0x18, 0x7E, 0x18, 0x18, 0x18, 0x18, 0x18, 0x1E, 0x18, 0x7E, 0x18, 0x78, 0x18, 0x58, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x78, 0xF8,
// U+626B (扫)
0x00, 0x00, 0x18, 0x00, 0x19, 0xFC, 0x19, 0xFE, 0x7C, 0x06, 0x7E, 0x06, 0x18, 0x06, 0x18, 0x06, 0x1F, 0xFE, 0x7F, 0xFE, 0x78, 0x06, 0x58, 0x06, 0x18, 0x06, 0x18, 0x06, 0x1B, 0xFE, 0x79, 0xFE,
// U+627E (找)
0x00, 0x00, 0x18, 0xC0, 0x18, 0xD8, 0x18, 0xCC, 0x7C, 0xCC, 0x7F, 0xFE, 0x1B, 0xFE, 0x18, 0x64, 0x1C, 0x6C, 0x7E, 0x6C, 0xF8, 0x78, 0x18, 0x78, 0x18, 0x72, 0x18, 0xF3, 0x19, 0xFA, 0x73, 0x9E,
// U+6297 (抗)
0x00, 0x00, 0x18, 0x60, 0x18, 0x60, 0x18, 0x20, 0x7F, 0xFF, 0x7F, 0xFE, 0x18, 0x00, 0x19, 0xF8, 0x1D, 0xF8, 0x7D, 0x98, 0xF9, 0x98, 0x19, 0x98, 0x19, 0x9A, 0x19, 0x9B, 0x1B, 0x1B, 0x77, 0x0E,
// U+629E (択)
0x00, 0x00, 0x18, 0x00, 0x19, 0xFE, 0x19, 0xFE, 0x7D, 0x86, 0x7F, 0x86, 0x19, 0x86, 0x19, 0xFE, 0x1F, 0xFE, 0x7F, 0xB0, 0xF9, 0xB0, 0x59, 0x98, 0x1B, 0x98, 0x1B, 0x1C, 0x1B, 0x0C, 0x7E, 0x07,
// U+62BC (押)
0x00, 0x00, 0x10, 0x00, 0x33, 0xFE, 0x33, 0xFE, 0x7F, 0x36, 0x7F, 0x36, 0x33, 0xFE, 0x33, 0xFE, 0x33, 0x36, 0x3F, 0xFE, 0x7B, 0xFE, 0x73, 0x36, 0x30, 0x30, 0x30, 0x30, 0x30, 0x30, 0x70, 0x30,
// U+62E9 (择)
0x00, 0x00, 0x10, 0x00, 0x33, 0xFE, 0x31, 0xFC, 0x7C, 0xDC, 0x7C, 0x78, 0x30, 0xF8, 0x33, 0xFE, 0x3F, 0xAE, 0x3C, 0x20, 0xFB, 0xFC, 0x71, 0xFC, 0x33, 0xFE, 0x33, 0xFF, 0x30, 0x20, 0x70, 0x20,
// U+6309 (按)
0x00, 0x00, 0x10, 0x60, 0x10, 0x70, 0x13, 0xFE, 0x7F, 0xFE, 0xFF, 0x66, 0x13, 0x62, 0x13, 0xFE, 0x17, 0xFF, 0x3D, 0xCC, 0xFD, 0x8C, 0xF1, 0x98, 0x11, 0xF8, 0x10, 0x78, 0x10, 0xFC, 0x73, 0xCE,
// U+6357 (捗)
0x00, 0x00, 0x30, 0x20, 0x31, 0x20, 0x31, 0x3E, 0x79, 0x30, 0xF9, 0x20, 0x37, 0xFF, 0x33, 0xFE, 0x39, 0xE4, 0x7B, 0x64, 0xF3, 0x6C, 0x36, 0x6C, 0x30, 0x18, 0x30, 0x30, 0x31, 0xE0, 0x73, 0xC0,
// U+6362 (换)
0x00, 0x00, 0x30, 0xC0, 0x30, 0xC8, 0x31, 0xFC, 0x7B, 0x98, 0x7F, 0x18, 0x33, 0xFC, 0x33, 0x6C, 0x33, 0x6C, 0x3F, 0x6C, 0xFF, 0xFE, 0x77, 0xFE, 0x30, 0xF0, 0x31, 0xD8, 0x33, 0x9C, 0x77, 0x06,
// U+63A5 (接)
0x00, 0x00, 0x30, 0x60, 0x30, 0x60, 0x33, 0xFE, 0x79, 0x9C, 0xFD, 0x98, 0x31, 0xF8, 0x37, 0xFE, 0x38, 0x60, 0x7C, 0xE0, 0xF7, 0xFF, 0x31, 0x8C, 0x33, 0x98, 0x33, 0xF8, 0x30, 0xF8, 0x73, 0xFE,
// U+63C3 (揃)
0x00, 0x00, 0x31, 0x0C, 0x31, 0x8C, 0x30, 0x98, 0x7F, 0xFF, 0x7F, 0xFE, 0x30, 0x06, 0x33, 0xDE, 0x3E, 0x5E, 0x7F, 0xDE, 0x72, 0x5E, 0x32, 0x5E, 0x33, 0xDE, 0x32, 0x56, 0x32, 0x46, 0x72, 0xCE,
// U+63CF (描)
0x00, 0x00, 0x30, 0x88, 0x30, 0x88, 0x33, 0xFE, 0x7F, 0xFE, 0xFC, 0x88, 0x30, 0x88, 0x31, 0xFC, 0x3B, 0xFE, 0x7F, 0x26, 0xF3, 0x26, 0x33, 0xFE, 0x33, 0xFE, 0x33, 0x26, 0x33, 0xFE, 0x73, 0xFE,
// U+6557 (敗)
0x00, 0x00, 0x00, 0x60, 0x7E, 0x60, 0x66, 0x60, 0x66, 0xFE, 0x7E, 0xFF, 0x67, 0xCC, 0x67, 0xCC, 0x7F, 0xEC, 0x67, 0x7C, 0x66, 0x38, 0x7E, 0x38, 0x34, 0x38, 0x36, 0x7C, 0x67, 0xEE, 0x63, 0xC7,
// U+6574 (整)
0x00, 0x00, 0x0C, 0x20, 0x7F, 0x60, 0x0C, 0x7E, 0x7F, 0xEC, 0x6F, 0xEC, 0x7F, 0x38, 0x1E, 0x38, 0x7F, 0xFE, 0x68, 0xC6, 0x3F, 0xFC, 0x01, 0x80, 0x19, 0x80, 0x19, 0xF8, 0x19, 0x80, 0x7F, 0xFE,
// U+6587 (文)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x38, 0x0C, 0x30, 0x0E, 0x70, 0x06, 0x60, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x0F, 0xF0, 0x3C, 0x3C, 0x78, 0x1E,
// U+65B0 (新)
0x00, 0x00, 0x18, 0x04, 0x18, 0x7E, 0x7F, 0x78, 0x37, 0x40, 0x36, 0x40, 0x36, 0xC0, 0x7F, 0xFF, 0x08, 0x7E, 0x1C, 0xCC, 0x7F, 0xCC, 0x2E, 0xCC, 0x6F, 0xCC, 0x6B, 0xCC, 0x49, 0xCC, 0x39, 0x8C,
// U+65B9 (方)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x06, 0x00, 0x06, 0x00, 0x07, 0xFC, 0x07, 0xFC, 0x0C, 0x0C, 0x0C, 0x18, 0x1C, 0x18, 0x18, 0x18, 0x38, 0x18, 0x71, 0xF8,
// U+65E0 (无)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0xC0, 0x03, 0xC0, 0x06, 0xC0, 0x0E, 0xC2, 0x1C, 0xC3, 0x38, 0xC6, 0x70, 0xFE,
// U+65E2 (既)
0x00, 0x00, 0x00, 0x00, 0x7E, 0xFE, 0x7E, 0xFE, 0x66, 0x58, 0x7E, 0xD8, 0x66, 0xD0, 0x66, 0xD0, 0x7F, 0xFE, 0x7E, 0xFE, 0x64, 0x38, 0x66, 0x78, 0x6E, 0x78, 0x7F, 0xFA, 0x7B, 0xDA, 0x61, 0x9E,
// U+65E5 (日)
0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC,
// U+65F6 (时)
0x00, 0x00, 0x00, 0x0C, 0x7C, 0x0C, 0x7E, 0x0C, 0x67, 0xFE, 0x67, 0xFF, 0x66, 0x0C, 0x7E, 0x0C, 0x7F, 0xCC, 0x66, 0xCC, 0x66, 0x6C, 0x66, 0x6C, 0x7E, 0x0C, 0x7C, 0x0C, 0x60, 0x0C, 0x00, 0x7C,
// U+662F (是)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x30, 0x0C, 0x3F, 0xFC, 0x30, 0x0C, 0x3F, 0xFC, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x09, 0x80, 0x19, 0xFC, 0x19, 0xFC, 0x3D, 0x80, 0x3F, 0x80, 0x67, 0xFE,
// U+663E (显)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x38, 0x1C, 0x38, 0x1C, 0x3F, 0xFC, 0x30, 0x0C, 0x38, 0x1C, 0x3F, 0xFC, 0x02, 0x40, 0x36, 0x6C, 0x36, 0x6C, 0x1E, 0x78, 0x1E, 0x78, 0x7F, 0xFE, 0xFF, 0xFF,
// U+6642 (時)
0x00, 0x00, 0x00, 0x30, 0x7C, 0x30, 0x7D, 0xFE, 0x6D, 0xFC, 0x6C, 0x30, 0x6F, 0xFE, 0x7F, 0xFF, 0x7C, 0x0C, 0x6D, 0xFE, 0x6F, 0xFE, 0x6D, 0x8C, 0x7D, 0xCC, 0x7C, 0xCC, 0x60, 0xCC, 0x00, 0x3C,
// U+666E (普)
0x00, 0x00, 0x0C, 0x30, 0x0C, 0x30, 0x7F, 0xFE, 0x37, 0xEC, 0x36, 0x48, 0x1E, 0x58, 0x1F, 0xF8, 0x7F, 0xFE, 0x00, 0x00, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x18, 0x18, 0x18, 0x18, 0x1F, 0xF8,
// U+6697 (暗)
0x00, 0x00, 0x00, 0x20, 0x78, 0x30, 0x7F, 0xFE, 0x6D, 0xFC, 0x6C, 0x8C, 0x6C, 0xCC, 0x7F, 0xFF, 0x7F, 0xFE, 0x6D, 0xFE, 0x6D, 0x8E, 0x6D, 0x86, 0x7D, 0xFE, 0x7D, 0x86, 0x61, 0x8E, 0x01, 0xFE,
// U+66F4 (更)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x3F, 0xFC, 0x31, 0x8C, 0x31, 0x8C, 0x3F, 0xFC, 0x31, 0x8C, 0x3F, 0xFC, 0x1B, 0x80, 0x1F, 0x00, 0x0F, 0x00, 0x1F, 0xE0, 0x7D, 0xFF,
// U+66F8 (書)
0x00, 0x00, 0x01, 0x80, 0x3F, 0xF8, 0x01, 0x98, 0x7F, 0xFE, 0x01, 0x98, 0x3F, 0xF8, 0x01, 0x80, 0x3F, 0xFC, 0x01, 0x80, 0x7F, 0xFE, 0x1F, 0xF8, 0x1F, 0xFC, 0x1F, 0xFC, 0x18, 0x0C, 0x1F, 0xFC,
// U+66FF (替)
0x00, 0x00, 0x08, 0x30, 0x1C, 0x30, 0x7E, 0xFE, 0x18, 0x30, 0x7F, 0xFE, 0x1C, 0x78, 0x3E, 0x6C, 0x77, 0xEE, 0x77, 0xCE, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x18, 0x18, 0x18, 0x18, 0x1F, 0xF8,
// U+6700 (最)
0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x10, 0x18, 0x1F, 0xF8, 0x10, 0x18, 0x1F, 0xF8, 0x00, 0x00, 0x7F, 0xFE, 0x33, 0x00, 0x3F, 0xFE, 0x33, 0x6C, 0x3F, 0x6C, 0x33, 0x38, 0x7F, 0xBC, 0x7F, 0xFE,
// U+6709 (有)
0x00, 0x00, 0x02, 0x00, 0x07, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x00, 0x0F, 0xF8, 0x1F, 0xF8, 0x3C, 0x18, 0x7C, 0x18, 0x6F, 0xF8, 0x0C, 0x18, 0x0F, 0xF8, 0x0C, 0x18, 0x0C, 0x18, 0x0C, 0x78,
// U+672A (未)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x07, 0xE0, 0x0F, 0xF0, 0x1D, 0xB8, 0x39, 0x9C, 0x71, 0x8E, 0x61, 0x86,
// U+672B (末)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x07, 0xE0, 0x0F, 0xF0, 0x1D, 0xB8, 0x39, 0x9C, 0x71, 0x8E, 0x61, 0x86,
// U+672C (本)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x07, 0xE0, 0x0F, 0xF0, 0x0D, 0xB0, 0x1D, 0xB8, 0x19, 0x98, 0x31, 0x8C, 0x7F, 0xFE, 0x4F, 0xF6, 0x01, 0x80, 0x01, 0x80,
// U+673A (机)
0x00, 0x00, 0x18, 0x00, 0x19, 0xF8, 0x19, 0xF8, 0x7F, 0x98, 0x7F, 0x98, 0x19, 0x98, 0x3D, 0x98, 0x3F, 0x98, 0x3F, 0x98, 0x7B, 0x98, 0x79, 0x98, 0x59, 0x9A, 0x1B, 0x9B, 0x1B, 0x1F, 0x1F, 0x0E,
// U+6761 (条)
0x00, 0x00, 0x06, 0x00, 0x07, 0x10, 0x0F, 0xF8, 0x1C, 0x30, 0x77, 0xE0, 0x23, 0xC0, 0x0F, 0xF0, 0x7E, 0x7E, 0x79, 0x9E, 0x1F, 0xF8, 0x3F, 0xFC, 0x01, 0x80, 0x1D, 0xB0, 0x39, 0x98, 0x73, 0x8C,
// U+677E (松)
0x00, 0x00, 0x18, 0x08, 0x18, 0xD8, 0x18, 0xD8, 0x7D, 0xCC, 0xFF, 0x8C, 0x19, 0xA6, 0x3B, 0x66, 0x3F, 0x62, 0x3E, 0x60, 0x7E, 0xC8, 0x78, 0xCC, 0xD8, 0xCC, 0x59, 0x8C, 0x1B, 0xFE, 0x1B, 0xFE,
// U+67E5 (查)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x0F, 0xF0, 0x39, 0xBC, 0x71, 0x8E, 0x7F, 0xFE, 0x18, 0x18, 0x18, 0x18, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x00, 0x00, 0x7F, 0xFE,
// U+680F (栏)
0x00, 0x00, 0x18, 0x8C, 0x19, 0xCC, 0x18, 0xCC, 0x7C, 0xD8, 0x7F, 0xFE, 0x19, 0xFE, 0x38, 0x00, 0x3C, 0x00, 0x3D, 0xFC, 0x7D, 0xFC, 0x78, 0x00, 0x58, 0x00, 0x18, 0x00, 0x1B, 0xFE, 0x1B, 0xFE,
// U+68C0 (检)
0x00, 0x00, 0x10, 0x60, 0x10, 0x60, 0x10, 0xF0, 0x79, 0xD8, 0x7F, 0x8C, 0x37, 0x8E, 0x3F, 0xFA, 0x3C, 0x00, 0x7F, 0x66, 0x73, 0x64, 0xF1, 0x2C, 0x51, 0xAC, 0x11, 0x18, 0x13, 0xFE, 0x17, 0xFE,
// U+6A21 (模)
0x00, 0x00, 0x10, 0xD8, 0x30, 0xD8, 0x33, 0xFE, 0x78, 0xD8, 0x7F, 0xFE, 0x3B, 0x06, 0x3B, 0xFE, 0x3F, 0x06, 0x7F, 0x06, 0x7F, 0xFE, 0x70, 0x60, 0xF7, 0xFE, 0x30, 0xF8, 0x31, 0xDC, 0x37, 0x8E,
// U+6A2A (横)
0x00, 0x00, 0x30, 0xC8, 0x30, 0xDC, 0x33, 0xFE, 0x78, 0xD8, 0x7C, 0xDC, 0x37, 0xFE, 0x38, 0x60, 0x3B, 0xFE, 0x7F, 0x26, 0x77, 0xFE, 0xF3, 0x26, 0xF3, 0xFE, 0x30, 0x88, 0x31, 0xDC, 0x37, 0x8E,
// U+6B21 (次)
0x00, 0x00, 0x01, 0x80, 0x41, 0x80, 0x73, 0x80, 0x3B, 0xFE, 0x13, 0xFE, 0x06, 0x66, 0x06, 0x6C, 0x06, 0x6C, 0x18, 0xE0, 0x18, 0xE0, 0x30, 0xF0, 0x71, 0xF0, 0x63, 0x98, 0x67, 0x1E, 0x0E, 0x0E,
// U+6B63 (正)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x19, 0x80, 0x19, 0xFC, 0x19, 0xFC, 0x19, 0x80, 0x19, 0x80, 0x19, 0x80, 0x19, 0x80, 0x7F, 0xFE, 0x7F, 0xFE,
// U+6B64 (此)
0x00, 0x00, 0x06, 0x60, 0x06, 0x60, 0x06, 0x60, 0x06, 0x60, 0x36, 0x64, 0x37, 0x6E, 0x37, 0xFC, 0x36, 0x70, 0x36, 0x60, 0x36, 0x60, 0x36, 0x60, 0x36, 0x62, 0x36, 0x63, 0x3F, 0xE3, 0x7F, 0x7E,
// U+6BB5 (段)
0x00, 0x00, 0x06, 0x00, 0x3E, 0xF8, 0x38, 0xF8, 0x30, 0xC8, 0x3E, 0xC8, 0x3C, 0xCC, 0x31, 0x8E, 0x3D, 0x80, 0x3F, 0xFC, 0x30, 0xCC, 0x32, 0xCC, 0x7E, 0x78, 0xF8, 0x78, 0x30, 0xFC, 0x33, 0xCE,
// U+6BD4 (比)
0x00, 0x00, 0x30, 0xC0, 0x30, 0xC0, 0x30, 0xC0, 0x30, 0xC4, 0x30, 0xCE, 0x3E, 0xDC, 0x3F, 0xF8, 0x30, 0xF0, 0x30, 0xC0, 0x30, 0xC0, 0x30, 0xC6, 0x30, 0xC6, 0x36, 0xC6, 0x3E, 0xC6, 0x7C, 0xFE,
// U+6CD5 (法)
0x00, 0x00, 0x20, 0x60, 0x38, 0x60, 0x18, 0x60, 0x03, 0xFE, 0x03, 0xFC, 0x60, 0x60, 0x70, 0x60, 0x17, 0xFE, 0x03, 0xFE, 0x08, 0xC0, 0x18, 0xD8, 0x19, 0x9C, 0x31, 0x8C, 0x33, 0xFE, 0x63, 0xF6,
// U+6D45 (浅)
0x00, 0x00, 0x20, 0xD0, 0x30, 0xF8, 0x18, 0xD8, 0x00, 0xFE, 0x07, 0xFE, 0x67, 0xC0, 0x70, 0xC2, 0x31, 0xFE, 0x07, 0xFE, 0x16, 0xE6, 0x18, 0x6C, 0x30, 0x78, 0x30, 0x72, 0x71, 0xF2, 0x67, 0xBE,
// U+6D4F (浏)
0x00, 0x00, 0x02, 0x06, 0x63, 0x06, 0x33, 0x16, 0x1F, 0xF6, 0x07, 0xF6, 0x41, 0x96, 0x67, 0x96, 0x37, 0x96, 0x03, 0x96, 0x03, 0x96, 0x33, 0x96, 0x33, 0x96, 0x36, 0xD6, 0x6E, 0xC6, 0x6C, 0x0E,
// U+6D88 (消)
0x00, 0x00, 0x20, 0x60, 0x73, 0x66, 0x3B, 0x66, 0x03, 0xEC, 0x01, 0x60, 0x63, 0xFE, 0x73, 0xFE, 0x13, 0x06, 0x03, 0xFE, 0x13, 0xFE, 0x1B, 0x06, 0x3B, 0xFE, 0x33, 0xFE, 0x73, 0x06, 0x63, 0x1C,
// U+6DF1 (深)
0x00, 0x00, 0x20, 0x00, 0x77, 0xFE, 0x1F, 0xFE, 0x06, 0x96, 0x01, 0x98, 0x63, 0x0C, 0x77, 0x0E, 0x36, 0x64, 0x07, 0xFE, 0x07, 0xFE, 0x18, 0xF0, 0x31, 0xF8, 0x33, 0x7C, 0x6F, 0x6E, 0x64, 0x66,
// U+6E08 (済)
0x00, 0x00, 0x00, 0x40, 0x70, 0x60, 0x3F, 0xFE, 0x17, 0xFE, 0x01, 0xB8, 0x40, 0xF0, 0x77, 0xFE, 0x37, 0x0E, 0x03, 0x0C, 0x13, 0xFC, 0x13, 0x0C, 0x33, 0x0C, 0x33, 0xFC, 0x66, 0x0C, 0x6E, 0x0C,
// U+6E90 (源)
0x00, 0x00, 0x20, 0x00, 0x7F, 0xFE, 0x1F, 0xFE, 0x06, 0x30, 0x07, 0xFE, 0x67, 0x86, 0x77, 0x86, 0x37, 0xFE, 0x07, 0x86, 0x17, 0xFE, 0x1E, 0x30, 0x36, 0xBC, 0x3D, 0xB6, 0x6D, 0xB6, 0x7F, 0x32,
// U+70B9 (点)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x01, 0xFE, 0x01, 0xFC, 0x01, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x00, 0x00, 0x36, 0x4C, 0x36, 0x6E, 0x66, 0x66,
// U+70ED (热)
0x00, 0x00, 0x18, 0x40, 0x18, 0x60, 0x7C, 0xE0, 0x7F, 0xFC, 0x18, 0xCC, 0x1E, 0xCC, 0x7F, 0xCC, 0x79, 0xCC, 0x18, 0xFE, 0x19, 0xBF, 0x7B, 0x0E, 0x32, 0x00, 0x36, 0x4C, 0x36, 0x6E, 0x66, 0x66,
// U+7121 (無)
0x00, 0x00, 0x08, 0x00, 0x18, 0x00, 0x3F, 0xFE, 0x7F, 0xFC, 0x7A, 0xD8, 0x1A, 0xD8, 0x7F, 0xFE, 0x3F, 0xFC, 0x1A, 0xD8, 0x1A, 0xD8, 0x7F, 0xFE, 0x7F, 0xFE, 0x36, 0x4C, 0x36, 0x6E, 0x66, 0x66,
// U+7248 (版)
0x00, 0x00, 0x2C, 0x0E, 0x6C, 0xFE, 0x6D, 0xF0, 0x6D, 0x80, 0x7F, 0x80, 0x7F, 0xFE, 0x61, 0xFE, 0x61, 0xE6, 0x7D, 0xE6, 0x7D, 0xAC, 0x65, 0xBC, 0x65, 0xB8, 0x65, 0x98, 0x67, 0xBC, 0xC7, 0xEE,
// U+7279 (特)
0x00, 0x00, 0x18, 0x30, 0x18, 0x30, 0x79, 0xFE, 0x7D, 0xFC, 0x7C, 0x30, 0x5B, 0xFF, 0x5B, 0xFE, 0x58, 0x0C, 0x1F, 0xFE, 0x7F, 0xFE, 0x78, 0x8C, 0x59, 0xCC, 0x18, 0xCC, 0x18, 0x4C, 0x18, 0x3C,
// U+72B6 (状)
0x00, 0x00, 0x08, 0x60, 0x0C, 0x6C, 0x4C, 0x6C, 0x6C, 0x66, 0x7C, 0x60, 0x3F, 0xFE, 0x0F, 0xFE, 0x0C, 0x60, 0x1C, 0x70, 0x3C, 0xF0, 0x7C, 0xF8, 0x6C, 0xD8, 0x0D, 0x9C, 0x0F, 0x8C, 0x0F, 0x07,
// U+72ED (狭)
0x00, 0x00, 0x48, 0x20, 0x6C, 0x60, 0x3B, 0xFE, 0x3B, 0xFE, 0x79, 0x64, 0x79, 0xE6, 0x19, 0xEC, 0x19, 0xEC, 0x3F, 0xFE, 0x7F, 0xFE, 0xEC, 0x70, 0x48, 0xF8, 0x08, 0xD8, 0x19, 0x8C, 0x7F, 0x0E,
// U+7387 (率)
0x00, 0x00, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0x00, 0x77, 0x6C, 0x3F, 0xD8, 0x01, 0xC0, 0x1B, 0x78, 0x7F, 0xFE, 0x67, 0xB6, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x01, 0x80,
// U+73B0 (现)
0x00, 0x00, 0x00, 0x00, 0x7D, 0xFC, 0x7F, 0xFC, 0x19, 0xA4, 0x19, 0xA4, 0x19, 0xA4, 0x7D, 0xA4, 0x7D, 0xE4, 0x19, 0xE4, 0x19, 0x74, 0x18, 0x70, 0x1E, 0xF2, 0x7C, 0xF2, 0x63, 0xB2, 0x07, 0x3E,
// U+73FE (現)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7D, 0x86, 0x19, 0x86, 0x19, 0xFE, 0x19, 0x86, 0x7D, 0xFE, 0x7D, 0x86, 0x19, 0x86, 0x19, 0xFE, 0x1E, 0xD8, 0x7E, 0xD8, 0xF9, 0xD8, 0x41, 0x9B, 0x07, 0x1F,
// U+7528 (用)
0x00, 0x00, 0x00, 0x00, 0x1F, 0xFC, 0x3F, 0xFC, 0x31, 0x8C, 0x31, 0x8C, 0x3F, 0xFC, 0x3F, 0xFC, 0x31, 0x8C, 0x31, 0x8C, 0x3F, 0xFC, 0x3F, 0xFC, 0x31, 0x8C, 0x71, 0x8C, 0x61, 0x8C, 0xE1, 0xBC,
// U+7535 (电)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xFC, 0x3F, 0xFC, 0x31, 0x8C, 0x31, 0x8C, 0x3F, 0xFC, 0x3F, 0xFC, 0x31, 0x8C, 0x31, 0x8C, 0x3F, 0xFC, 0x3F, 0xFE, 0x21, 0x83, 0x01, 0x86, 0x01, 0xFE,
// U+753B (画)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFE, 0x7F, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x6D, 0xB6, 0x6D, 0xB6, 0x6F, 0xF6, 0x6D, 0xB6, 0x6D, 0xB6, 0x6F, 0xF6, 0x60, 0x06, 0x7F, 0xFE, 0x7F, 0xFE,
// U+7565 (略)
0x00, 0x00, 0x00, 0x60, 0x3C, 0x60, 0x7E, 0xFE, 0x7E, 0xFE, 0x7F, 0xEC, 0x7F, 0x7C, 0x7E, 0x38, 0x7E, 0xFC, 0x7F, 0xEF, 0x7F, 0xFF, 0x7E, 0xFE, 0x7E, 0xC6, 0x7E, 0xC6, 0x60, 0xFE, 0x00, 0xFE,
// U+767D (白)
0x00, 0x00, 0x01, 0x80, 0x03, 0x80, 0x03, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC,
// U+767E (百)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x03, 0x80, 0x1F, 0xFC, 0x1F, 0xFC, 0x18, 0x0C, 0x18, 0x0C, 0x1F, 0xFC, 0x1F, 0xFC, 0x18, 0x0C, 0x18, 0x0C, 0x1F, 0xFC, 0x1F, 0xFC,
// U+7684 (的)
0x00, 0x00, 0x18, 0x60, 0x18, 0x60, 0x18, 0x60, 0x7E, 0xFE, 0x7E, 0xFE, 0x67, 0x86, 0x67, 0x86, 0x7E, 0x66, 0x7E, 0x66, 0x66, 0x36, 0x66, 0x36, 0x66, 0x06, 0x7E, 0x06, 0x7E, 0x0C, 0x60, 0x7C,
// U+76EE (目)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC,
// U+7720 (眠)
0x00, 0x00, 0x00, 0x00, 0x7D, 0xFE, 0x7D, 0xFE, 0x6D, 0x06, 0x6D, 0xFE, 0x7D, 0xFE, 0x6D, 0x30, 0x6D, 0x30, 0x7D, 0xFE, 0x7D, 0xFE, 0x6D, 0x18, 0x7D, 0x18, 0x7D, 0x1B, 0x63, 0xFF, 0x43, 0xEE,
// U+77ED (短)
0x00, 0x00, 0x20, 0x00, 0x21, 0xFE, 0x7D, 0xFE, 0x7E, 0x00, 0x79, 0xFE, 0x59, 0xFE, 0x19, 0x86, 0xFF, 0x86, 0x7F, 0xFE, 0x18, 0xFC, 0x1C, 0xCC, 0x3C, 0xCC, 0x36, 0xCC, 0x64, 0x58, 0x63, 0xFF,
// U+7801 (码)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFC, 0x7D, 0xFC, 0x30, 0x8C, 0x31, 0x8C, 0x31, 0x8C, 0x7D, 0x8C, 0x7D, 0xFE, 0xED, 0xFE, 0x6C, 0x06, 0x6C, 0x06, 0x2F, 0xFE, 0x3D, 0xF6, 0x3C, 0x06, 0x20, 0x1C,
// U+786E (确)
0x00, 0x00, 0x00, 0xC0, 0x7E, 0xC0, 0x7D, 0xFC, 0x31, 0x98, 0x33, 0xB8, 0x33, 0xFE, 0x7D, 0xB6, 0x7D, 0xB6, 0xED, 0xFE, 0x6D, 0xB6, 0x6D, 0xB6, 0x2D, 0xFE, 0x3F, 0x36, 0x3F, 0x36, 0x26, 0x3E,
// U+78BA (確)
0x00, 0x00, 0x00, 0x30, 0x7C, 0x30, 0x7F, 0xFE, 0x33, 0xFE, 0x33, 0xFE, 0x30, 0xD8, 0x3D, 0xFE, 0x7F, 0x98, 0x6F, 0xFE, 0xED, 0x98, 0x6D, 0x98, 0x2D, 0xFE, 0x3D, 0x98, 0x3D, 0x98, 0x21, 0xFE,
// U+793A (示)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x3F, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x19, 0x90, 0x19, 0x98, 0x39, 0x9C, 0x31, 0x8C, 0x61, 0x8E, 0x61, 0x86, 0x0F, 0x80,
// U+7981 (禁)
0x00, 0x00, 0x08, 0x10, 0x1C, 0x38, 0x7F, 0xFE, 0x1C, 0x38, 0x3E, 0x7C, 0x7B, 0xF6, 0x49, 0xB6, 0x18, 0x30, 0x1F, 0xFC, 0x00, 0x00, 0x7F, 0xFE, 0x3F, 0xFE, 0x19, 0x98, 0x39, 0x9C, 0x77, 0x8E,
// U+7A7A (空)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x66, 0x66, 0x6E, 0x76, 0x3C, 0x3C, 0x78, 0x0E, 0x3F, 0xFC, 0x1F, 0xF8, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE,
// U+7AD6 (竖)
0x00, 0x00, 0x04, 0x00, 0x25, 0xFC, 0x65, 0xFC, 0x64, 0xDC, 0x64, 0xF8, 0x64, 0x70, 0x64, 0xF8, 0x67, 0xFE, 0x25, 0x8E, 0x01, 0x80, 0x7F, 0xFE, 0x3F, 0xFC, 0x0C, 0x30, 0x0C, 0x30, 0x7F, 0xFE,
// U+7AE0 (章)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x3F, 0xFC, 0x0C, 0x30, 0x7F, 0xFE, 0x00, 0x00, 0x3F, 0xFC, 0x30, 0x0C, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x01, 0x80, 0x7F, 0xFE, 0x01, 0x80,
// U+7AEF (端)
0x00, 0x00, 0x10, 0x20, 0x33, 0x36, 0x1B, 0x36, 0x7F, 0xFE, 0x7F, 0xFE, 0x4C, 0x00, 0x6F, 0xFE, 0x6F, 0xFE, 0x6C, 0x60, 0x6B, 0xFE, 0x2B, 0xFE, 0x3F, 0x5E, 0x7F, 0x5E, 0x73, 0x5E, 0x03, 0x5E,
// U+7BC4 (範)
0x00, 0x00, 0x10, 0x60, 0x38, 0xE0, 0x7F, 0xFE, 0x6D, 0x98, 0x49, 0x10, 0x7F, 0xFE, 0x0C, 0xFE, 0x7F, 0xC6, 0x6B, 0xC6, 0x7F, 0xC6, 0x6B, 0xDE, 0x7F, 0xC8, 0x08, 0xC2, 0x7F, 0xC3, 0x08, 0x7E,
// U+7C4D (籍)
0x00, 0x00, 0x18, 0x60, 0x38, 0x60, 0x3F, 0xFE, 0x6D, 0xD8, 0x7C, 0xEC, 0x7F, 0xFE, 0x3C, 0x6C, 0x7E, 0x6C, 0x19, 0xFE, 0x7E, 0x00, 0x1C, 0xFC, 0x3E, 0xC4, 0x7E, 0xFC, 0x7A, 0xC4, 0x58, 0xFC,
// U+7D22 (索)
0x00, 0x00, 0x01, 0x80, 0x3F, 0xFC, 0x7F, 0xFE, 0x01, 0x80, 0x7F, 0xFE, 0x67, 0x86, 0x67, 0x26, 0x7C, 0x76, 0x1F, 0xD0, 0x0F, 0x18, 0x3F, 0xFC, 0x3F, 0x86, 0x0D, 0xB0, 0x19, 0x9C, 0x77, 0x8E,
// U+7D27 (紧)
0x00, 0x00, 0x0D, 0xFC, 0x2D, 0xFC, 0x6C, 0xCC, 0x6C, 0x78, 0x6C, 0x70, 0x6D, 0xFC, 0x0F, 0xDE, 0x0E, 0x60, 0x1F, 0xE0, 0x1F, 0x98, 0x1F, 0x1C, 0x3F, 0xFE, 0x3F, 0x86, 0x1D, 0xB8, 0x79, 0x9C,
// U+7D42 (終)
0x00, 0x00, 0x18, 0x60, 0x18, 0xE0, 0x30, 0xFE, 0x3D, 0xFC, 0x6F, 0xDC, 0xFB, 0x78, 0x78, 0x70, 0x3C, 0xF8, 0x7F, 0xCE, 0x7F, 0xE6, 0x44, 0x78, 0x7C, 0x18, 0x7E, 0xC0, 0x5E, 0xF0, 0xD8, 0x3C,
// U+7D9A (続)
0x00, 0x00, 0x10, 0x30, 0x10, 0x30, 0x33, 0xFE, 0x2C, 0x30, 0x7D, 0xFE, 0xF8, 0x00, 0x78, 0x00, 0x3F, 0xFE, 0x7F, 0x02, 0x7F, 0x5A, 0x64, 0xD8, 0x7C, 0xD8, 0x7C, 0xDB, 0x7F, 0x9B, 0x5B, 0x9E,
// U+7E26 (縦)
0x00, 0x00, 0x11, 0xA6, 0x31, 0xE6, 0x33, 0x34, 0x2E, 0x3C, 0x69, 0xFE, 0xF9, 0xFE, 0x73, 0x18, 0x3F, 0x78, 0x7F, 0x7E, 0x7D, 0x7E, 0x45, 0x78, 0x5D, 0x78, 0x55, 0x78, 0x55, 0xF8, 0xD1, 0xDF,
// U+7EBF (线)
0x00, 0x00, 0x18, 0x68, 0x18, 0x7C, 0x18, 0x64, 0x30, 0x7E, 0x37, 0xFE, 0x7D, 0xE0, 0x7C, 0x66, 0x58, 0xFE, 0x33, 0xF8, 0x7F, 0x36, 0x7C, 0x3E, 0x60, 0x3C, 0x0E, 0x38, 0x7C, 0xFA, 0x73, 0xDE,
// U+7EC8 (终)
0x00, 0x00, 0x18, 0x60, 0x18, 0xE0, 0x30, 0xFE, 0x31, 0xFC, 0x67, 0xDC, 0x6F, 0x78, 0xF8, 0x70, 0x58, 0xFC, 0x33, 0xCE, 0x7F, 0x66, 0x7C, 0x78, 0x40, 0x18, 0x0D, 0xC0, 0xFE, 0xF8, 0x70, 0x3C,
// U+7EDC (络)
0x00, 0x00, 0x10, 0x40, 0x18, 0xC0, 0x38, 0xFE, 0x31, 0xFC, 0x77, 0xCC, 0x7F, 0xF8, 0xFC, 0x70, 0x78, 0xFC, 0x33, 0xCE, 0x7F, 0xFE, 0x7D, 0xFC, 0x41, 0x84, 0x0D, 0x84, 0x7D, 0xFC, 0x71, 0xFC,
// U+7EE7 (继)
0x00, 0x00, 0x10, 0x10, 0x1B, 0x12, 0x33, 0xD6, 0x33, 0x56, 0x67, 0x5C, 0x6F, 0x5C, 0xFB, 0xFE, 0x7B, 0x38, 0x33, 0x3C, 0x7F, 0x7E, 0x7F, 0xD6, 0x43, 0xD0, 0x0F, 0x10, 0x7F, 0xFE, 0x63, 0xFF,
// U+7EED (续)
0x00, 0x00, 0x18, 0x20, 0x18, 0x70, 0x31, 0xFC, 0x30, 0x20, 0x6F, 0xFE, 0x7D, 0xFE, 0x78, 0xF6, 0x11, 0xF4, 0x3F, 0xB0, 0x7C, 0xB0, 0x73, 0xFE, 0x07, 0xFE, 0x3C, 0xF8, 0x71, 0xCC, 0x43, 0x86,
// U+7EF4 (维)
0x00, 0x00, 0x18, 0xD0, 0x18, 0xD8, 0x30, 0xD8, 0x35, 0xFE, 0x6F, 0xFE, 0x7F, 0x98, 0xFB, 0xFC, 0x5B, 0xFE, 0x31, 0x98, 0x7D, 0xFC, 0x79, 0xFE, 0x01, 0x98, 0x1F, 0x98, 0x7D, 0xFF, 0x61, 0xFE,
// U+7F51 (网)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x63, 0x16, 0x63, 0x36, 0x7B, 0xF6, 0x6E, 0xF6, 0x6E, 0x76, 0x66, 0x76, 0x67, 0x76, 0x6F, 0xFE, 0x7D, 0xDE, 0x79, 0x86, 0x60, 0x86, 0x60, 0x3E,
// U+7F6E (置)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFE, 0x36, 0x66, 0x3F, 0xFE, 0x01, 0x80, 0x7F, 0xFE, 0x03, 0x80, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x1F, 0xF8, 0x18, 0x18, 0x7F, 0xFE,
// U+7FFB (翻)
0x00, 0x00, 0x0E, 0x00, 0x7E, 0xFE, 0x6A, 0x3E, 0x2E, 0x36, 0x7F, 0xBE, 0x1C, 0xFE, 0x3F, 0xFE, 0x7B, 0x76, 0x48, 0x36, 0x7F, 0x7E, 0x6B, 0xFE, 0x7F, 0xBE, 0x6B, 0x36, 0x6B, 0x36, 0x7F, 0x7E,
// U+81EA (自)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x1F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x0C, 0x3F, 0xFC, 0x3F, 0xFC,
// U+8272 (色)
0x00, 0x00, 0x06, 0x00, 0x0F, 0xE0, 0x0F, 0xF0, 0x18, 0x60, 0x38, 0xC0, 0x7F, 0xFC, 0x7F, 0xFC, 0x31, 0x8C, 0x31, 0x8C, 0x3F, 0xFC, 0x3F, 0xFC, 0x30, 0x02, 0x30, 0x03, 0x38, 0x06, 0x1F, 0xFE,
// U+8282 (节)
0x00, 0x00, 0x0C, 0x30, 0x0C, 0x30, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x30, 0x00, 0x00, 0x7F, 0xFC, 0x3F, 0xFC, 0x06, 0x0C, 0x06, 0x0C, 0x06, 0x0C, 0x06, 0x0C, 0x06, 0x7C, 0x06, 0x38, 0x06, 0x00,
// U+8303 (范)
0x00, 0x00, 0x0C, 0x30, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x30, 0x24, 0x20, 0x3B, 0xFC, 0x1F, 0xFC, 0x03, 0x0C, 0x73, 0x0C, 0x3B, 0x3C, 0x0F, 0x3C, 0x0F, 0x00, 0x1B, 0x02, 0x33, 0x86, 0x71, 0xFE,
// U+843D (落)
0x00, 0x00, 0x0C, 0x30, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0xB0, 0x31, 0xC0, 0x3B, 0xFC, 0x0F, 0x9C, 0x67, 0xF8, 0x74, 0xF8, 0x33, 0xFE, 0x07, 0x8E, 0x1B, 0xFC, 0x3B, 0x0C, 0x33, 0x0C, 0x63, 0xFC,
// U+8535 (蔵)
0x00, 0x00, 0x0C, 0x30, 0x7F, 0xFE, 0x0E, 0x7C, 0x0C, 0x2C, 0x00, 0x7C, 0x7F, 0xFE, 0x60, 0x30, 0x7F, 0xF4, 0x7B, 0x3C, 0x7F, 0xBC, 0x78, 0xB8, 0x7F, 0xB8, 0x7B, 0x3A, 0x7F, 0xFB, 0x60, 0xEF,
// U+85CF (藏)
0x00, 0x00, 0x0C, 0x30, 0x7F, 0xFE, 0x0E, 0x7E, 0x04, 0x36, 0x7F, 0xFE, 0x7F, 0xFE, 0x78, 0x30, 0x7F, 0xF6, 0x1E, 0x96, 0x1F, 0xFC, 0xFE, 0x5C, 0x7F, 0xDC, 0x7E, 0x9A, 0x5F, 0xFB, 0x50, 0x7E,
// U+884C (行)
0x00, 0x00, 0x0C, 0x00, 0x19, 0xFE, 0x31, 0xFE, 0x60, 0x00, 0x4C, 0x00, 0x1C, 0x00, 0x1B, 0xFE, 0x3B, 0xFE, 0x78, 0x18, 0x78, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x78,
// U+88C1 (裁)
0x00, 0x00, 0x0C, 0x60, 0x3F, 0x6C, 0x7F, 0xEE, 0x0C, 0x64, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0x60, 0x0E, 0x26, 0x7F, 0xBC, 0x19, 0x3C, 0x3F, 0xB8, 0x7F, 0x3A, 0x53, 0xFB, 0x1E, 0xFA, 0x3C, 0xCE,
// U+898B (見)
0x00, 0x00, 0x1F, 0xF8, 0x1F, 0xF8, 0x18, 0x18, 0x18, 0x18, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x18, 0x18, 0x18, 0x18, 0x1F, 0xF8, 0x1F, 0xF8, 0x0E, 0x60, 0x0C, 0x62, 0x3C, 0x66, 0x78, 0x7E,
// U+8996 (視)
0x00, 0x00, 0x10, 0x00, 0x19, 0xFE, 0x19, 0x86, 0x7F, 0x86, 0x7D, 0xFE, 0x0D, 0x86, 0x19, 0xFE, 0x3D, 0x86, 0x7D, 0x86, 0xFF, 0xFE, 0x58, 0xF8, 0x18, 0xD8, 0x18, 0xD8, 0x18, 0xDB, 0x1B, 0x9F,
// U+89A7 (覧)
0x00, 0x00, 0x00, 0x60, 0x7F, 0x60, 0x7F, 0x7E, 0x7F, 0xD0, 0x7F, 0xD8, 0x6C, 0x9C, 0x7F, 0x0C, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x06, 0x62, 0x1C, 0x66,
// U+89C8 (览)
0x00, 0x00, 0x06, 0x60, 0x26, 0x60, 0x26, 0xFE, 0x26, 0xFE, 0x27, 0xD8, 0x27, 0x98, 0x06, 0x08, 0x1F, 0xF8, 0x1F, 0xF8, 0x19, 0x98, 0x19, 0x98, 0x19, 0xD8, 0x13, 0xC0, 0x0E, 0xC6, 0x7C, 0xFE,
// U+8A00 (言)
0x00, 0x00, 0x01, 0x00, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xF8, 0x18, 0x18, 0x18, 0x18, 0x1F, 0xF8,
// U+8A08 (計)
0x00, 0x00, 0x10, 0x10, 0x18, 0x30, 0x08, 0x30, 0x7F, 0x30, 0x00, 0x30, 0x7E, 0x30, 0x01, 0xFF, 0x00, 0xFE, 0x7E, 0x30, 0x00, 0x30, 0x3E, 0x30, 0x26, 0x30, 0x26, 0x30, 0x36, 0x30, 0x3E, 0x30,
// U+8A2D (設)
0x00, 0x00, 0x10, 0x00, 0x18, 0xF8, 0x18, 0xF8, 0x7E, 0xC8, 0x01, 0x88, 0x7F, 0x8E, 0x03, 0x00, 0x01, 0xFC, 0x7F, 0xFE, 0x01, 0x8C, 0x7C, 0xCC, 0x64, 0xF8, 0x64, 0x70, 0x6C, 0xF8, 0x7F, 0xFE,
// U+8A66 (試)
0x00, 0x00, 0x30, 0x1C, 0x30, 0x1E, 0x10, 0x1E, 0x7D, 0xFE, 0x03, 0xFF, 0x7C, 0x18, 0x00, 0x18, 0x03, 0xF8, 0x7D, 0xD8, 0x00, 0x98, 0x7C, 0x98, 0x6C, 0x98, 0x6D, 0xFA, 0x6F, 0xEF, 0x7E, 0x0E,
// U+8A8D (認)
0x00, 0x00, 0x30, 0x00, 0x31, 0xFE, 0x19, 0xB6, 0xFF, 0xB6, 0x01, 0xE6, 0x7D, 0x66, 0x00, 0xC6, 0x01, 0xDC, 0x7D, 0xA0, 0x00, 0x74, 0x7D, 0xF6, 0x67, 0xD6, 0x67, 0xCE, 0x6F, 0xCE, 0x7E, 0xFC,
// U+8A9E (語)
0x00, 0x00, 0x10, 0x00, 0x19, 0xFE, 0x18, 0x60, 0xFE, 0x60, 0x01, 0xFC, 0x7C, 0x4C, 0x00, 0xCC, 0x7F, 0xFF, 0x7C, 0x00, 0x00, 0xFC, 0x7D, 0xFE, 0x65, 0x86, 0x65, 0x86, 0x6D, 0xFE, 0x7D, 0xFE,
// U+8AAD (読)
0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x13, 0xFE, 0xFC, 0x30, 0x01, 0xFE, 0x7C, 0x00, 0x00, 0x00, 0x03, 0xFE, 0x7F, 0x02, 0x03, 0xDA, 0x7C, 0xD8, 0x6C, 0xD8, 0x6C, 0xDA, 0x6D, 0xDB, 0x7F, 0x9E,
// U+8BA4 (认)
0x00, 0x00, 0x00, 0x20, 0x30, 0x30, 0x18, 0x20, 0x08, 0x60, 0x00, 0x60, 0x78, 0x60, 0x78, 0x70, 0x18, 0x70, 0x18, 0x70, 0x18, 0x70, 0x18, 0x78, 0x1E, 0xD8, 0x1E, 0xCC, 0x1D, 0x8E, 0x33, 0x86,
// U+8BB0 (记)
0x00, 0x00, 0x00, 0x00, 0x31, 0xFC, 0x3B, 0xFE, 0x18, 0x06, 0x00, 0x06, 0x78, 0x06, 0x78, 0x06, 0x19, 0xFE, 0x19, 0xFE, 0x19, 0x80, 0x19, 0x80, 0x19, 0x82, 0x1F, 0x83, 0x1D, 0x86, 0x39, 0xFE,
// U+8BBE (设)
0x00, 0x00, 0x00, 0x00, 0x31, 0xF8, 0x19, 0xF8, 0x09, 0x98, 0x01, 0x98, 0x73, 0x0E, 0xF3, 0x00, 0x13, 0xFC, 0x13, 0xFC, 0x11, 0x8C, 0x11, 0x9C, 0x1C, 0xF8, 0x1C, 0x70, 0x39, 0xF8, 0x37, 0xDE,
// U+8BD5 (试)
0x00, 0x00, 0x00, 0x34, 0x30, 0x3E, 0x18, 0x36, 0x0B, 0xFE, 0x07, 0xFF, 0x70, 0x18, 0x78, 0x18, 0x1B, 0xD8, 0x1B, 0xF8, 0x19, 0x98, 0x19, 0x98, 0x19, 0x98, 0x1D, 0xFB, 0x3F, 0xFF, 0x33, 0x0E,
// U+8BED (语)
0x00, 0x00, 0x23, 0xFC, 0x37, 0xFE, 0x38, 0xC0, 0x19, 0xF8, 0x03, 0xFC, 0x70, 0xCC, 0x70, 0xCC, 0x17, 0xFF, 0x17, 0xFE, 0x11, 0xFC, 0x13, 0xFE, 0x17, 0x06, 0x1F, 0x06, 0x3B, 0xFE, 0x33, 0xFE,
// U+8BEF (误)
0x00, 0x00, 0x00, 0x00, 0x33, 0xFE, 0x3B, 0xFE, 0x1B, 0x06, 0x03, 0xFE, 0x73, 0xFE, 0xF8, 0x00, 0x1B, 0xFE, 0x19, 0xFC, 0x18, 0x60, 0x1B, 0xFF, 0x1F, 0xFE, 0x1C, 0xF8, 0x3D, 0xDC, 0x37, 0x8E,
// U+8BFB (读)
0x00, 0x00, 0x00, 0x60, 0x30, 0x70, 0x3B, 0xFE, 0x18, 0x60, 0x03, 0xFE, 0x73, 0xFE, 0xF1, 0xB6, 0x10, 0xF4, 0x13, 0x30, 0x11, 0xB0, 0x17, 0xFE, 0x1F, 0xFE, 0x1C, 0xF8, 0x39, 0xCC, 0x37, 0x86,
// U+8D25 (败)
0x00, 0x00, 0x00, 0x20, 0x7F, 0x60, 0x7F, 0x60, 0x6B, 0x7E, 0x7B, 0xFE, 0x7B, 0xCC, 0x7B, 0xCC, 0x7B, 0xCC, 0x7B, 0x6C, 0x7B, 0x78, 0x7B, 0x38, 0x18, 0x38, 0x3E, 0x38, 0x36, 0x7C, 0x63, 0xC6,
// U+8D77 (起)
0x00, 0x00, 0x08, 0x00, 0x08, 0xFE, 0x7F, 0x7E, 0x7E, 0x06, 0x08, 0x06, 0x7F, 0x7E, 0x7F, 0xFE, 0x0C, 0xC0, 0x6C, 0xC0, 0x6F, 0xC2, 0x6F, 0xC6, 0x7C, 0x7E, 0x7C, 0x3C, 0x7C, 0x00, 0xCF, 0xFF,
// U+8D85 (超)
0x00, 0x00, 0x18, 0x00, 0x18, 0xFE, 0x7F, 0xFE, 0x3E, 0x66, 0x18, 0x66, 0x7E, 0xDE, 0x7F, 0xC0, 0x08, 0x80, 0x68, 0xFE, 0x6F, 0xC6, 0x6E, 0xC6, 0x78, 0xFE, 0x78, 0x00, 0x7C, 0x00, 0xCF, 0xFF,
// U+8DDD (距)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x67, 0x80, 0x67, 0x80, 0x7F, 0xFC, 0x3D, 0xFC, 0x19, 0x84, 0x7F, 0x84, 0x7F, 0xFC, 0x79, 0xFC, 0x79, 0x80, 0x7F, 0x80, 0x7F, 0xFE, 0x71, 0xFF,
// U+8DF3 (跳)
0x00, 0x00, 0x00, 0xD8, 0x7C, 0xD8, 0x7F, 0xDA, 0x67, 0xDE, 0x65, 0xDE, 0x7D, 0xDC, 0x18, 0xD8, 0x78, 0xD8, 0x7C, 0xDC, 0x7F, 0xDE, 0x7B, 0xDA, 0x78, 0xD8, 0x7F, 0x9A, 0xFD, 0x9B, 0x43, 0x1E,
// U+8EE2 (転)
0x00, 0x00, 0x18, 0x00, 0x18, 0x7E, 0x7F, 0xFE, 0x7E, 0x00, 0x18, 0x00, 0x7F, 0x00, 0x7B, 0xFF, 0x7F, 0xFE, 0x7B, 0x30, 0x7F, 0x60, 0x18, 0x6C, 0x7E, 0x6C, 0xFF, 0xC6, 0x19, 0xFE, 0x19, 0xFE,
// U+8F6C (转)
0x00, 0x00, 0x18, 0x30, 0x18, 0x30, 0xFF, 0xFE, 0x7E, 0xFE, 0x38, 0x60, 0x39, 0xFF, 0x69, 0xFE, 0x7C, 0xE0, 0x7E, 0xFC, 0x08, 0xFE, 0x0E, 0x0C, 0x7E, 0x58, 0x7C, 0xF8, 0x08, 0x38, 0x08, 0x1C,
// U+8F7D (载)
0x00, 0x00, 0x0C, 0x20, 0x0C, 0x6C, 0x7F, 0xEE, 0x0C, 0x64, 0x7F, 0xFE, 0x1C, 0x70, 0x18, 0x36, 0x7F, 0xFC, 0x36, 0x3C, 0x36, 0x3C, 0x7F, 0xB8, 0x06, 0x38, 0x07, 0xFB, 0x7F, 0xFA, 0x76, 0xEE,
// U+8F93 (输)
0x00, 0x00, 0x30, 0x30, 0x30, 0x70, 0x7C, 0xF8, 0x7F, 0x8E, 0x23, 0xFE, 0x78, 0x00, 0x78, 0x0E, 0x7F, 0xFE, 0x7F, 0x7E, 0x1B, 0xFE, 0x1F, 0x7E, 0x7F, 0xFE, 0x7B, 0x7E, 0x1B, 0x6E, 0x1B, 0x66,
// U+8FB9 (边)
0x00, 0x00, 0x20, 0x40, 0x70, 0xC0, 0x38, 0xC0, 0x1F, 0xFE, 0x03, 0xFE, 0x00, 0xC6, 0xF0, 0xC6, 0x70, 0xC6, 0x31, 0xC6, 0x31, 0x86, 0x33, 0x8E, 0x37, 0x3C, 0x3E, 0x3C, 0x7E, 0x00, 0xEF, 0xFF,
// U+8FBC (込)
0x00, 0x00, 0x20, 0x00, 0x71, 0x80, 0x38, 0xC0, 0x18, 0x60, 0x00, 0x60, 0x00, 0xF0, 0x70, 0xF0, 0x78, 0xF0, 0x19, 0x98, 0x19, 0x98, 0x1B, 0x0C, 0x1F, 0x0E, 0x3A, 0x02, 0x7E, 0x02, 0x67, 0xFE,
// U+8FD4 (返)
0x00, 0x00, 0x00, 0x04, 0x61, 0xFE, 0x73, 0xFC, 0x3B, 0x00, 0x13, 0xFC, 0x03, 0xFC, 0x73, 0x0C, 0x73, 0xCC, 0x33, 0xF8, 0x36, 0x78, 0x36, 0x78, 0x3F, 0xEC, 0x3D, 0x84, 0x7E, 0x02, 0xEF, 0xFE,
// U+8FDB (进)
0x00, 0x00, 0x20, 0x98, 0x71, 0x98, 0x39, 0x98, 0x1F, 0xFE, 0x03, 0xFE, 0x01, 0x98, 0x71, 0x98, 0x7F, 0xFE, 0x1F, 0xFE, 0x19, 0x98, 0x19, 0x98, 0x1B, 0x18, 0x3B, 0x18, 0x7E, 0x02, 0x67, 0xFE,
// U+8FDE (连)
0x00, 0x00, 0x20, 0xC0, 0x70, 0xC0, 0x37, 0xFE, 0x1B, 0xFC, 0x03, 0x30, 0x03, 0x30, 0x77, 0xFE, 0x73, 0xFC, 0x30, 0x30, 0x37, 0xFE, 0x37, 0xFE, 0x30, 0x30, 0x38, 0x30, 0x7E, 0x30, 0xEF, 0xFF,
// U+8FFD (追)
0x00, 0x00, 0x00, 0x60, 0x60, 0x60, 0x33, 0xFC, 0x1B, 0x0C, 0x03, 0x0C, 0x03, 0xFC, 0x7B, 0x00, 0x7B, 0x00, 0x1B, 0xFE, 0x1B, 0x0E, 0x1B, 0x06, 0x1B, 0x0E, 0x1B, 0xFE, 0x7C, 0x00, 0x67, 0xFF,
// U+9000 (退)
0x00, 0x00, 0x00, 0x00, 0x63, 0xFC, 0x33, 0x0C, 0x3B, 0x0C, 0x13, 0xFC, 0x03, 0x0C, 0x7B, 0xFC, 0x7B, 0x6E, 0x1B, 0x7E, 0x1B, 0x38, 0x1B, 0x5C, 0x1B, 0xCE, 0x3B, 0x04, 0x7E, 0x00, 0x67, 0xFE,
// U+9001 (送)
0x00, 0x00, 0x21, 0x0C, 0x71, 0x9C, 0x31, 0x98, 0x1F, 0xFE, 0x03, 0xFC, 0x00, 0x60, 0x70, 0x60, 0x77, 0xFE, 0x37, 0xFE, 0x30, 0xF0, 0x31, 0xD8, 0x33, 0x8C, 0x3F, 0x04, 0x7E, 0x02, 0xEF, 0xFF,
// U+9002 (适)
0x00, 0x00, 0x00, 0x0C, 0x63, 0xFC, 0x73, 0xF8, 0x30, 0x60, 0x07, 0xFE, 0x07, 0xFE, 0xF0, 0x60, 0x70, 0x60, 0x33, 0xFC, 0x33, 0x0C, 0x33, 0x0C, 0x33, 0xFC, 0x38, 0x00, 0x7E, 0x02, 0x67, 0xFE,
// U+9006 (逆)
0x00, 0x00, 0x03, 0x0C, 0x63, 0x9C, 0x71, 0x98, 0x37, 0xFE, 0x07, 0xFE, 0x06, 0x66, 0x76, 0x66, 0x76, 0x66, 0x17, 0xFE, 0x17, 0xFE, 0x10, 0x60, 0x10, 0xC0, 0x3B, 0x80, 0x7F, 0x00, 0x67, 0xFE,
// U+9009 (选)
0x00, 0x00, 0x00, 0x60, 0x61, 0x60, 0x73, 0xF0, 0x3B, 0xFE, 0x16, 0x60, 0x00, 0x60, 0x77, 0xFE, 0x77, 0xFE, 0x11, 0x90, 0x11, 0x90, 0x13, 0x93, 0x17, 0x1E, 0x1E, 0x1E, 0x3E, 0x00, 0x67, 0xFE,
// U+901A (通)
0x00, 0x00, 0x00, 0x00, 0x63, 0xFE, 0x70, 0x9C, 0x39, 0xF8, 0x18, 0xF0, 0x03, 0xFE, 0x73, 0x66, 0xFB, 0xFE, 0x1A, 0x66, 0x1B, 0xFE, 0x1B, 0x66, 0x1A, 0x66, 0x3A, 0x7E, 0x7E, 0x0E, 0x67, 0xFE,
// U+9032 (進)
0x00, 0x00, 0x01, 0x80, 0x61, 0xB0, 0x33, 0x30, 0x3F, 0xFE, 0x07, 0xFC, 0x0F, 0x30, 0x7F, 0xFE, 0x73, 0x30, 0x13, 0x30, 0x13, 0xFE, 0x13, 0x30, 0x13, 0xFE, 0x3B, 0xFE, 0x7E, 0x00, 0x67, 0xFE,
// U+9078 (選)
0x00, 0x00, 0x00, 0x00, 0x67, 0xFE, 0x36, 0xF6, 0x1F, 0xFE, 0x06, 0x72, 0x07, 0xFE, 0x71, 0x98, 0x77, 0xFE, 0x11, 0x98, 0x11, 0x98, 0x1F, 0xFE, 0x11, 0x98, 0x3F, 0x0E, 0x7E, 0x06, 0x67, 0xFE,
// U+90E8 (部)
0x00, 0x00, 0x0C, 0x00, 0x0C, 0x3E, 0x7F, 0xBE, 0x7F, 0xA6, 0x33, 0x2C, 0x13, 0x2C, 0x7F, 0xAC, 0x7F, 0xEC, 0x00, 0x26, 0x3F, 0x26, 0x7F, 0xA6, 0x61, 0xA6, 0x61, 0xBE, 0x7F, 0xB8, 0x7F, 0xA0,
// U+914D (配)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0x7E, 0x14, 0x06, 0x7F, 0x06, 0x7F, 0x06, 0x77, 0x7E, 0x77, 0x7E, 0x77, 0x46, 0x63, 0x40, 0x63, 0x40, 0x7F, 0x42, 0x63, 0x43, 0x7F, 0x62, 0x63, 0x7E,
// U+91CD (重)
0x00, 0x00, 0x01, 0xF8, 0x3F, 0xF8, 0x01, 0x80, 0x7F, 0xFE, 0x01, 0x80, 0x3F, 0xFC, 0x31, 0x8C, 0x3F, 0xFC, 0x31, 0x8C, 0x31, 0x8C, 0x3F, 0xFC, 0x01, 0x80, 0x3F, 0xFC, 0x01, 0x80, 0x7F, 0xFE,
// U+91CF (量)
0x00, 0x00, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x18, 0x18, 0x1F, 0xF8, 0x7F, 0xFE, 0x7F, 0xFE, 0x3F, 0xFC, 0x31, 0x8C, 0x3F, 0xFC, 0x3F, 0xFC, 0x1F, 0xFC, 0x3F, 0xFC, 0x3F, 0xFC, 0x7F, 0xFE,
// U+9488 (针)
0x00, 0x00, 0x10, 0x30, 0x30, 0x30, 0x7E, 0x30, 0x7E, 0x30, 0xC0, 0x30, 0x7E, 0x30, 0x3F, 0xFE, 0x19, 0xFE, 0x7C, 0x30, 0x7E, 0x30, 0x18, 0x30, 0x18, 0x30, 0x1A, 0x30, 0x1E, 0x30, 0x3C, 0x30,
// U+949F (钟)
0x00, 0x00, 0x30, 0x30, 0x30, 0x30, 0x3E, 0x30, 0x7F, 0xFE, 0xC1, 0xFE, 0x7F, 0xB6, 0x3D, 0xB6, 0x19, 0xB6, 0x7D, 0xB6, 0x7F, 0xFE, 0x19, 0xFE, 0x19, 0x32, 0x1A, 0x30, 0x1E, 0x30, 0x3C, 0x30,
// U+94AE (钮)
0x00, 0x00, 0x30, 0x00, 0x31, 0xFE, 0x7F, 0xFE, 0x7C, 0x66, 0x40, 0x64, 0x7C, 0x64, 0x3C, 0x6C, 0x19, 0xFC, 0x7D, 0xFC, 0x7E, 0xCC, 0x18, 0xCC, 0x18, 0xCC, 0x1E, 0xCC, 0x1E, 0xCC, 0x3B, 0xFF,
// U+9519 (错)
0x00, 0x00, 0x30, 0xC8, 0x30, 0xC8, 0x7F, 0xFE, 0x7D, 0xFE, 0x40, 0xC8, 0x7F, 0xFE, 0x3F, 0xFF, 0x18, 0x00, 0x7D, 0xFE, 0x7F, 0x8E, 0x19, 0x86, 0x19, 0xFE, 0x1D, 0x8E, 0x1F, 0x8E, 0x39, 0xFE,
// U+952E (键)
0x00, 0x00, 0x20, 0x18, 0x20, 0x18, 0x7F, 0xFE, 0x79, 0x9E, 0xC3, 0x7F, 0x7B, 0x1E, 0x3B, 0xFE, 0x31, 0x98, 0x31, 0xFE, 0x7F, 0x98, 0x33, 0x98, 0x33, 0x7E, 0x3F, 0x98, 0x3F, 0xF8, 0x36, 0x7F,
// U+952F (锯)
0x00, 0x00, 0x10, 0x00, 0x31, 0xFE, 0x3D, 0x86, 0x7D, 0x86, 0xC1, 0xFE, 0x7D, 0xB8, 0x3D, 0x90, 0x19, 0xFE, 0x7D, 0xFE, 0x7F, 0x10, 0x1B, 0xFE, 0x1B, 0xC6, 0x1F, 0xC6, 0x1F, 0xC6, 0x3E, 0xFE,
// U+9577 (長)
0x00, 0x00, 0x00, 0x00, 0x1F, 0xFC, 0x18, 0x00, 0x1C, 0x00, 0x1F, 0xF8, 0x1C, 0x00, 0x1F, 0xF8, 0x18, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x18, 0xDC, 0x18, 0xF8, 0x18, 0x70, 0x1F, 0xBC, 0x3F, 0x1E,
// U+957F (长)
0x00, 0x00, 0x0C, 0x08, 0x0C, 0x1C, 0x0C, 0x38, 0x0C, 0xE0, 0x0F, 0xC0, 0x0D, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x0C, 0xC0, 0x0C, 0xC0, 0x0C, 0x60, 0x0C, 0x70, 0x0C, 0xB8, 0x1F, 0x9E, 0x1F, 0x0E,
// U+958B (開)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x63, 0xC6, 0x7F, 0xFE, 0x63, 0xC6, 0x63, 0xC6, 0x7F, 0xFE, 0x60, 0x06, 0x6F, 0xF6, 0x66, 0x46, 0x66, 0xE6, 0x6F, 0xF6, 0x66, 0x46, 0x66, 0x46, 0x6C, 0x5E,
// U+9593 (間)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x63, 0xC6, 0x7F, 0xFE, 0x63, 0xC6, 0x63, 0xC6, 0x7F, 0xFE, 0x60, 0x06, 0x67, 0xE6, 0x66, 0x66, 0x66, 0x66, 0x67, 0xE6, 0x66, 0x66, 0x67, 0xE6, 0x66, 0x3E,
// U+95F4 (间)
0x00, 0x00, 0x20, 0x00, 0x37, 0xFE, 0x1B, 0xFE, 0x10, 0x06, 0x60, 0x06, 0x67, 0xE6, 0x66, 0x66, 0x64, 0x66, 0x67, 0xE6, 0x66, 0x66, 0x64, 0x66, 0x66, 0x66, 0x67, 0xE6, 0x60, 0x06, 0x60, 0x0E,
// U+9605 (阅)
0x00, 0x00, 0x20, 0x00, 0x37, 0xFE, 0x1B, 0xFE, 0x06, 0x66, 0x66, 0x66, 0x63, 0xC6, 0x6F, 0xF6, 0x6C, 0x36, 0x6C, 0x36, 0x6F, 0xF6, 0x63, 0xC6, 0x66, 0xC6, 0x66, 0xDE, 0x7C, 0xF6, 0x68, 0x1E,
// U+9664 (除)
0x00, 0x00, 0x00, 0x30, 0x7C, 0x70, 0x7C, 0xF8, 0x6C, 0xDC, 0x7B, 0x8E, 0x7F, 0xFF, 0x7B, 0xFC, 0x6C, 0x30, 0x6F, 0xFE, 0x6F, 0xFE, 0x7C, 0x30, 0x79, 0xBC, 0x63, 0x36, 0x63, 0x36, 0x66, 0xF2,
// U+9690 (隐)
0x00, 0x00, 0x00, 0xC0, 0x7C, 0xC0, 0x7D, 0xF8, 0x6F, 0x18, 0x6B, 0xFE, 0x78, 0x06, 0x78, 0x06, 0x6D, 0xFE, 0x6C, 0x06, 0x6F, 0xFE, 0x7C, 0x60, 0x7E, 0xF4, 0x63, 0xB6, 0x67, 0x8E, 0x66, 0xFA,
// U+9694 (隔)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7D, 0xFE, 0x6D, 0xFE, 0x79, 0x86, 0x79, 0x86, 0x79, 0xFE, 0x6C, 0x00, 0x6F, 0xFE, 0x6F, 0xCE, 0x7F, 0x5E, 0x7B, 0xFE, 0x63, 0x36, 0x63, 0x36, 0x63, 0x36,
// U+96A0 (隠)
0x00, 0x00, 0x00, 0x7C, 0x7F, 0xFC, 0x7D, 0x66, 0x6F, 0x6C, 0x79, 0xAC, 0x7B, 0xFE, 0x78, 0x06, 0x6F, 0xFE, 0x6C, 0x06, 0x6F, 0xFE, 0x7C, 0xE0, 0x7C, 0x70, 0x63, 0x8C, 0x66, 0x9E, 0x64, 0xFA,
// U+96FB (電)
0x00, 0x00, 0x00, 0x00, 0x3F, 0xFC, 0x01, 0x80, 0x7F, 0xFE, 0x61, 0x86, 0x7F, 0xFE, 0x7F, 0xFE, 0x1D, 0xB8, 0x3F, 0xFC, 0x31, 0x8C, 0x3F, 0xFC, 0x31, 0x8C, 0x3F, 0xFE, 0x31, 0x83, 0x11, 0x86,
// U+9762 (面)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x01, 0x80, 0x3F, 0xFC, 0x7F, 0xFE, 0x66, 0x66, 0x66, 0x66, 0x67, 0xE6, 0x66, 0x66, 0x66, 0x66, 0x67, 0xE6, 0x66, 0x66, 0x7F, 0xFE, 0x7F, 0xFE,
// U+983B (頻)
0x00, 0x00, 0x08, 0x00, 0x2C, 0xFE, 0x6F, 0x7E, 0x6C, 0x30, 0x6C, 0xFE, 0x7F, 0xC6, 0xFF, 0xFE, 0x08, 0xC6, 0x3B, 0xC6, 0x6B, 0xFE, 0x6F, 0xC6, 0x4E, 0xFE, 0x0C, 0x00, 0x1C, 0x6C, 0x79, 0xE6,
// U+9875 (页)
0x00, 0x00, 0x00, 0x00, 0x7F, 0xFE, 0x7F, 0xFE, 0x03, 0x00, 0x1F, 0xF8, 0x3F, 0xFC, 0x30, 0x0C, 0x31, 0x8C, 0x31, 0x8C, 0x31, 0x8C, 0x31, 0x8C, 0x31, 0x8C, 0x13, 0x70, 0x0F, 0x3C, 0x7C, 0x0E,
// U+987A (顺)
0x00, 0x00, 0x00, 0x00, 0x67, 0xFE, 0x7E, 0xFE, 0x7E, 0x30, 0x7E, 0xFE, 0x7E, 0xFE, 0x7E, 0xD6, 0x7E, 0xF6, 0x7E, 0xF6, 0x7E, 0xF6, 0x7E, 0xF6, 0x7E, 0xF6, 0x7E, 0x38, 0x5E, 0x7C, 0xC7, 0xE6,
// U+9891 (频)
0x00, 0x00, 0x0C, 0x00, 0x2C, 0xFE, 0x6F, 0x7E, 0x6C, 0x10, 0x6C, 0xFE, 0x7F, 0xC6, 0xFF, 0xD6, 0x08, 0xDE, 0x3B, 0xDE, 0x6B, 0xD6, 0x6F, 0xD6, 0x4E, 0xF6, 0x0C, 0x38, 0x1C, 0x7C, 0x79, 0xE6,
// U+989D (额)
0x00, 0x00, 0x18, 0x00, 0x1C, 0xFE, 0x7F, 0x7E, 0x43, 0x30, 0x7B, 0xFE, 0x3F, 0xC6, 0x66, 0xD6, 0x7C, 0xD6, 0x1E, 0xD6, 0x7F, 0xD6, 0x63, 0xF6, 0x3E, 0xF6, 0x32, 0x38, 0x32, 0x6C, 0x3F, 0xE6,
// U+9F50 (齐)
0x00, 0x00, 0x01, 0x80, 0x01, 0x80, 0x7F, 0xFE, 0x7F, 0xFC, 0x0E, 0x70, 0x07, 0xE0, 0x07, 0xE0, 0x7F, 0xFE, 0x7C, 0x1E, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x0C, 0x30, 0x18, 0x30, 0x38, 0x30,
// U+9F7F (齿)
0x00, 0x00, 0x01, 0x80, 0x19, 0x80, 0x19, 0xFC, 0x19, 0x80, 0x19, 0x80, 0x7F, 0xFE, 0x7F, 0xFE, 0x31, 0x86, 0x31, 0x86, 0x33, 0xC6, 0x33, 0xE6, 0x36, 0x7E, 0x3C, 0x3E, 0x3F, 0xFE, 0x3F, 0xFE,
};
/**
* Binary search for codepoint in lookup table.
* @return Index of glyph, or -1 if not found
*/
inline int16_t findCjkUiGlyphIndex(uint16_t cp) {
int16_t lo = 0;
int16_t hi = CJK_UI_FONT_GLYPH_COUNT - 1;
while (lo <= hi) {
int16_t mid = (lo + hi) / 2;
uint16_t midCp = pgm_read_word(&CJK_UI_CODEPOINTS[mid]);
if (midCp == cp) {
return mid;
} else if (midCp < cp) {
lo = mid + 1;
} else {
hi = mid - 1;
}
}
return -1;
}
/**
* Get glyph bitmap for a codepoint from built-in CJK UI font.
* @param cp Unicode codepoint
* @return Pointer to glyph bitmap in PROGMEM, or nullptr if not available
*/
inline const uint8_t* getCjkUiGlyph(uint32_t cp) {
if (cp > 0xFFFF) return nullptr; // Only BMP supported
int16_t idx = findCjkUiGlyphIndex(static_cast<uint16_t>(cp));
if (idx < 0) return nullptr;
return &CJK_UI_FONT_DATA[idx * CJK_UI_FONT_BYTES_PER_CHAR];
}
/**
* Check if a codepoint has a glyph in the built-in CJK UI font.
*/
inline bool hasCjkUiGlyph(uint32_t cp) {
if (cp > 0xFFFF) return false;
return findCjkUiGlyphIndex(static_cast<uint16_t>(cp)) >= 0;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1031
lib/I18n/I18n.cpp Normal file

File diff suppressed because it is too large Load Diff

350
lib/I18n/I18n.h Normal file
View File

@ -0,0 +1,350 @@
#pragma once
#include <cstdint>
/**
* Internationalization (i18n) system for CrossPoint Reader
* Supports English and Chinese UI languages
*/
// String IDs - organized by category
enum class StrId : uint16_t {
// === Boot/Sleep ===
CROSSPOINT, // "CrossPoint"
BOOTING, // "BOOTING" / "启动中"
SLEEPING, // "SLEEPING" / "休眠中"
ENTERING_SLEEP, // "Entering Sleep..." / "进入休眠..."
// === Home Menu ===
BROWSE_FILES, // "Browse Files" / "浏览文件"
FILE_TRANSFER, // "File Transfer" / "文件传输"
SETTINGS_TITLE, // "Settings" / "设置"
CALIBRE_LIBRARY, // "Calibre Library" / "Calibre书库"
CONTINUE_READING, // "Continue Reading" / "继续阅读"
NO_OPEN_BOOK, // "No open book" / "无打开的书籍"
START_READING, // "Start reading below" / "从下方开始阅读"
// === File Browser ===
BOOKS, // "Books" / "书籍"
NO_BOOKS_FOUND, // "No books found" / "未找到书籍"
// === Reader ===
SELECT_CHAPTER, // "Select Chapter" / "选择章节"
NO_CHAPTERS, // "No chapters" / "无章节"
END_OF_BOOK, // "End of book" / "已到书末"
EMPTY_CHAPTER, // "Empty chapter" / "空章节"
INDEXING, // "Indexing..." / "索引中..."
MEMORY_ERROR, // "Memory error" / "内存错误"
PAGE_LOAD_ERROR, // "Page load error" / "页面加载错误"
EMPTY_FILE, // "Empty file" / "空文件"
OUT_OF_BOUNDS, // "Out of bounds" / "超出范围"
LOADING, // "Loading..." / "加载中..."
LOAD_XTC_FAILED, // "Failed to load XTC" / "加载XTC失败"
LOAD_TXT_FAILED, // "Failed to load TXT" / "加载TXT失败"
LOAD_EPUB_FAILED, // "Failed to load EPUB" / "加载EPUB失败"
SD_CARD_ERROR, // "SD card error" / "SD卡错误"
// === Network ===
WIFI_NETWORKS, // "WiFi Networks" / "WiFi网络"
NO_NETWORKS, // "No networks found" / "未找到网络"
NETWORKS_FOUND, // "%zu networks found" / "找到%zu个网络"
SCANNING, // "Scanning..." / "扫描中..."
CONNECTING, // "Connecting..." / "连接中..."
CONNECTED, // "Connected!" / "已连接!"
CONNECTION_FAILED, // "Connection Failed" / "连接失败"
CONNECTION_TIMEOUT, // "Connection timeout" / "连接超时"
FORGET_NETWORK, // "Forget Network?" / "忘记网络?"
SAVE_PASSWORD, // "Save password for next time?" / "保存密码?"
REMOVE_PASSWORD, // "Remove saved password?" / "删除已保存密码?"
PRESS_OK_SCAN, // "Press OK to scan again" / "按确定重新扫描"
PRESS_ANY_CONTINUE, // "Press any button to continue" / "按任意键继续"
SELECT_HINT, // "LEFT/RIGHT: Select | OK: Confirm" / "左/右:选择 | 确定:确认"
HOW_CONNECT, // "How would you like to connect?" / "选择连接方式"
JOIN_NETWORK, // "Join a Network" / "加入网络"
CREATE_HOTSPOT, // "Create Hotspot" / "创建热点"
JOIN_DESC, // "Connect to an existing WiFi network" / "连接到现有WiFi网络"
HOTSPOT_DESC, // "Create a WiFi network others can join" /
// "创建WiFi网络供他人连接"
STARTING_HOTSPOT, // "Starting Hotspot..." / "启动热点中..."
HOTSPOT_MODE, // "Hotspot Mode" / "热点模式"
CONNECT_WIFI_HINT, // "Connect your device to this WiFi network" /
// "将设备连接到此WiFi"
OPEN_URL_HINT, // "Open this URL in your browser" / "在浏览器中打开此URL"
OR_HTTP_PREFIX, // "or http://" / "或 http://"
SCAN_QR_HINT, // "or scan QR code with your phone:" / "或用手机扫描二维码:"
CALIBRE_WIRELESS, // "Calibre Wireless" / "Calibre无线连接"
CALIBRE_WEB_URL, // "Calibre Web URL" / "Calibre Web地址"
CONNECT_WIRELESS, // "Connect as Wireless Device" / "作为无线设备连接"
NETWORK_LEGEND, // "* = Encrypted | + = Saved" / "* = 加密 | + = 已保存"
MAC_ADDRESS, // "MAC address:" / "MAC地址:"
CHECKING_WIFI, // "Checking WiFi..." / "检查WiFi..."
ENTER_WIFI_PASSWORD, // "Enter WiFi Password" / "输入WiFi密码"
ENTER_TEXT, // "Enter Text" / "输入文字"
// === Calibre Wireless ===
CALIBRE_DISCOVERING, // "Discovering Calibre..." / "正在搜索Calibre..."
CALIBRE_CONNECTING_TO, // "Connecting to " / "正在连接到 "
CALIBRE_CONNECTED_TO, // "Connected to " / "已连接到 "
CALIBRE_WAITING_COMMANDS, // "Waiting for commands..." / "等待指令中..."
CONNECTION_FAILED_RETRYING, // "(Connection failed, retrying)" / "(连接失败,重试中)"
CALIBRE_DISCONNECTED, // "Calibre disconnected" / "Calibre已断开"
CALIBRE_WAITING_TRANSFER, // "Waiting for transfer..." / "等待传输中..."
CALIBRE_TRANSFER_HINT, // Transfer hint text
CALIBRE_RECEIVING, // "Receiving: " / "正在接收: "
CALIBRE_RECEIVED, // "Received: " / "已接收: "
CALIBRE_WAITING_MORE, // "Waiting for more..." / "等待更多内容..."
CALIBRE_FAILED_CREATE_FILE, // "Failed to create file" / "创建文件失败"
CALIBRE_PASSWORD_REQUIRED, // "Password required" / "需要密码"
CALIBRE_TRANSFER_INTERRUPTED, // "Transfer interrupted" / "传输中断"
// === Settings Categories ===
CAT_DISPLAY, // "Display" / "显示"
CAT_READER, // "Reader" / "阅读"
CAT_CONTROLS, // "Controls" / "控制"
CAT_SYSTEM, // "System" / "系统"
// === Settings ===
SLEEP_SCREEN, // "Sleep Screen" / "休眠屏幕"
SLEEP_COVER_MODE, // "Sleep Screen Cover Mode" / "封面显示模式"
STATUS_BAR, // "Status Bar" / "状态栏"
HIDE_BATTERY, // "Hide Battery %" / "隐藏电量百分比"
EXTRA_SPACING, // "Extra Paragraph Spacing" / "段落额外间距"
TEXT_AA, // "Text Anti-Aliasing" / "文字抗锯齿"
SHORT_PWR_BTN, // "Short Power Button Click" / "电源键短按"
ORIENTATION, // "Reading Orientation" / "阅读方向"
FRONT_BTN_LAYOUT, // "Front Button Layout" / "前置按钮布局"
SIDE_BTN_LAYOUT, // "Side Button Layout (reader)" / "侧边按钮布局"
LONG_PRESS_SKIP, // "Long-press Chapter Skip" / "长按跳转章节"
FONT_FAMILY, // "Reader Font Family" / "阅读字体"
EXT_READER_FONT, // "External Reader Font" / "阅读器字体"
EXT_CHINESE_FONT, // "Reader Font" / "阅读器字体"
EXT_UI_FONT, // "External UI Font" / "UI字体"
FONT_SIZE, // "Reader Font Size" / "字体大小"
LINE_SPACING, // "Reader Line Spacing" / "行间距"
ASCII_LETTER_SPACING, // "ASCII Letter Spacing" / "ASCII 字母间距"
ASCII_DIGIT_SPACING, // "ASCII Digit Spacing" / "ASCII 数字间距"
CJK_SPACING, // "CJK Spacing" / "汉字间距"
COLOR_MODE, // "Color Mode" / "颜色模式"
SCREEN_MARGIN, // "Reader Screen Margin" / "页面边距"
PARA_ALIGNMENT, // "Reader Paragraph Alignment" / "段落对齐"
HYPHENATION, // "Hyphenation" / "连字符"
TIME_TO_SLEEP, // "Time to Sleep" / "休眠时间"
REFRESH_FREQ, // "Refresh Frequency" / "刷新频率"
CALIBRE_SETTINGS, // "Calibre Settings" / "Calibre设置"
KOREADER_SYNC, // "KOReader Sync" / "KOReader同步"
CHECK_UPDATES, // "Check for updates" / "检查更新"
LANGUAGE, // "Language" / "语言"
SELECT_WALLPAPER, // "Select Wallpaper" / "选择壁纸"
CLEAR_READING_CACHE, // "Clear Reading Cache" / "清理阅读缓存"
// === Calibre Settings ===
CALIBRE, // "Calibre" / "Calibre"
// === KOReader Settings ===
USERNAME, // "Username" / "用户名"
PASSWORD, // "Password" / "密码"
SYNC_SERVER_URL, // "Sync Server URL" / "同步服务器地址"
DOCUMENT_MATCHING, // "Document Matching" / "文档匹配"
AUTHENTICATE, // "Authenticate" / "认证"
KOREADER_USERNAME, // "KOReader Username" / "KOReader用户名"
KOREADER_PASSWORD, // "KOReader Password" / "KOReader密码"
FILENAME, // "Filename" / "文件名"
BINARY, // "Binary" / "二进制"
SET_CREDENTIALS_FIRST, // "Set credentials first" / "请先设置凭据"
// === KOReader Auth ===
WIFI_CONN_FAILED, // "WiFi connection failed" / "WiFi连接失败"
AUTHENTICATING, // "Authenticating..." / "认证中..."
AUTH_SUCCESS, // "Successfully authenticated!" / "认证成功!"
KOREADER_AUTH, // "KOReader Auth" / "KOReader认证"
SYNC_READY, // "KOReader sync is ready to use" / "KOReader同步已就绪"
AUTH_FAILED, // "Authentication Failed" / "认证失败"
DONE, // "Done" / "完成"
// === Clear Cache ===
CLEAR_CACHE_WARNING_1, // "This will clear all cached book data." / "这将清除所有缓存的书籍数据。"
CLEAR_CACHE_WARNING_2, // "All reading progress will be lost!" / "所有阅读进度将丢失!"
CLEAR_CACHE_WARNING_3, // "Books will need to be re-indexed" / "书籍需要重新索引"
CLEAR_CACHE_WARNING_4, // "when opened again." / "当再次打开时。"
CLEARING_CACHE, // "Clearing cache..." / "清理缓存中..."
CACHE_CLEARED, // "Cache Cleared" / "缓存已清理"
ITEMS_REMOVED, // "items removed" / "项已删除"
FAILED_LOWER, // "failed" / "失败"
CLEAR_CACHE_FAILED, // "Failed to clear cache" / "清理缓存失败"
CHECK_SERIAL_OUTPUT, // "Check serial output for details" / "查看串口输出了解详情"
// Setting Values
DARK, // "Dark" / "深色"
LIGHT, // "Light" / "浅色"
CUSTOM, // "Custom" / "自定义"
COVER, // "Cover" / "封面"
NONE, // "None" / "无"
FIT, // "Fit" / "适应"
CROP, // "Crop" / "裁剪"
NO_PROGRESS, // "No Progress" / "无进度"
FULL, // "Full" / "完整"
NEVER, // "Never" / "从不"
IN_READER, // "In Reader" / "阅读时"
ALWAYS, // "Always" / "始终"
IGNORE, // "Ignore" / "忽略"
SLEEP, // "Sleep" / "休眠"
PAGE_TURN, // "Page Turn" / "翻页"
PORTRAIT, // "Portrait" / "竖屏"
LANDSCAPE_CW, // "Landscape CW" / "横屏顺时针"
INVERTED, // "Inverted" / "倒置"
LANDSCAPE_CCW, // "Landscape CCW" / "横屏逆时针"
FRONT_LAYOUT_BCLR, // "Bck, Cnfrm, Lft, Rght" / "返回, 确认, 左, 右"
FRONT_LAYOUT_LRBC, // "Lft, Rght, Bck, Cnfrm" / "左, 右, 返回, 确认"
FRONT_LAYOUT_LBCR, // "Lft, Bck, Cnfrm, Rght" / "左, 返回, 确认, 右"
PREV_NEXT, // "Prev/Next" / "上一页/下一页"
NEXT_PREV, // "Next/Prev" / "下一页/上一页"
BOOKERLY, // "Bookerly"
NOTO_SANS, // "Noto Sans"
OPEN_DYSLEXIC, // "Open Dyslexic"
SMALL, // "Small" / "小"
MEDIUM, // "Medium" / "中"
LARGE, // "Large" / "大"
X_LARGE, // "X Large" / "特大"
TIGHT, // "Tight" / "紧凑"
NORMAL, // "Normal" / "正常"
WIDE, // "Wide" / "宽松"
JUSTIFY, // "Justify" / "两端对齐"
LEFT, // "Left" / "左对齐"
CENTER, // "Center" / "居中"
RIGHT, // "Right" / "右对齐"
MIN_1, // "1 min" / "1分钟"
MIN_5, // "5 min" / "5分钟"
MIN_10, // "10 min" / "10分钟"
MIN_15, // "15 min" / "15分钟"
MIN_30, // "30 min" / "30分钟"
PAGES_1, // "1 page" / "1页"
PAGES_5, // "5 pages" / "5页"
PAGES_10, // "10 pages" / "10页"
PAGES_15, // "15 pages" / "15页"
PAGES_30, // "30 pages" / "30页"
// === OTA Update ===
UPDATE, // "Update" / "更新"
CHECKING_UPDATE, // "Checking for update..." / "检查更新中..."
NEW_UPDATE, // "New update available!" / "有新版本可用!"
CURRENT_VERSION, // "Current Version: " / "当前版本: "
NEW_VERSION, // "New Version: " / "新版本: "
UPDATING, // "Updating..." / "更新中..."
NO_UPDATE, // "No update available" / "已是最新版本"
UPDATE_FAILED, // "Update failed" / "更新失败"
UPDATE_COMPLETE, // "Update complete" / "更新完成"
POWER_ON_HINT, // "Press and hold power button to turn back on" /
// "长按电源键开机"
// === Font Selection ===
EXTERNAL_FONT, // "External Font" / "外置字体"
BUILTIN_DISABLED, // "Built-in (Disabled)" / "内置(已禁用)"
// === OPDS Browser ===
NO_ENTRIES, // "No entries found" / "无条目"
DOWNLOADING, // "Downloading..." / "下载中..."
DOWNLOAD_FAILED, // "Download failed" / "下载失败"
ERROR, // "Error:" / "错误:"
UNNAMED, // "Unnamed" / "未命名"
NO_SERVER_URL, // "No server URL configured" / "未配置服务器地址"
FETCH_FEED_FAILED, // "Failed to fetch feed" / "获取订阅失败"
PARSE_FEED_FAILED, // "Failed to parse feed" / "解析订阅失败"
NETWORK_PREFIX, // "Network: " / "网络: "
IP_ADDRESS_PREFIX, // "IP Address: " / "IP地址: "
SCAN_QR_WIFI_HINT, // "or scan QR code with your phone to connect to Wifi." /
// "或用手机扫描二维码连接WiFi"
// === Buttons ===
BACK, // "« Back" / "« 返回"
EXIT, // "« Exit" / "« 退出"
HOME, // "« Home" / "« 主页"
SAVE, // "« Save" / "« 保存"
SELECT, // "Select" / "选择"
TOGGLE, // "Toggle" / "切换"
CONFIRM, // "Confirm" / "确定"
CANCEL, // "Cancel" / "取消"
CONNECT, // "Connect" / "连接"
OPEN, // "Open" / "打开"
DOWNLOAD, // "Download" / "下载"
RETRY, // "Retry" / "重试"
YES, // "Yes" / "是"
NO, // "No" / "否"
ON, // "ON" / "开"
OFF, // "OFF" / "关"
SET, // "Set" / "已设置"
NOT_SET, // "Not Set" / "未设置"
DIR_LEFT, // "Left" / "左"
DIR_RIGHT, // "Right" / "右"
DIR_UP, // "Up" / "上"
DIR_DOWN, // "Down" / "下"
CAPS_ON, // "CAPS" / "大写"
CAPS_OFF, // "caps" / "小写"
OK_BUTTON, // "OK" / "确定"
// === Languages ===
ENGLISH, // "English"
CHINESE_SIMPLIFIED, // "简体中文"
JAPANESE, // "日本語"
// Sentinel - must be last
_COUNT
};
// Language enum
enum class Language : uint8_t {
ENGLISH = 0,
CHINESE_SIMPLIFIED = 1,
JAPANESE = 2,
_COUNT
};
class I18n {
public:
static I18n &getInstance();
// Disable copy
I18n(const I18n &) = delete;
I18n &operator=(const I18n &) = delete;
/**
* Get localized string by ID
*/
const char *get(StrId id) const;
/**
* Shorthand operator for get()
*/
const char *operator[](StrId id) const { return get(id); }
/**
* Get/Set current language
*/
Language getLanguage() const { return _language; }
void setLanguage(Language lang);
/**
* Save/Load language setting
*/
void saveSettings();
void loadSettings();
/**
* Get all unique characters used in a specific language
* Returns a sorted string of unique characters
*/
static const char *getCharacterSet(Language lang);
// String arrays (public for static_assert access)
static const char *const STRINGS_EN[];
static const char *const STRINGS_ZH[];
static const char *const STRINGS_JA[];
private:
I18n() : _language(Language::ENGLISH) {}
Language _language;
};
// Convenience macros
#define TR(id) I18n::getInstance().get(StrId::id)
#define I18N I18n::getInstance()

@ -1 +1 @@
Subproject commit bd4e6707503ab9c97d13ee0d8f8c69e9ff03cd12 Subproject commit 6fa905c7b977df254c3642c35c6277e4e588abf8

View File

@ -0,0 +1,271 @@
#!/usr/bin/env python3
"""
Generate CJK UI font header file for CrossPoint Reader.
Uses Source Han Sans (思源黑体) to generate bitmap font data.
Usage:
python3 generate_cjk_ui_font.py --size 26 --font /path/to/SourceHanSansSC-Medium.otf
"""
import argparse
import sys
from pathlib import Path
try:
from PIL import Image, ImageDraw, ImageFont
except ImportError:
print("Error: PIL/Pillow not installed. Run: pip3 install Pillow")
sys.exit(1)
# UI characters needed (extracted from I18n strings + common punctuation)
UI_CHARS = """
!#$%&'()*+,-./0123456789:;<=>?@
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`
abcdefghijklmnopqrstuvwxyz{|}~
启动中休眠进入浏览文件传输设置书库继续阅读无打开的籍从下方开始
未找到选择章节空已末索引内存错误页面加载
网络扫描连接失败超时忘记保存密码删除按确定重新任意键
左右上确认加热点现有供他人
此址或手机二维码地检查输入文字
正在搜寻等待指令断更多容创建需要中
屏幕封显示模式状态栏隐藏电量百分比段落额外间距抗锯齿
电源短阅读方向前置按钮布局侧边长跳转字体大小行
颜色边距对齐时间刷新频率语言壁纸清理缓存
深浅自定义适应裁剪度完整从不始终忽略翻竖横顺逆针
返确左右上一下紧凑正常宽松两端居分钟
版本新可用当前中检查数据成功信息
外内禁停全最后退出主保切换取消打回重试是否关
大写小决
英简繁體日本語
个令信先写制力务卡去受同名命壁多容待得必指控搜收数断服期析步母汉清理符等简系纸统缓获要解订需颜
ファイル一覧転送定ライブラリ続読開本ありません下書始
見つかりませんを章なし終わり空のインデックスメモリエラー
ページ込み範囲外失敗しました
ネットワークスキャン接続完了タイムアウト忘れるパスワード
押して再任意ボタン行方法参加ホットスポット作成既存
デバイスブラウザこのURLまたはスマホQRコードをして
無線ケーブル送受信待機
画面カバー非表示常にベル配置向きボン前後サイド押し飛ばし
フォントサイズ幅余白揃え分
アップート利チェック新しいバージョン現更失敗完成功
フ使用可中壊オフオンセ解除左右上回戻キャンセル再度はいいいえ
検機決漢紙題
"""
# Extract unique characters
def get_unique_chars(text):
chars = set()
for c in text:
if c.strip() and ord(c) >= 0x20:
chars.add(c)
return sorted(chars, key=ord)
def generate_font_header(font_path, pixel_size, output_path):
"""Generate CJK UI font header file."""
# Calculate font point size (approximately pixel_size * 0.7)
pt_size = int(pixel_size * 0.7)
try:
font = ImageFont.truetype(font_path, pt_size)
except Exception as e:
print(f"Error loading font: {e}")
return False
chars = get_unique_chars(UI_CHARS)
print(f"Generating {pixel_size}x{pixel_size} font with {len(chars)} characters...")
# Collect glyph data
codepoints = []
widths = []
bitmaps = []
for char in chars:
cp = ord(char)
# Create image for character
img = Image.new('1', (pixel_size, pixel_size), 0)
draw = ImageDraw.Draw(img)
# Get character bounding box
try:
bbox = font.getbbox(char)
if bbox:
char_width = bbox[2] - bbox[0]
char_height = bbox[3] - bbox[1]
else:
char_width = pixel_size // 2
char_height = pixel_size
except:
char_width = pixel_size // 2
char_height = pixel_size
# Center character in cell
x = (pixel_size - char_width) // 2
y = (pixel_size - char_height) // 2 - (bbox[1] if bbox else 0)
# Draw character
draw.text((x, y), char, font=font, fill=1)
# Convert to bytes
bytes_per_row = (pixel_size + 7) // 8
bitmap_bytes = []
for row in range(pixel_size):
for byte_idx in range(bytes_per_row):
byte_val = 0
for bit in range(8):
px = byte_idx * 8 + bit
if px < pixel_size:
pixel = img.getpixel((px, row))
if pixel:
byte_val |= (1 << (7 - bit))
bitmap_bytes.append(byte_val)
codepoints.append(cp)
# Calculate advance width
if cp < 0x80:
# ASCII: use actual width + small padding
widths.append(min(char_width + 2, pixel_size))
else:
# CJK: use full width
widths.append(pixel_size)
bitmaps.append(bitmap_bytes)
# Generate header file
bytes_per_row = (pixel_size + 7) // 8
bytes_per_char = bytes_per_row * pixel_size
with open(output_path, 'w') as f:
f.write(f'''/**
* Auto-generated CJK UI font data (optimized - UI characters only)
* Font: 思源黑体-Medium
* Size: {pt_size}pt
* Dimensions: {pixel_size}x{pixel_size}
* Characters: {len(chars)}
* Total size: {len(chars) * bytes_per_char} bytes ({len(chars) * bytes_per_char / 1024:.1f} KB)
*
* This is a sparse font containing only UI-required CJK characters.
* Uses a lookup table for codepoint -> glyph index mapping.
* Supports proportional spacing for English characters.
*/
#pragma once
namespace CjkUiFont{pixel_size} {{
#include <cstdint>
#include <pgmspace.h>
// Font parameters
static constexpr uint8_t CJK_UI_FONT_WIDTH = {pixel_size};
static constexpr uint8_t CJK_UI_FONT_HEIGHT = {pixel_size};
static constexpr uint8_t CJK_UI_FONT_BYTES_PER_ROW = {bytes_per_row};
static constexpr uint8_t CJK_UI_FONT_BYTES_PER_CHAR = {bytes_per_char};
static constexpr uint16_t CJK_UI_FONT_GLYPH_COUNT = {len(chars)};
// Codepoint lookup table (sorted for binary search)
static const uint16_t CJK_UI_CODEPOINTS[] PROGMEM = {{
''')
# Write codepoints
for i, cp in enumerate(codepoints):
if i % 16 == 0:
f.write(' ')
f.write(f'0x{cp:04X}, ')
if (i + 1) % 16 == 0:
f.write('\n')
if len(codepoints) % 16 != 0:
f.write('\n')
f.write('};\n\n')
# Write widths
f.write('// Glyph width table (actual advance width for proportional spacing)\n')
f.write('static const uint8_t CJK_UI_GLYPH_WIDTHS[] PROGMEM = {\n')
for i, w in enumerate(widths):
if i % 16 == 0:
f.write(' ')
f.write(f'{w:3}, ')
if (i + 1) % 16 == 0:
f.write('\n')
if len(widths) % 16 != 0:
f.write('\n')
f.write('};\n\n')
# Write bitmap data
f.write('// Glyph bitmap data\n')
f.write('static const uint8_t CJK_UI_GLYPHS[] PROGMEM = {\n')
for i, bitmap in enumerate(bitmaps):
f.write(f' // U+{codepoints[i]:04X} ({chr(codepoints[i])})\n ')
for j, b in enumerate(bitmap):
f.write(f'0x{b:02X}, ')
if (j + 1) % 16 == 0 and j < len(bitmap) - 1:
f.write('\n ')
f.write('\n')
f.write('};\n\n')
# Write lookup functions
f.write('''// Binary search for codepoint
inline int findGlyphIndex(uint16_t codepoint) {
int low = 0;
int high = CJK_UI_FONT_GLYPH_COUNT - 1;
while (low <= high) {
int mid = (low + high) / 2;
uint16_t midCp = pgm_read_word(&CJK_UI_CODEPOINTS[mid]);
if (midCp == codepoint) return mid;
if (midCp < codepoint) low = mid + 1;
else high = mid - 1;
}
return -1;
}
inline bool hasCjkUiGlyph(uint32_t codepoint) {
if (codepoint > 0xFFFF) return false;
return findGlyphIndex(static_cast<uint16_t>(codepoint)) >= 0;
}
inline const uint8_t* getCjkUiGlyph(uint32_t codepoint) {
if (codepoint > 0xFFFF) return nullptr;
int idx = findGlyphIndex(static_cast<uint16_t>(codepoint));
if (idx < 0) return nullptr;
return &CJK_UI_GLYPHS[idx * CJK_UI_FONT_BYTES_PER_CHAR];
}
inline uint8_t getCjkUiGlyphWidth(uint32_t codepoint) {
if (codepoint > 0xFFFF) return 0;
int idx = findGlyphIndex(static_cast<uint16_t>(codepoint));
if (idx < 0) return 0;
return pgm_read_byte(&CJK_UI_GLYPH_WIDTHS[idx]);
}
} // namespace CjkUiFont''' + str(pixel_size) + '\n')
print(f"Generated: {output_path}")
print(f" - {len(chars)} characters")
print(f" - {len(chars) * bytes_per_char} bytes bitmap data")
return True
def main():
parser = argparse.ArgumentParser(description='Generate CJK UI font header')
parser.add_argument('--size', type=int, default=26, help='Pixel size (default: 26)')
parser.add_argument('--font', type=str, required=True, help='Path to Source Han Sans font file')
parser.add_argument('--output', type=str, help='Output path (default: lib/GfxRenderer/cjk_ui_font_SIZE.h)')
args = parser.parse_args()
script_dir = Path(__file__).parent
project_root = script_dir.parent
if args.output:
output_path = Path(args.output)
else:
output_path = project_root / 'lib' / 'GfxRenderer' / f'cjk_ui_font_{args.size}.h'
if not Path(args.font).exists():
print(f"Error: Font file not found: {args.font}")
sys.exit(1)
if generate_font_header(args.font, args.size, output_path):
print("Success!")
else:
print("Failed!")
sys.exit(1)
if __name__ == '__main__':
main()

View File

@ -1,5 +1,6 @@
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include <FontManager.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <Serialization.h> #include <Serialization.h>
@ -196,6 +197,16 @@ int CrossPointSettings::getRefreshFrequency() const {
} }
int CrossPointSettings::getReaderFontId() const { int CrossPointSettings::getReaderFontId() const {
// Check if external font is enabled - if so, return a unique ID based on font index
// This ensures cache invalidation when external font changes
FontManager &fm = FontManager::getInstance();
if (fm.isExternalFontEnabled()) {
// Return a unique negative ID based on external font index
// Using negative values to avoid collision with built-in font IDs
return -(fm.getSelectedIndex() + 1000);
}
// Fall back to built-in font selection
switch (fontFamily) { switch (fontFamily) {
case BOOKERLY: case BOOKERLY:
default: default:

View File

@ -72,6 +72,8 @@ decltype(InputManager::BTN_BACK) MappedInputManager::mapButton(const Button butt
return InputManager::BTN_BACK; return InputManager::BTN_BACK;
} }
void MappedInputManager::update() { inputManager.update(); }
bool MappedInputManager::wasPressed(const Button button) const { return inputManager.wasPressed(mapButton(button)); } bool MappedInputManager::wasPressed(const Button button) const { return inputManager.wasPressed(mapButton(button)); }
bool MappedInputManager::wasReleased(const Button button) const { return inputManager.wasReleased(mapButton(button)); } bool MappedInputManager::wasReleased(const Button button) const { return inputManager.wasReleased(mapButton(button)); }

View File

@ -15,6 +15,7 @@ class MappedInputManager {
explicit MappedInputManager(InputManager& inputManager) : inputManager(inputManager) {} explicit MappedInputManager(InputManager& inputManager) : inputManager(inputManager) {}
void update(); // Update button state (call before wasPressed/wasReleased)
bool wasPressed(Button button) const; bool wasPressed(Button button) const;
bool wasReleased(Button button) const; bool wasReleased(Button button) const;
bool isPressed(Button button) const; bool isPressed(Button button) const;

View File

@ -1,6 +1,7 @@
#include "BootActivity.h" #include "BootActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include "fontIds.h" #include "fontIds.h"
#include "images/CrossLarge.h" #include "images/CrossLarge.h"
@ -13,8 +14,8 @@ void BootActivity::onEnter() {
renderer.clearScreen(); 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(UI_10_FONT_ID, pageHeight / 2 + 70, TR(CROSSPOINT), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "BOOTING"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, TR(BOOTING));
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, CROSSPOINT_VERSION);
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -2,6 +2,7 @@
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <Txt.h> #include <Txt.h>
#include <Xtc.h> #include <Xtc.h>
@ -14,7 +15,7 @@
void SleepActivity::onEnter() { void SleepActivity::onEnter() {
Activity::onEnter(); Activity::onEnter();
renderPopup("Entering Sleep..."); renderPopup(TR(ENTERING_SLEEP));
if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::BLANK) { if (SETTINGS.sleepScreen == CrossPointSettings::SLEEP_SCREEN_MODE::BLANK) {
return renderBlankSleepScreen(); return renderBlankSleepScreen();
@ -125,8 +126,8 @@ void SleepActivity::renderDefaultSleepScreen() const {
renderer.clearScreen(); 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(UI_10_FONT_ID, pageHeight / 2 + 70, TR(CROSSPOINT), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, "SLEEPING"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight / 2 + 95, TR(SLEEPING));
// Make sleep screen dark unless light is selected in settings // Make sleep screen dark unless light is selected in settings
if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) { if (SETTINGS.sleepScreen != CrossPointSettings::SLEEP_SCREEN_MODE::LIGHT) {

View File

@ -3,6 +3,7 @@
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <I18n.h>
#include <OpdsStream.h> #include <OpdsStream.h>
#include <WiFi.h> #include <WiFi.h>
@ -36,7 +37,7 @@ void OpdsBookBrowserActivity::onEnter() {
currentPath = OPDS_ROOT_PATH; currentPath = OPDS_ROOT_PATH;
selectorIndex = 0; selectorIndex = 0;
errorMessage.clear(); errorMessage.clear();
statusMessage = "Checking WiFi..."; statusMessage = TR(CHECKING_WIFI);
updateRequired = true; updateRequired = true;
xTaskCreate(&OpdsBookBrowserActivity::taskTrampoline, "OpdsBookBrowserTask", xTaskCreate(&OpdsBookBrowserActivity::taskTrampoline, "OpdsBookBrowserTask",
@ -82,7 +83,7 @@ void OpdsBookBrowserActivity::loop() {
// WiFi connected - just retry fetching the feed // WiFi connected - just retry fetching the feed
Serial.printf("[%lu] [OPDS] Retry: WiFi connected, retrying fetch\n", millis()); Serial.printf("[%lu] [OPDS] Retry: WiFi connected, retrying fetch\n", millis());
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = TR(LOADING);
updateRequired = true; updateRequired = true;
fetchFeed(currentPath); fetchFeed(currentPath);
} else { } else {
@ -172,11 +173,11 @@ void OpdsBookBrowserActivity::render() const {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Calibre Library", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(CALIBRE_LIBRARY), true, EpdFontFamily::BOLD);
if (state == BrowserState::CHECK_WIFI) { if (state == BrowserState::CHECK_WIFI) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
const auto labels = mappedInput.mapLabels("« Back", "", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@ -184,23 +185,23 @@ void OpdsBookBrowserActivity::render() const {
if (state == BrowserState::LOADING) { if (state == BrowserState::LOADING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, statusMessage.c_str());
const auto labels = mappedInput.mapLabels("« Back", "", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == BrowserState::ERROR) { if (state == BrowserState::ERROR) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Error:"); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, TR(ERROR));
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, errorMessage.c_str());
const auto labels = mappedInput.mapLabels("« Back", "Retry", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), TR(RETRY), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == BrowserState::DOWNLOADING) { if (state == BrowserState::DOWNLOADING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, "Downloading..."); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 40, TR(DOWNLOADING));
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, statusMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 10, statusMessage.c_str());
if (downloadTotal > 0) { if (downloadTotal > 0) {
const int barWidth = pageWidth - 100; const int barWidth = pageWidth - 100;
@ -215,15 +216,15 @@ void OpdsBookBrowserActivity::render() const {
// Browsing state // Browsing state
// Show appropriate button hint based on selected entry type // Show appropriate button hint based on selected entry type
const char* confirmLabel = "Open"; const char* confirmLabel = TR(OPEN);
if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) { if (!entries.empty() && entries[selectorIndex].type == OpdsEntryType::BOOK) {
confirmLabel = "Download"; confirmLabel = TR(DOWNLOAD);
} }
const auto labels = mappedInput.mapLabels("« Back", confirmLabel, "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), confirmLabel, "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
if (entries.empty()) { if (entries.empty()) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "No entries found"); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, TR(NO_ENTRIES));
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@ -258,7 +259,7 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
const char* serverUrl = SETTINGS.opdsServerUrl; const char* serverUrl = SETTINGS.opdsServerUrl;
if (strlen(serverUrl) == 0) { if (strlen(serverUrl) == 0) {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "No server URL configured"; errorMessage = TR(NO_SERVER_URL);
updateRequired = true; updateRequired = true;
return; return;
} }
@ -272,7 +273,7 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
OpdsParserStream stream{parser}; OpdsParserStream stream{parser};
if (!HttpDownloader::fetchUrl(url, stream)) { if (!HttpDownloader::fetchUrl(url, stream)) {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "Failed to fetch feed"; errorMessage = TR(FETCH_FEED_FAILED);
updateRequired = true; updateRequired = true;
return; return;
} }
@ -280,7 +281,7 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
if (!parser) { if (!parser) {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "Failed to parse feed"; errorMessage = TR(PARSE_FEED_FAILED);
updateRequired = true; updateRequired = true;
return; return;
} }
@ -291,7 +292,7 @@ void OpdsBookBrowserActivity::fetchFeed(const std::string& path) {
if (entries.empty()) { if (entries.empty()) {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "No entries found"; errorMessage = TR(NO_ENTRIES);
updateRequired = true; updateRequired = true;
return; return;
} }
@ -306,7 +307,7 @@ void OpdsBookBrowserActivity::navigateToEntry(const OpdsEntry& entry) {
currentPath = entry.href; currentPath = entry.href;
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = TR(LOADING);
entries.clear(); entries.clear();
selectorIndex = 0; selectorIndex = 0;
updateRequired = true; updateRequired = true;
@ -324,7 +325,7 @@ void OpdsBookBrowserActivity::navigateBack() {
navigationHistory.pop_back(); navigationHistory.pop_back();
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = TR(LOADING);
entries.clear(); entries.clear();
selectorIndex = 0; selectorIndex = 0;
updateRequired = true; updateRequired = true;
@ -371,7 +372,7 @@ void OpdsBookBrowserActivity::downloadBook(const OpdsEntry& book) {
updateRequired = true; updateRequired = true;
} else { } else {
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "Download failed"; errorMessage = TR(DOWNLOAD_FAILED);
updateRequired = true; updateRequired = true;
} }
} }
@ -380,7 +381,7 @@ void OpdsBookBrowserActivity::checkAndConnectWifi() {
// Already connected? Verify connection is valid by checking IP // Already connected? Verify connection is valid by checking IP
if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) { if (WiFi.status() == WL_CONNECTED && WiFi.localIP() != IPAddress(0, 0, 0, 0)) {
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = TR(LOADING);
updateRequired = true; updateRequired = true;
fetchFeed(currentPath); fetchFeed(currentPath);
return; return;
@ -404,7 +405,7 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
if (connected) { if (connected) {
Serial.printf("[%lu] [OPDS] WiFi connected via selection, fetching feed\n", millis()); Serial.printf("[%lu] [OPDS] WiFi connected via selection, fetching feed\n", millis());
state = BrowserState::LOADING; state = BrowserState::LOADING;
statusMessage = "Loading..."; statusMessage = TR(LOADING);
updateRequired = true; updateRequired = true;
fetchFeed(currentPath); fetchFeed(currentPath);
} else { } else {
@ -414,7 +415,7 @@ void OpdsBookBrowserActivity::onWifiSelectionComplete(const bool connected) {
WiFi.disconnect(); WiFi.disconnect();
WiFi.mode(WIFI_OFF); WiFi.mode(WIFI_OFF);
state = BrowserState::ERROR; state = BrowserState::ERROR;
errorMessage = "WiFi connection failed"; errorMessage = TR(CONNECTION_FAILED);
updateRequired = true; updateRequired = true;
} }
} }

View File

@ -3,6 +3,7 @@
#include <Bitmap.h> #include <Bitmap.h>
#include <Epub.h> #include <Epub.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <Xtc.h> #include <Xtc.h>
@ -477,7 +478,7 @@ void HomeActivity::render() {
const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2; const int continueY = bookY + bookHeight - renderer.getLineHeight(UI_10_FONT_ID) * 3 / 2;
if (coverRendered) { if (coverRendered) {
// Draw box behind "Continue Reading" text (inverted when selected: black box instead of white) // Draw box behind "Continue Reading" text (inverted when selected: black box instead of white)
const char* continueText = "Continue Reading"; const char* continueText = TR(CONTINUE_READING);
const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText); const int continueTextWidth = renderer.getTextWidth(UI_10_FONT_ID, continueText);
constexpr int continuePadding = 6; constexpr int continuePadding = 6;
const int continueBoxWidth = continueTextWidth + continuePadding * 2; const int continueBoxWidth = continueTextWidth + continuePadding * 2;
@ -488,22 +489,22 @@ void HomeActivity::render() {
renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected); renderer.drawRect(continueBoxX, continueBoxY, continueBoxWidth, continueBoxHeight, !bookSelected);
renderer.drawCenteredText(UI_10_FONT_ID, continueY, continueText, !bookSelected); renderer.drawCenteredText(UI_10_FONT_ID, continueY, continueText, !bookSelected);
} else { } else {
renderer.drawCenteredText(UI_10_FONT_ID, continueY, "Continue Reading", !bookSelected); renderer.drawCenteredText(UI_10_FONT_ID, continueY, TR(CONTINUE_READING), !bookSelected);
} }
} else { } else {
// No book to continue reading // No book to continue reading
const int y = const int y =
bookY + (bookHeight - renderer.getLineHeight(UI_12_FONT_ID) - renderer.getLineHeight(UI_10_FONT_ID)) / 2; bookY + (bookHeight - renderer.getLineHeight(UI_12_FONT_ID) - renderer.getLineHeight(UI_10_FONT_ID)) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, y, "No open book"); renderer.drawCenteredText(UI_12_FONT_ID, y, TR(NO_OPEN_BOOK));
renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), "Start reading below"); renderer.drawCenteredText(UI_10_FONT_ID, y + renderer.getLineHeight(UI_12_FONT_ID), TR(START_READING));
} }
// --- Bottom menu tiles --- // --- Bottom menu tiles ---
// Build menu items dynamically // Build menu items dynamically
std::vector<const char*> menuItems = {"My Library", "File Transfer", "Settings"}; std::vector<const char*> menuItems = {TR(BROWSE_FILES), TR(FILE_TRANSFER), TR(SETTINGS_TITLE)};
if (hasOpdsUrl) { if (hasOpdsUrl) {
// Insert Calibre Library after My Library // Insert Calibre Library after My Library
menuItems.insert(menuItems.begin() + 1, "Calibre Library"); menuItems.insert(menuItems.begin() + 1, TR(CALIBRE_LIBRARY));
} }
const int menuTileWidth = pageWidth - 2 * margin; const int menuTileWidth = pageWidth - 2 * margin;
@ -541,7 +542,7 @@ void HomeActivity::render() {
renderer.drawText(UI_10_FONT_ID, textX, textY, label, !selected); renderer.drawText(UI_10_FONT_ID, textX, textY, label, !selected);
} }
const auto labels = mappedInput.mapLabels("", "Select", "Up", "Down"); const auto labels = mappedInput.mapLabels("", TR(SELECT), TR(DIR_UP), TR(DIR_DOWN));
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
const bool showBatteryPercentage = const bool showBatteryPercentage =

View File

@ -1,6 +1,7 @@
#include "MyLibraryActivity.h" #include "MyLibraryActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <algorithm> #include <algorithm>
@ -303,7 +304,7 @@ void MyLibraryActivity::render() const {
renderer.clearScreen(); renderer.clearScreen();
// Draw tab bar // Draw tab bar
std::vector<TabInfo> tabs = {{"Recent", currentTab == Tab::Recent}, {"Files", currentTab == Tab::Files}}; std::vector<TabInfo> tabs = {{TR(BOOKS), currentTab == Tab::Recent}, {TR(BROWSE_FILES), currentTab == Tab::Files}};
ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs); ScreenComponents::drawTabBar(renderer, TAB_BAR_Y, tabs);
// Draw content based on current tab // Draw content based on current tab
@ -323,7 +324,7 @@ void MyLibraryActivity::render() const {
renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<"); renderer.drawSideButtonHints(UI_10_FONT_ID, ">", "<");
// Draw bottom button hints // Draw bottom button hints
const auto labels = mappedInput.mapLabels("« Back", "Open", "<", ">"); const auto labels = mappedInput.mapLabels(TR(BACK), TR(OPEN), "<", ">");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
@ -335,7 +336,7 @@ void MyLibraryActivity::renderRecentTab() const {
const int bookCount = static_cast<int>(bookTitles.size()); const int bookCount = static_cast<int>(bookTitles.size());
if (bookCount == 0) { if (bookCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No recent books"); renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, TR(NO_BOOKS_FOUND));
return; return;
} }
@ -359,7 +360,7 @@ void MyLibraryActivity::renderFilesTab() const {
const int fileCount = static_cast<int>(files.size()); const int fileCount = static_cast<int>(files.size());
if (fileCount == 0) { if (fileCount == 0) {
renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, "No books found"); renderer.drawText(UI_10_FONT_ID, LEFT_MARGIN, CONTENT_START_Y, TR(NO_BOOKS_FOUND));
return; return;
} }

View File

@ -2,6 +2,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <I18n.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <WiFi.h> #include <WiFi.h>
@ -34,7 +35,7 @@ void CalibreWirelessActivity::onEnter() {
stateMutex = xSemaphoreCreateMutex(); stateMutex = xSemaphoreCreateMutex();
state = WirelessState::DISCOVERING; state = WirelessState::DISCOVERING;
statusMessage = "Discovering Calibre..."; statusMessage = TR(CALIBRE_DISCOVERING);
errorMessage.clear(); errorMessage.clear();
calibreHostname.clear(); calibreHostname.clear();
calibreHost.clear(); calibreHost.clear();
@ -220,7 +221,7 @@ void CalibreWirelessActivity::listenForDiscovery() {
if (calibrePort > 0) { if (calibrePort > 0) {
// Connect to Calibre's TCP server - try main port first, then alt port // Connect to Calibre's TCP server - try main port first, then alt port
setState(WirelessState::CONNECTING); setState(WirelessState::CONNECTING);
setStatus("Connecting to " + calibreHostname + "..."); setStatus(std::string(TR(CALIBRE_CONNECTING_TO)) + calibreHostname + "...");
// Small delay before connecting // Small delay before connecting
vTaskDelay(100 / portTICK_PERIOD_MS); vTaskDelay(100 / portTICK_PERIOD_MS);
@ -242,11 +243,11 @@ void CalibreWirelessActivity::listenForDiscovery() {
if (connected) { if (connected) {
setState(WirelessState::WAITING); setState(WirelessState::WAITING);
setStatus("Connected to " + calibreHostname + "\nWaiting for commands..."); setStatus(std::string(TR(CALIBRE_CONNECTED_TO)) + calibreHostname + "\n" + TR(CALIBRE_WAITING_COMMANDS));
} else { } else {
// Don't set error yet, keep trying discovery // Don't set error yet, keep trying discovery
setState(WirelessState::DISCOVERING); setState(WirelessState::DISCOVERING);
setStatus("Discovering Calibre...\n(Connection failed, retrying)"); setStatus(std::string(TR(CALIBRE_DISCOVERING)) + "\n" + TR(CONNECTION_FAILED_RETRYING));
calibrePort = 0; calibrePort = 0;
calibreAltPort = 0; calibreAltPort = 0;
} }
@ -258,7 +259,7 @@ void CalibreWirelessActivity::listenForDiscovery() {
void CalibreWirelessActivity::handleTcpClient() { void CalibreWirelessActivity::handleTcpClient() {
if (!tcpClient.connected()) { if (!tcpClient.connected()) {
setState(WirelessState::DISCONNECTED); setState(WirelessState::DISCONNECTED);
setStatus("Calibre disconnected"); setStatus(TR(CALIBRE_DISCONNECTED));
return; return;
} }
@ -455,9 +456,7 @@ void CalibreWirelessActivity::handleCommand(const OpCode opcode, const std::stri
void CalibreWirelessActivity::handleGetInitializationInfo(const std::string& data) { void CalibreWirelessActivity::handleGetInitializationInfo(const std::string& data) {
setState(WirelessState::WAITING); setState(WirelessState::WAITING);
setStatus("Connected to " + calibreHostname + setStatus(std::string(TR(CALIBRE_CONNECTED_TO)) + calibreHostname + "\n" + TR(CALIBRE_WAITING_TRANSFER) + "\n\n" + TR(CALIBRE_TRANSFER_HINT));
"\nWaiting for transfer...\n\nIf transfer fails, enable\n'Ignore free space' in Calibre's\nSmartDevice "
"plugin settings.");
// Build response with device capabilities // Build response with device capabilities
// Format must match what Calibre expects from a smart device // Format must match what Calibre expects from a smart device
@ -589,11 +588,11 @@ void CalibreWirelessActivity::handleSendBook(const std::string& data) {
bytesReceived = 0; bytesReceived = 0;
setState(WirelessState::RECEIVING); setState(WirelessState::RECEIVING);
setStatus("Receiving: " + filename); setStatus(std::string(TR(CALIBRE_RECEIVING)) + filename);
// Open file for writing // Open file for writing
if (!SdMan.openFileForWrite("CAL", currentFilename.c_str(), currentFile)) { if (!SdMan.openFileForWrite("CAL", currentFilename.c_str(), currentFile)) {
setError("Failed to create file"); setError(TR(CALIBRE_FAILED_CREATE_FILE));
sendJsonResponse(OpCode::ERROR, "{\"message\":\"Failed to create file\"}"); sendJsonResponse(OpCode::ERROR, "{\"message\":\"Failed to create file\"}");
return; return;
} }
@ -625,7 +624,7 @@ void CalibreWirelessActivity::handleDisplayMessage(const std::string& data) {
// Calibre may send messages to display // Calibre may send messages to display
// Check messageKind - 1 means password error // Check messageKind - 1 means password error
if (data.find("\"messageKind\":1") != std::string::npos) { if (data.find("\"messageKind\":1") != std::string::npos) {
setError("Password required"); setError(TR(CALIBRE_PASSWORD_REQUIRED));
} }
sendJsonResponse(OpCode::OK, "{}"); sendJsonResponse(OpCode::OK, "{}");
} }
@ -634,7 +633,7 @@ void CalibreWirelessActivity::handleNoop(const std::string& data) {
// Check for ejecting flag // Check for ejecting flag
if (data.find("\"ejecting\":true") != std::string::npos) { if (data.find("\"ejecting\":true") != std::string::npos) {
setState(WirelessState::DISCONNECTED); setState(WirelessState::DISCONNECTED);
setStatus("Calibre disconnected"); setStatus(TR(CALIBRE_DISCONNECTED));
} }
sendJsonResponse(OpCode::NOOP, "{}"); sendJsonResponse(OpCode::NOOP, "{}");
} }
@ -646,7 +645,7 @@ void CalibreWirelessActivity::receiveBinaryData() {
if (!tcpClient.connected()) { if (!tcpClient.connected()) {
currentFile.close(); currentFile.close();
inBinaryMode = false; inBinaryMode = false;
setError("Transfer interrupted"); setError(TR(CALIBRE_TRANSFER_INTERRUPTED));
} }
return; return;
} }
@ -668,7 +667,7 @@ void CalibreWirelessActivity::receiveBinaryData() {
inBinaryMode = false; inBinaryMode = false;
setState(WirelessState::WAITING); setState(WirelessState::WAITING);
setStatus("Received: " + currentFilename + "\nWaiting for more..."); setStatus(std::string(TR(CALIBRE_RECEIVED)) + currentFilename + "\n" + TR(CALIBRE_WAITING_MORE));
// Send OK to acknowledge completion // Send OK to acknowledge completion
sendJsonResponse(OpCode::OK, "{}"); sendJsonResponse(OpCode::OK, "{}");
@ -683,11 +682,11 @@ void CalibreWirelessActivity::render() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 30, "Calibre Wireless", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 30, TR(CALIBRE_WIRELESS), true, EpdFontFamily::BOLD);
// Draw IP address // Draw IP address
const std::string ipAddr = WiFi.localIP().toString().c_str(); const std::string ipAddr = WiFi.localIP().toString().c_str();
renderer.drawCenteredText(UI_10_FONT_ID, 60, ("IP: " + ipAddr).c_str()); renderer.drawCenteredText(UI_10_FONT_ID, 60, (std::string(TR(IP_ADDRESS_PREFIX)) + ipAddr).c_str());
// Draw status message // Draw status message
int statusY = pageHeight / 2 - 40; int statusY = pageHeight / 2 - 40;
@ -720,7 +719,7 @@ void CalibreWirelessActivity::render() const {
} }
// Draw button hints // Draw button hints
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -3,6 +3,7 @@
#include <DNSServer.h> #include <DNSServer.h>
#include <ESPmDNS.h> #include <ESPmDNS.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h> #include <WiFi.h>
#include <esp_task_wdt.h> #include <esp_task_wdt.h>
#include <qrcode.h> #include <qrcode.h>
@ -329,6 +330,9 @@ void CrossPointWebServerActivity::loop() {
// Yield and check for exit button every 64 iterations // Yield and check for exit button every 64 iterations
if ((i & 0x3F) == 0x3F) { if ((i & 0x3F) == 0x3F) {
yield(); yield();
// CRITICAL: Must call update() before wasPressed() to refresh button state
// Otherwise button presses during the loop will be missed
mappedInput.update();
// Check for exit button inside loop for responsiveness // Check for exit button inside loop for responsiveness
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onGoBack(); onGoBack();
@ -369,7 +373,7 @@ void CrossPointWebServerActivity::render() const {
} else if (state == WebServerActivityState::AP_STARTING) { } else if (state == WebServerActivityState::AP_STARTING) {
renderer.clearScreen(); renderer.clearScreen();
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, "Starting Hotspot...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, pageHeight / 2 - 20, TR(STARTING_HOTSPOT), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
} }
} }
@ -400,21 +404,20 @@ void CrossPointWebServerActivity::renderServerRunning() const {
// Use consistent line spacing // Use consistent line spacing
constexpr int LINE_SPACING = 28; // Space between lines constexpr int LINE_SPACING = 28; // Space between lines
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(FILE_TRANSFER), true, EpdFontFamily::BOLD);
if (isApMode) { if (isApMode) {
// AP mode display - center the content block // AP mode display - center the content block
int startY = 55; int startY = 55;
renderer.drawCenteredText(UI_10_FONT_ID, startY, "Hotspot Mode", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, startY, TR(HOTSPOT_MODE), true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + connectedSSID; std::string ssidInfo = std::string(TR(NETWORK_PREFIX)) + connectedSSID;
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ssidInfo.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, "Connect your device to this WiFi network"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 2, TR(CONNECT_WIFI_HINT));
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, TR(SCAN_QR_WIFI_HINT));
"or scan QR code with your phone to connect to Wifi.");
// Show QR code for URL // Show QR code for URL
const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;"; const std::string wifiConfig = std::string("WIFI:S:") + connectedSSID + ";;";
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 4, wifiConfig);
@ -425,24 +428,24 @@ void CrossPointWebServerActivity::renderServerRunning() const {
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str(), true, EpdFontFamily::BOLD);
// Show IP address as fallback // Show IP address as fallback
std::string ipUrl = "or http://" + connectedIP + "/"; std::string ipUrl = std::string(TR(OR_HTTP_PREFIX)) + connectedIP + "/";
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str()); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, ipUrl.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "Open this URL in your browser"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, TR(OPEN_URL_HINT));
// Show QR code for URL // Show QR code for URL
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, "or scan QR code with your phone:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 6, TR(SCAN_QR_HINT));
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 7, hostnameUrl);
} else { } else {
// STA mode display (original behavior) // STA mode display (original behavior)
const int startY = 65; const int startY = 65;
std::string ssidInfo = "Network: " + connectedSSID; std::string ssidInfo = std::string(TR(NETWORK_PREFIX)) + connectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, startY, ssidInfo.c_str());
std::string ipInfo = "IP Address: " + connectedIP; std::string ipInfo = std::string(TR(IP_ADDRESS_PREFIX)) + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING, ipInfo.c_str());
// Show web server URL prominently // Show web server URL prominently
@ -450,16 +453,16 @@ void CrossPointWebServerActivity::renderServerRunning() const {
renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, startY + LINE_SPACING * 2, webInfo.c_str(), true, EpdFontFamily::BOLD);
// Also show hostname URL // Also show hostname URL
std::string hostnameUrl = std::string("or http://") + AP_HOSTNAME + ".local/"; std::string hostnameUrl = std::string(TR(OR_HTTP_PREFIX)) + AP_HOSTNAME + ".local/";
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str()); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 3, hostnameUrl.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, "Open this URL in your browser"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 4, TR(OPEN_URL_HINT));
// Show QR code for URL // Show QR code for URL
drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo); drawQRCode(renderer, (480 - 6 * 33) / 2, startY + LINE_SPACING * 6, webInfo);
renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, "or scan QR code with your phone:"); renderer.drawCenteredText(SMALL_FONT_ID, startY + LINE_SPACING * 5, TR(SCAN_QR_HINT));
} }
const auto labels = mappedInput.mapLabels("« Exit", "", "", ""); const auto labels = mappedInput.mapLabels(TR(EXIT), "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }

View File

@ -1,15 +1,13 @@
#include "NetworkModeSelectionActivity.h" #include "NetworkModeSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "fontIds.h" #include "fontIds.h"
namespace { namespace {
constexpr int MENU_ITEM_COUNT = 2; 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"};
} // namespace } // namespace
void NetworkModeSelectionActivity::taskTrampoline(void* param) { void NetworkModeSelectionActivity::taskTrampoline(void* param) {
@ -97,10 +95,15 @@ void NetworkModeSelectionActivity::render() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "File Transfer", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(FILE_TRANSFER), true, EpdFontFamily::BOLD);
// Draw subtitle // Draw subtitle
renderer.drawCenteredText(UI_10_FONT_ID, 50, "How would you like to connect?"); renderer.drawCenteredText(UI_10_FONT_ID, 50, TR(HOW_CONNECT));
// Menu items and descriptions
const char* menuItems[] = {TR(JOIN_NETWORK), TR(CREATE_HOTSPOT)};
const char* menuDescs[] = {TR(JOIN_DESC), TR(HOTSPOT_DESC)};
constexpr int MENU_ITEM_COUNT = 2;
// Draw menu items centered on screen // Draw menu items centered on screen
constexpr int itemHeight = 50; // Height for each menu item (including description) constexpr int itemHeight = 50; // Height for each menu item (including description)
@ -117,12 +120,12 @@ void NetworkModeSelectionActivity::render() const {
// Draw text: black=false (white text) when selected (on black background) // Draw text: black=false (white text) when selected (on black background)
// black=true (black text) when not selected (on white background) // black=true (black text) when not selected (on white background)
renderer.drawText(UI_10_FONT_ID, 30, itemY, MENU_ITEMS[i], /*black=*/!isSelected); renderer.drawText(UI_10_FONT_ID, 30, itemY, menuItems[i], /*black=*/!isSelected);
renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, MENU_DESCRIPTIONS[i], /*black=*/!isSelected); renderer.drawText(SMALL_FONT_ID, 30, itemY + 22, menuDescs[i], /*black=*/!isSelected);
} }
// Draw help text at bottom // Draw help text at bottom
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), TR(SELECT), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -1,6 +1,7 @@
#include "WifiSelectionActivity.h" #include "WifiSelectionActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h> #include <WiFi.h>
#include <map> #include <map>
@ -199,7 +200,7 @@ void WifiSelectionActivity::selectNetwork(const int index) {
// Don't allow screen updates while changing activity // Don't allow screen updates while changing activity
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Enter WiFi Password", renderer, mappedInput, TR(ENTER_WIFI_PASSWORD),
"", // No initial text "", // No initial text
50, // Y position 50, // Y position
64, // Max password length 64, // Max password length
@ -266,9 +267,9 @@ void WifiSelectionActivity::checkConnectionStatus() {
} }
if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) { if (status == WL_CONNECT_FAILED || status == WL_NO_SSID_AVAIL) {
connectionError = "Connection failed"; connectionError = TR(CONNECTION_FAILED);
if (status == WL_NO_SSID_AVAIL) { if (status == WL_NO_SSID_AVAIL) {
connectionError = "Network not found"; connectionError = TR(NO_NETWORKS);
} }
state = WifiSelectionState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true; updateRequired = true;
@ -278,7 +279,7 @@ void WifiSelectionActivity::checkConnectionStatus() {
// Check for timeout // Check for timeout
if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) { if (millis() - connectionStartTime > CONNECTION_TIMEOUT_MS) {
WiFi.disconnect(); WiFi.disconnect();
connectionError = "Connection timeout"; connectionError = TR(CONNECTION_TIMEOUT);
state = WifiSelectionState::CONNECTION_FAILED; state = WifiSelectionState::CONNECTION_FAILED;
updateRequired = true; updateRequired = true;
return; return;
@ -513,14 +514,14 @@ void WifiSelectionActivity::renderNetworkList() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "WiFi Networks", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(WIFI_NETWORKS), true, EpdFontFamily::BOLD);
if (networks.empty()) { if (networks.empty()) {
// No networks found or scan failed // No networks found or scan failed
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
renderer.drawCenteredText(UI_10_FONT_ID, top, "No networks found"); renderer.drawCenteredText(UI_10_FONT_ID, top, TR(NO_NETWORKS));
renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, "Press OK to scan again"); renderer.drawCenteredText(SMALL_FONT_ID, top + height + 10, TR(PRESS_OK_SCAN));
} else { } else {
// Calculate how many networks we can display // Calculate how many networks we can display
constexpr int startY = 60; constexpr int startY = 60;
@ -584,8 +585,8 @@ void WifiSelectionActivity::renderNetworkList() const {
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str()); renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 105, cachedMacAddress.c_str());
// Draw help text // Draw help text
renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, "* = Encrypted | + = Saved"); renderer.drawText(SMALL_FONT_ID, 20, pageHeight - 75, TR(NETWORK_LEGEND));
const auto labels = mappedInput.mapLabels("« Back", "Connect", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), TR(CONNECT), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
} }
@ -595,13 +596,13 @@ void WifiSelectionActivity::renderConnecting() const {
const auto top = (pageHeight - height) / 2; const auto top = (pageHeight - height) / 2;
if (state == WifiSelectionState::SCANNING) { if (state == WifiSelectionState::SCANNING) {
renderer.drawCenteredText(UI_10_FONT_ID, top, "Scanning..."); renderer.drawCenteredText(UI_10_FONT_ID, top, TR(SCANNING));
} else { } else {
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connecting...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 40, TR(CONNECTING), true, EpdFontFamily::BOLD);
std::string ssidInfo = "to " + selectedSSID; std::string ssidInfo = std::string(TR(NETWORK_PREFIX)) + selectedSSID;
if (ssidInfo.length() > 25) { if (ssidInfo.length() > 28) {
ssidInfo.replace(22, ssidInfo.length() - 22, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str());
} }
@ -612,18 +613,18 @@ void WifiSelectionActivity::renderConnected() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height * 4) / 2; const auto top = (pageHeight - height * 4) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 30, "Connected!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 30, TR(CONNECTED), true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = std::string(TR(NETWORK_PREFIX)) + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, top + 10, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top + 10, ssidInfo.c_str());
const std::string ipInfo = "IP Address: " + connectedIP; const std::string ipInfo = std::string(TR(IP_ADDRESS_PREFIX)) + connectedIP;
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, ipInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top + 40, ipInfo.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, TR(PRESS_ANY_CONTINUE));
} }
void WifiSelectionActivity::renderSavePrompt() const { void WifiSelectionActivity::renderSavePrompt() const {
@ -632,15 +633,15 @@ void WifiSelectionActivity::renderSavePrompt() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height * 3) / 2; const auto top = (pageHeight - height * 3) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Connected!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 40, TR(CONNECTED), true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = std::string(TR(NETWORK_PREFIX)) + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Save password for next time?"); renderer.drawCenteredText(UI_10_FONT_ID, top + 40, TR(SAVE_PASSWORD));
// Draw Yes/No buttons // Draw Yes/No buttons
const int buttonY = top + 80; const int buttonY = top + 80;
@ -651,19 +652,19 @@ void WifiSelectionActivity::renderSavePrompt() const {
// Draw "Yes" button // Draw "Yes" button
if (savePromptSelection == 0) { if (savePromptSelection == 0) {
renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Yes]"); renderer.drawText(UI_10_FONT_ID, startX, buttonY, (std::string("[") + TR(YES) + "]").c_str());
} else { } else {
renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Yes"); renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, TR(YES));
} }
// Draw "No" button // Draw "No" button
if (savePromptSelection == 1) { if (savePromptSelection == 1) {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[No]"); renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, (std::string("[") + TR(NO) + "]").c_str());
} else { } else {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No"); renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, TR(NO));
} }
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, TR(SELECT_HINT));
} }
void WifiSelectionActivity::renderConnectionFailed() const { void WifiSelectionActivity::renderConnectionFailed() const {
@ -671,9 +672,9 @@ void WifiSelectionActivity::renderConnectionFailed() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height * 2) / 2; const auto top = (pageHeight - height * 2) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 20, "Connection Failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 20, TR(CONNECTION_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, top + 20, connectionError.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top + 20, connectionError.c_str());
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "Press any button to continue"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, TR(PRESS_ANY_CONTINUE));
} }
void WifiSelectionActivity::renderForgetPrompt() const { void WifiSelectionActivity::renderForgetPrompt() const {
@ -682,15 +683,15 @@ void WifiSelectionActivity::renderForgetPrompt() const {
const auto height = renderer.getLineHeight(UI_10_FONT_ID); const auto height = renderer.getLineHeight(UI_10_FONT_ID);
const auto top = (pageHeight - height * 3) / 2; const auto top = (pageHeight - height * 3) / 2;
renderer.drawCenteredText(UI_12_FONT_ID, top - 40, "Forget Network?", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, top - 40, TR(FORGET_NETWORK), true, EpdFontFamily::BOLD);
std::string ssidInfo = "Network: " + selectedSSID; std::string ssidInfo = std::string(TR(NETWORK_PREFIX)) + selectedSSID;
if (ssidInfo.length() > 28) { if (ssidInfo.length() > 28) {
ssidInfo.replace(25, ssidInfo.length() - 25, "..."); ssidInfo.replace(25, ssidInfo.length() - 25, "...");
} }
renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, top, ssidInfo.c_str());
renderer.drawCenteredText(UI_10_FONT_ID, top + 40, "Remove saved password?"); renderer.drawCenteredText(UI_10_FONT_ID, top + 40, TR(REMOVE_PASSWORD));
// Draw Yes/No buttons // Draw Yes/No buttons
const int buttonY = top + 80; const int buttonY = top + 80;
@ -701,17 +702,17 @@ void WifiSelectionActivity::renderForgetPrompt() const {
// Draw "Yes" button // Draw "Yes" button
if (forgetPromptSelection == 0) { if (forgetPromptSelection == 0) {
renderer.drawText(UI_10_FONT_ID, startX, buttonY, "[Yes]"); renderer.drawText(UI_10_FONT_ID, startX, buttonY, (std::string("[") + TR(YES) + "]").c_str());
} else { } else {
renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, "Yes"); renderer.drawText(UI_10_FONT_ID, startX + 4, buttonY, TR(YES));
} }
// Draw "No" button // Draw "No" button
if (forgetPromptSelection == 1) { if (forgetPromptSelection == 1) {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, "[No]"); renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing, buttonY, (std::string("[") + TR(NO) + "]").c_str());
} else { } else {
renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, "No"); renderer.drawText(UI_10_FONT_ID, startX + buttonWidth + buttonSpacing + 4, buttonY, TR(NO));
} }
renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, "LEFT/RIGHT: Select | OK: Confirm"); renderer.drawCenteredText(SMALL_FONT_ID, pageHeight - 30, TR(SELECT_HINT));
} }

View File

@ -3,6 +3,7 @@
#include <Epub/Page.h> #include <Epub/Page.h>
#include <FsHelpers.h> #include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@ -258,7 +259,7 @@ void EpubReaderActivity::renderScreen() {
// Show end of book screen // Show end of book screen
if (currentSpineIndex == epub->getSpineItemsCount()) { if (currentSpineIndex == epub->getSpineItemsCount()) {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, TR(END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@ -303,7 +304,7 @@ void EpubReaderActivity::renderScreen() {
// Always show "Indexing..." text first // Always show "Indexing..." text first
{ {
renderer.fillRect(boxXNoBar, boxY, boxWidthNoBar, boxHeightNoBar, false); renderer.fillRect(boxXNoBar, boxY, boxWidthNoBar, boxHeightNoBar, false);
renderer.drawText(UI_12_FONT_ID, boxXNoBar + boxMargin, boxY + boxMargin, "Indexing..."); renderer.drawText(UI_12_FONT_ID, boxXNoBar + boxMargin, boxY + boxMargin, TR(INDEXING));
renderer.drawRect(boxXNoBar + 5, boxY + 5, boxWidthNoBar - 10, boxHeightNoBar - 10); renderer.drawRect(boxXNoBar + 5, boxY + 5, boxWidthNoBar - 10, boxHeightNoBar - 10);
renderer.displayBuffer(); renderer.displayBuffer();
pagesUntilFullRefresh = 0; pagesUntilFullRefresh = 0;
@ -312,7 +313,7 @@ void EpubReaderActivity::renderScreen() {
// Setup callback - only called for chapters >= 50KB, redraws with progress bar // Setup callback - only called for chapters >= 50KB, redraws with progress bar
auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, barX, barY] { auto progressSetup = [this, boxXWithBar, boxWidthWithBar, boxHeightWithBar, barX, barY] {
renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false); renderer.fillRect(boxXWithBar, boxY, boxWidthWithBar, boxHeightWithBar, false);
renderer.drawText(UI_12_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, "Indexing..."); renderer.drawText(UI_12_FONT_ID, boxXWithBar + boxMargin, boxY + boxMargin, TR(INDEXING));
renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10); renderer.drawRect(boxXWithBar + 5, boxY + 5, boxWidthWithBar - 10, boxHeightWithBar - 10);
renderer.drawRect(barX, barY, barWidth, barHeight); renderer.drawRect(barX, barY, barWidth, barHeight);
renderer.displayBuffer(); renderer.displayBuffer();
@ -347,7 +348,7 @@ void EpubReaderActivity::renderScreen() {
if (section->pageCount == 0) { if (section->pageCount == 0) {
Serial.printf("[%lu] [ERS] No pages to render\n", millis()); Serial.printf("[%lu] [ERS] No pages to render\n", millis());
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty chapter", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, TR(EMPTY_CHAPTER), true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@ -355,7 +356,7 @@ void EpubReaderActivity::renderScreen() {
if (section->currentPage < 0 || section->currentPage >= section->pageCount) { if (section->currentPage < 0 || section->currentPage >= section->pageCount) {
Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount); Serial.printf("[%lu] [ERS] Page out of bounds: %d (max %d)\n", millis(), section->currentPage, section->pageCount);
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Out of bounds", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, TR(OUT_OF_BOUNDS), true, EpdFontFamily::BOLD);
renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft); renderStatusBar(orientedMarginRight, orientedMarginBottom, orientedMarginLeft);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@ -478,8 +479,8 @@ void EpubReaderActivity::renderStatusBar(const int orientedMarginRight, const in
std::string title; std::string title;
int titleWidth; int titleWidth;
if (tocIndex == -1) { if (tocIndex == -1) {
title = "Unnamed"; title = TR(UNNAMED);
titleWidth = renderer.getTextWidth(SMALL_FONT_ID, "Unnamed"); titleWidth = renderer.getTextWidth(SMALL_FONT_ID, TR(UNNAMED));
} else { } else {
const auto tocItem = epub->getTocItem(tocIndex); const auto tocItem = epub->getTocItem(tocIndex);
title = tocItem.title; title = tocItem.title;

View File

@ -1,6 +1,7 @@
#include "TxtReaderActivity.h" #include "TxtReaderActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <Serialization.h> #include <Serialization.h>
#include <Utf8.h> #include <Utf8.h>
@ -206,7 +207,7 @@ void TxtReaderActivity::buildPageIndex() {
// Draw initial progress box // Draw initial progress box
renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false); renderer.fillRect(boxX, boxY, boxWidth, boxHeight, false);
renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, "Indexing..."); renderer.drawText(UI_12_FONT_ID, boxX + boxMargin, boxY + boxMargin, TR(INDEXING));
renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10); renderer.drawRect(boxX + 5, boxY + 5, boxWidth - 10, boxHeight - 10);
renderer.drawRect(barX, barY, barWidth, barHeight); renderer.drawRect(barX, barY, barWidth, barHeight);
renderer.displayBuffer(); renderer.displayBuffer();
@ -384,14 +385,14 @@ void TxtReaderActivity::renderScreen() {
// Initialize reader if not done // Initialize reader if not done
if (!initialized) { if (!initialized) {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Indexing...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, TR(INDEXING), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
initializeReader(); initializeReader();
} }
if (pageOffsets.empty()) { if (pageOffsets.empty()) {
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Empty file", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, TR(EMPTY_FILE), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }

View File

@ -9,6 +9,7 @@
#include <FsHelpers.h> #include <FsHelpers.h>
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
@ -169,7 +170,7 @@ void XtcReaderActivity::renderScreen() {
if (currentPage >= xtc->getPageCount()) { if (currentPage >= xtc->getPageCount()) {
// Show end of book screen // Show end of book screen
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "End of book", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, TR(END_OF_BOOK), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@ -198,7 +199,7 @@ void XtcReaderActivity::renderPage() {
if (!pageBuffer) { if (!pageBuffer) {
Serial.printf("[%lu] [XTR] Failed to allocate page buffer (%lu bytes)\n", millis(), pageBufferSize); Serial.printf("[%lu] [XTR] Failed to allocate page buffer (%lu bytes)\n", millis(), pageBufferSize);
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Memory error", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, TR(MEMORY_ERROR), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
@ -209,7 +210,7 @@ void XtcReaderActivity::renderPage() {
Serial.printf("[%lu] [XTR] Failed to load page %lu\n", millis(), currentPage); Serial.printf("[%lu] [XTR] Failed to load page %lu\n", millis(), currentPage);
free(pageBuffer); free(pageBuffer);
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 300, "Page load error", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 300, TR(PAGE_LOAD_ERROR), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }

View File

@ -1,6 +1,7 @@
#include "CalibreSettingsActivity.h" #include "CalibreSettingsActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h> #include <WiFi.h>
#include <cstring> #include <cstring>
@ -12,11 +13,6 @@
#include "activities/util/KeyboardEntryActivity.h" #include "activities/util/KeyboardEntryActivity.h"
#include "fontIds.h" #include "fontIds.h"
namespace {
constexpr int MENU_ITEMS = 2;
const char* menuNames[MENU_ITEMS] = {"Calibre Web URL", "Connect as Wireless Device"};
} // namespace
void CalibreSettingsActivity::taskTrampoline(void* param) { void CalibreSettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<CalibreSettingsActivity*>(param); auto* self = static_cast<CalibreSettingsActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
@ -27,7 +23,7 @@ void CalibreSettingsActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
selectedIndex = 0; selectedIndex = 0;
updateRequired = true; updateRequired = false; // Don't trigger render immediately to avoid race with parent activity
xTaskCreate(&CalibreSettingsActivity::taskTrampoline, "CalibreSettingsTask", xTaskCreate(&CalibreSettingsActivity::taskTrampoline, "CalibreSettingsTask",
4096, // Stack size 4096, // Stack size
@ -67,23 +63,24 @@ void CalibreSettingsActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) { mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; selectedIndex = (selectedIndex + 2 - 1) % 2;
updateRequired = true; updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) { mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % MENU_ITEMS; selectedIndex = (selectedIndex + 1) % 2;
updateRequired = true; updateRequired = true;
} }
} }
void CalibreSettingsActivity::handleSelection() { void CalibreSettingsActivity::handleSelection() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); // Don't hold mutex while creating subactivities to avoid race conditions
// between parent and child rendering tasks
if (selectedIndex == 0) { if (selectedIndex == 0) {
// Calibre Web URL // Calibre Web URL
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Calibre Web URL", SETTINGS.opdsServerUrl, 10, renderer, mappedInput, TR(CALIBRE_WEB_URL), SETTINGS.opdsServerUrl, 10,
127, // maxLength 127, // maxLength
false, // not password false, // not password
[this](const std::string& url) { [this](const std::string& url) {
@ -119,11 +116,14 @@ void CalibreSettingsActivity::handleSelection() {
})); }));
} }
} }
xSemaphoreGive(renderingMutex);
} }
void CalibreSettingsActivity::displayTaskLoop() { void CalibreSettingsActivity::displayTaskLoop() {
// Wait for parent activity's rendering to complete (screen refresh takes ~422ms)
// Wait 500ms to be safe and avoid race conditions with parent activity
vTaskDelay(500 / portTICK_PERIOD_MS);
updateRequired = true;
while (true) { while (true) {
if (updateRequired && !subActivity) { if (updateRequired && !subActivity) {
updateRequired = false; updateRequired = false;
@ -141,13 +141,14 @@ void CalibreSettingsActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Calibre", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(CALIBRE), true, EpdFontFamily::BOLD);
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30);
// Draw menu items // Draw menu items
for (int i = 0; i < MENU_ITEMS; i++) { const char* menuNames[2] = {TR(CALIBRE_WEB_URL), TR(CONNECT_WIRELESS)};
for (int i = 0; i < 2; i++) {
const int settingY = 60 + i * 30; const int settingY = 60 + i * 30;
const bool isSelected = (i == selectedIndex); const bool isSelected = (i == selectedIndex);
@ -155,14 +156,14 @@ void CalibreSettingsActivity::render() {
// Draw status for URL setting // Draw status for URL setting
if (i == 0) { if (i == 0) {
const char* status = (strlen(SETTINGS.opdsServerUrl) > 0) ? "[Set]" : "[Not Set]"; const char* status = (strlen(SETTINGS.opdsServerUrl) > 0) ? TR(SET) : TR(NOT_SET);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected); renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, settingY, status, !isSelected);
} }
} }
// Draw button hints // Draw button hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), TR(SELECT), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -2,13 +2,14 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <I18n.h>
#include <cstring>
#include "CalibreSettingsActivity.h" #include "CalibreSettingsActivity.h"
#include "ClearCacheActivity.h" #include "ClearCacheActivity.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "FontSelectActivity.h"
#include "KOReaderSettingsActivity.h" #include "KOReaderSettingsActivity.h"
#include "LanguageSelectActivity.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "OtaUpdateActivity.h" #include "OtaUpdateActivity.h"
#include "fontIds.h" #include "fontIds.h"
@ -51,7 +52,11 @@ void CategorySettingsActivity::loop() {
// Handle actions with early return // Handle actions with early return
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
toggleCurrentSetting(); toggleCurrentSetting();
// Only update if we didn't enter a subactivity
// If we entered a subactivity, it will handle its own rendering
if (!subActivity) {
updateRequired = true; updateRequired = true;
}
return; return;
} }
@ -95,38 +100,45 @@ void CategorySettingsActivity::toggleCurrentSetting() {
SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step; SETTINGS.*(setting.valuePtr) = currentValue + setting.valueRange.step;
} }
} else if (setting.type == SettingType::ACTION) { } else if (setting.type == SettingType::ACTION) {
if (strcmp(setting.name, "KOReader Sync") == 0) { // 创建新的 Activity 但不持有 mutex
xSemaphoreTake(renderingMutex, portMAX_DELAY); // 注意:不能在持有 mutex 的情况下调用 enterNewActivity
exitActivity(); // 因为新 Activity 的 onEnter 会创建自己的任务,可能导致 FreeRTOS 冲突
if (setting.nameId == StrId::KOREADER_SYNC) {
enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] { enterNewActivity(new KOReaderSettingsActivity(renderer, mappedInput, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); } else if (setting.nameId == StrId::CALIBRE_SETTINGS) {
} else if (strcmp(setting.name, "Calibre Settings") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] { enterNewActivity(new CalibreSettingsActivity(renderer, mappedInput, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); } else if (setting.nameId == StrId::CLEAR_READING_CACHE) {
} else if (strcmp(setting.name, "Clear Cache") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] { enterNewActivity(new ClearCacheActivity(renderer, mappedInput, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); } else if (setting.nameId == StrId::CHECK_UPDATES) {
} else if (strcmp(setting.name, "Check for updates") == 0) {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
exitActivity();
enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] { enterNewActivity(new OtaUpdateActivity(renderer, mappedInput, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
})); }));
xSemaphoreGive(renderingMutex); } else if (setting.nameId == StrId::EXT_UI_FONT) {
enterNewActivity(new FontSelectActivity(renderer, mappedInput, FontSelectActivity::SelectMode::UI, [this] {
exitActivity();
updateRequired = true;
}));
} else if (setting.nameId == StrId::EXT_READER_FONT) {
enterNewActivity(new FontSelectActivity(renderer, mappedInput, FontSelectActivity::SelectMode::Reader, [this] {
exitActivity();
updateRequired = true;
}));
} else if (setting.nameId == StrId::LANGUAGE) {
enterNewActivity(new LanguageSelectActivity(renderer, mappedInput, [this] {
exitActivity();
updateRequired = true;
}));
} }
} else { } else {
return; return;
@ -137,6 +149,8 @@ void CategorySettingsActivity::toggleCurrentSetting() {
void CategorySettingsActivity::displayTaskLoop() { void CategorySettingsActivity::displayTaskLoop() {
while (true) { while (true) {
// CRITICAL: Check both updateRequired AND subActivity atomically
// This prevents race condition where parent and child render simultaneously
if (updateRequired && !subActivity) { if (updateRequired && !subActivity) {
updateRequired = false; updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
@ -163,17 +177,17 @@ void CategorySettingsActivity::render() const {
const int settingY = 60 + i * 30; // 30 pixels between settings const int settingY = 60 + i * 30; // 30 pixels between settings
const bool isSelected = (i == selectedSettingIndex); const bool isSelected = (i == selectedSettingIndex);
// Draw setting name // Draw setting name (translated)
renderer.drawText(UI_10_FONT_ID, 20, settingY, settingsList[i].name, !isSelected); renderer.drawText(UI_10_FONT_ID, 20, settingY, I18N.get(settingsList[i].nameId), !isSelected);
// Draw value based on setting type // Draw value based on setting type
std::string valueText; std::string valueText;
if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) { if (settingsList[i].type == SettingType::TOGGLE && settingsList[i].valuePtr != nullptr) {
const bool value = SETTINGS.*(settingsList[i].valuePtr); const bool value = SETTINGS.*(settingsList[i].valuePtr);
valueText = value ? "ON" : "OFF"; valueText = value ? TR(ON) : TR(OFF);
} else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) { } else if (settingsList[i].type == SettingType::ENUM && settingsList[i].valuePtr != nullptr) {
const uint8_t value = SETTINGS.*(settingsList[i].valuePtr); const uint8_t value = SETTINGS.*(settingsList[i].valuePtr);
valueText = settingsList[i].enumValues[value]; valueText = I18N.get(settingsList[i].enumValues[value]);
} else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) { } else if (settingsList[i].type == SettingType::VALUE && settingsList[i].valuePtr != nullptr) {
valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr)); valueText = std::to_string(SETTINGS.*(settingsList[i].valuePtr));
} }
@ -186,7 +200,7 @@ void CategorySettingsActivity::render() const {
renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION), renderer.drawText(SMALL_FONT_ID, pageWidth - 20 - renderer.getTextWidth(SMALL_FONT_ID, CROSSPOINT_VERSION),
pageHeight - 60, CROSSPOINT_VERSION); pageHeight - 60, CROSSPOINT_VERSION);
const auto labels = mappedInput.mapLabels("« Back", "Toggle", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), TR(TOGGLE), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -2,6 +2,7 @@
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
#include <freertos/task.h> #include <freertos/task.h>
#include <I18n.h>
#include <functional> #include <functional>
#include <string> #include <string>
@ -14,10 +15,10 @@ class CrossPointSettings;
enum class SettingType { TOGGLE, ENUM, ACTION, VALUE }; enum class SettingType { TOGGLE, ENUM, ACTION, VALUE };
struct SettingInfo { struct SettingInfo {
const char* name; StrId nameId;
SettingType type; SettingType type;
uint8_t CrossPointSettings::* valuePtr; uint8_t CrossPointSettings::* valuePtr;
std::vector<std::string> enumValues; std::vector<StrId> enumValues;
struct ValueRange { struct ValueRange {
uint8_t min; uint8_t min;
@ -26,18 +27,18 @@ struct SettingInfo {
}; };
ValueRange valueRange; ValueRange valueRange;
static SettingInfo Toggle(const char* name, uint8_t CrossPointSettings::* ptr) { static SettingInfo Toggle(StrId nameId, uint8_t CrossPointSettings::* ptr) {
return {name, SettingType::TOGGLE, ptr}; return {nameId, SettingType::TOGGLE, ptr};
} }
static SettingInfo Enum(const char* name, uint8_t CrossPointSettings::* ptr, std::vector<std::string> values) { static SettingInfo Enum(StrId nameId, uint8_t CrossPointSettings::* ptr, std::vector<StrId> values) {
return {name, SettingType::ENUM, ptr, std::move(values)}; return {nameId, SettingType::ENUM, ptr, std::move(values)};
} }
static SettingInfo Action(const char* name) { return {name, SettingType::ACTION, nullptr}; } static SettingInfo Action(StrId nameId) { return {nameId, SettingType::ACTION, nullptr}; }
static SettingInfo Value(const char* name, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) { static SettingInfo Value(StrId nameId, uint8_t CrossPointSettings::* ptr, const ValueRange valueRange) {
return {name, SettingType::VALUE, ptr, {}, valueRange}; return {nameId, SettingType::VALUE, ptr, {}, valueRange};
} }
}; };

View File

@ -2,6 +2,7 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <I18n.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
@ -17,7 +18,7 @@ void ClearCacheActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
state = WARNING; state = WARNING;
updateRequired = true; updateRequired = false; // Don't trigger render immediately to avoid race with parent activity
xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask", xTaskCreate(&ClearCacheActivity::taskTrampoline, "ClearCacheActivityTask",
4096, // Stack size 4096, // Stack size
@ -30,6 +31,21 @@ void ClearCacheActivity::onEnter() {
void ClearCacheActivity::onExit() { void ClearCacheActivity::onExit() {
ActivityWithSubactivity::onExit(); ActivityWithSubactivity::onExit();
// Set exit flag to prevent clearCache from accessing mutex after deletion
isExiting = true;
// Wait for clearCache task to complete (max 10 seconds)
if (clearCacheTaskHandle) {
for (int i = 0; i < 1000 && clearCacheTaskHandle != nullptr; i++) {
vTaskDelay(10 / portTICK_PERIOD_MS);
}
// Force delete if still running (shouldn't happen)
if (clearCacheTaskHandle) {
vTaskDelete(clearCacheTaskHandle);
clearCacheTaskHandle = nullptr;
}
}
// Wait until not rendering to delete task to avoid killing mid-instruction to EPD // Wait until not rendering to delete task to avoid killing mid-instruction to EPD
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) { if (displayTaskHandle) {
@ -41,8 +57,15 @@ void ClearCacheActivity::onExit() {
} }
void ClearCacheActivity::displayTaskLoop() { void ClearCacheActivity::displayTaskLoop() {
// Wait for parent activity's rendering to complete (screen refresh takes ~422ms)
// Wait 500ms to be safe and avoid race conditions with parent activity
vTaskDelay(500 / portTICK_PERIOD_MS);
updateRequired = true;
while (true) { while (true) {
if (updateRequired) { // CRITICAL: Check both updateRequired AND subActivity atomically
// This prevents race condition where parent and child render simultaneously
if (updateRequired && !subActivity) {
updateRequired = false; updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
render(); render();
@ -56,46 +79,46 @@ void ClearCacheActivity::render() {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Clear Cache", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(CLEAR_READING_CACHE), true, EpdFontFamily::BOLD);
if (state == WARNING) { if (state == WARNING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, "This will clear all cached book data.", true); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 60, TR(CLEAR_CACHE_WARNING_1), true);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 30, "All reading progress will be lost!", true, renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 30, TR(CLEAR_CACHE_WARNING_2), true,
EpdFontFamily::BOLD); EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Books will need to be re-indexed", true); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, TR(CLEAR_CACHE_WARNING_3), true);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 30, "when opened again.", true); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 30, TR(CLEAR_CACHE_WARNING_4), true);
const auto labels = mappedInput.mapLabels("« Cancel", "Clear", "", ""); const auto labels = mappedInput.mapLabels(TR(CANCEL), TR(CONFIRM), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == CLEARING) { if (state == CLEARING) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, "Clearing cache...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2, TR(CLEARING_CACHE), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == SUCCESS) { if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Cache Cleared", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, TR(CACHE_CLEARED), true, EpdFontFamily::BOLD);
String resultText = String(clearedCount) + " items removed"; String resultText = String(clearedCount) + " " + TR(ITEMS_REMOVED);
if (failedCount > 0) { if (failedCount > 0) {
resultText += ", " + String(failedCount) + " failed"; resultText += ", " + String(failedCount) + " " + TR(FAILED_LOWER);
} }
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, resultText.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, resultText.c_str());
const auto labels = mappedInput.mapLabels("« Back", "", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FAILED) { if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, "Failed to clear cache", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 - 20, TR(CLEAR_CACHE_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, "Check serial output for details"); renderer.drawCenteredText(UI_10_FONT_ID, pageHeight / 2 + 10, TR(CHECK_SERIAL_OUTPUT));
const auto labels = mappedInput.mapLabels("« Back", "", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
@ -105,13 +128,23 @@ void ClearCacheActivity::render() {
void ClearCacheActivity::clearCache() { void ClearCacheActivity::clearCache() {
Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis()); Serial.printf("[%lu] [CLEAR_CACHE] Clearing cache...\n", millis());
// Check if exiting before starting
if (isExiting) {
Serial.printf("[%lu] [CLEAR_CACHE] Aborted: activity is exiting\n", millis());
clearCacheTaskHandle = nullptr;
return;
}
// Open .crosspoint directory // Open .crosspoint directory
auto root = SdMan.open("/.crosspoint"); auto root = SdMan.open("/.crosspoint");
if (!root || !root.isDirectory()) { if (!root || !root.isDirectory()) {
Serial.printf("[%lu] [CLEAR_CACHE] Failed to open cache directory\n", millis()); Serial.printf("[%lu] [CLEAR_CACHE] Failed to open cache directory\n", millis());
if (root) root.close(); if (root) root.close();
if (!isExiting) {
state = FAILED; state = FAILED;
updateRequired = true; updateRequired = true;
}
clearCacheTaskHandle = nullptr;
return; return;
} }
@ -121,6 +154,15 @@ void ClearCacheActivity::clearCache() {
// Iterate through all entries in the directory // Iterate through all entries in the directory
for (auto file = root.openNextFile(); file; file = root.openNextFile()) { for (auto file = root.openNextFile(); file; file = root.openNextFile()) {
// Check if exiting during iteration
if (isExiting) {
file.close();
root.close();
Serial.printf("[%lu] [CLEAR_CACHE] Aborted during iteration\n", millis());
clearCacheTaskHandle = nullptr;
return;
}
file.getName(name, sizeof(name)); file.getName(name, sizeof(name));
String itemName(name); String itemName(name);
@ -145,21 +187,30 @@ void ClearCacheActivity::clearCache() {
Serial.printf("[%lu] [CLEAR_CACHE] Cache cleared: %d removed, %d failed\n", millis(), clearedCount, failedCount); Serial.printf("[%lu] [CLEAR_CACHE] Cache cleared: %d removed, %d failed\n", millis(), clearedCount, failedCount);
// Only update state if not exiting
if (!isExiting) {
state = SUCCESS; state = SUCCESS;
updateRequired = true; updateRequired = true;
} }
clearCacheTaskHandle = nullptr;
}
void ClearCacheActivity::loop() { void ClearCacheActivity::loop() {
if (state == WARNING) { if (state == WARNING) {
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) { if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
Serial.printf("[%lu] [CLEAR_CACHE] User confirmed, starting cache clear\n", millis()); Serial.printf("[%lu] [CLEAR_CACHE] User confirmed, starting cache clear\n", millis());
xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = CLEARING; state = CLEARING;
xSemaphoreGive(renderingMutex);
updateRequired = true; updateRequired = true;
vTaskDelay(10 / portTICK_PERIOD_MS);
clearCache(); // Run clearCache in a separate task to avoid blocking loop()
xTaskCreate(
[](void* param) {
auto* self = static_cast<ClearCacheActivity*>(param);
self->clearCache();
vTaskDelete(nullptr);
},
"ClearCacheTask", 4096, this, 1, &clearCacheTaskHandle);
} }
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) { if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {

View File

@ -23,8 +23,10 @@ class ClearCacheActivity final : public ActivityWithSubactivity {
State state = WARNING; State state = WARNING;
TaskHandle_t displayTaskHandle = nullptr; TaskHandle_t displayTaskHandle = nullptr;
TaskHandle_t clearCacheTaskHandle = nullptr; // Track clearCache task
SemaphoreHandle_t renderingMutex = nullptr; SemaphoreHandle_t renderingMutex = nullptr;
bool updateRequired = false; bool updateRequired = false;
bool isExiting = false; // Flag to prevent new operations during exit
const std::function<void()> goBack; const std::function<void()> goBack;
int clearedCount = 0; int clearedCount = 0;

View File

@ -0,0 +1,164 @@
#include "FontSelectActivity.h"
#include <FontManager.h>
#include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h"
#include "fontIds.h"
void FontSelectActivity::onEnter() {
ActivityWithSubactivity::onEnter();
// Wait for parent activity's rendering to complete (screen refresh takes ~422ms)
// Wait 500ms to be safe and avoid race conditions with parent activity
vTaskDelay(500 / portTICK_PERIOD_MS);
// Scan fonts
FontMgr.scanFonts();
// Total items = 1 (Built-in) + external font count
totalItems = 1 + FontMgr.getFontCount();
// Set current selection based on mode
int currentFont = (mode == SelectMode::Reader) ? FontMgr.getSelectedIndex()
: FontMgr.getUiSelectedIndex();
if (currentFont < 0) {
selectedIndex = 0; // Built-in
} else {
selectedIndex = currentFont + 1; // External font index + 1
}
// 同步渲染,不使用后台任务
render();
}
void FontSelectActivity::onExit() {
ActivityWithSubactivity::onExit();
// 不需要清理任务和 mutex因为我们不再使用它们
}
void FontSelectActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBack();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
}
bool needsRender = false;
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex + totalItems - 1) % totalItems;
needsRender = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % totalItems;
needsRender = true;
}
// 同步渲染
if (needsRender) {
render();
}
}
void FontSelectActivity::handleSelection() {
Serial.printf("[FONT_SELECT] handleSelection: mode=%d, selectedIndex=%d\n",
static_cast<int>(mode), selectedIndex);
if (selectedIndex == 0) {
// Select Built-in (disable external font)
if (mode == SelectMode::Reader) {
Serial.printf("[FONT_SELECT] Disabling reader font\n");
FontMgr.selectFont(-1);
} else {
Serial.printf("[FONT_SELECT] Disabling UI font\n");
FontMgr.selectUiFont(-1);
}
} else {
// Select external font
if (mode == SelectMode::Reader) {
Serial.printf("[FONT_SELECT] Selecting reader font index %d\n", selectedIndex - 1);
FontMgr.selectFont(selectedIndex - 1);
} else {
Serial.printf("[FONT_SELECT] Selecting UI font index %d\n", selectedIndex - 1);
FontMgr.selectUiFont(selectedIndex - 1);
}
}
Serial.printf("[FONT_SELECT] After selection: readerIndex=%d, uiIndex=%d\n",
FontMgr.getSelectedIndex(), FontMgr.getUiSelectedIndex());
// Return to previous page
onBack();
}
void FontSelectActivity::render() {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
constexpr int rowHeight = 30;
// Title
const char *title = (mode == SelectMode::Reader) ? TR(EXT_CHINESE_FONT)
: TR(EXT_UI_FONT);
renderer.drawCenteredText(UI_12_FONT_ID, 15, title, true,
EpdFontFamily::BOLD);
// Current selected font marker
int currentFont = (mode == SelectMode::Reader) ? FontMgr.getSelectedIndex()
: FontMgr.getUiSelectedIndex();
// Draw options
for (int i = 0; i < totalItems && i < 20; i++) { // Max 20 items
const int itemY = 60 + i * rowHeight;
const bool isSelected = (i == selectedIndex);
const bool isCurrent =
(i == 0 && currentFont < 0) || (i > 0 && i - 1 == currentFont);
// Draw selection highlight
if (isSelected) {
renderer.fillRect(0, itemY - 2, pageWidth - 1, rowHeight);
}
// Draw text
if (i == 0) {
// Built-in option
renderer.drawText(UI_10_FONT_ID, 20, itemY, TR(BUILTIN_DISABLED),
!isSelected);
} else {
// External font
const FontInfo *info = FontMgr.getFontInfo(i - 1);
if (info) {
char label[64];
snprintf(label, sizeof(label), "%s (%dpt)", info->name, info->size);
renderer.drawText(UI_10_FONT_ID, 20, itemY, label, !isSelected);
}
}
// Draw current selection marker
if (isCurrent) {
const char* marker = TR(ON);
const auto width = renderer.getTextWidth(UI_10_FONT_ID, marker);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, itemY,
marker, !isSelected);
}
}
// Button hints
const auto labels = mappedInput.mapLabels(TR(BACK), TR(SELECT), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3,
labels.btn4);
renderer.displayBuffer();
}

View File

@ -0,0 +1,33 @@
#pragma once
#include <functional>
#include "activities/ActivityWithSubactivity.h"
/**
* Font selection page
* Shows available external fonts and allows user to select
* Uses synchronous rendering (no background task) to avoid FreeRTOS conflicts
*/
class FontSelectActivity final : public ActivityWithSubactivity {
public:
enum class SelectMode { Reader, UI };
explicit FontSelectActivity(GfxRenderer &renderer,
MappedInputManager &mappedInput, SelectMode mode,
const std::function<void()> &onBack)
: ActivityWithSubactivity("FontSelect", renderer, mappedInput),
mode(mode), onBack(onBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
private:
SelectMode mode;
int selectedIndex = 0; // 0 = Built-in, 1+ = external fonts
int totalItems = 1; // At least "Built-in" option
const std::function<void()> onBack;
void render();
void handleSelection();
};

View File

@ -1,6 +1,7 @@
#include "KOReaderAuthActivity.h" #include "KOReaderAuthActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h> #include <WiFi.h>
#include "KOReaderCredentialStore.h" #include "KOReaderCredentialStore.h"
@ -20,7 +21,7 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
if (!success) { if (!success) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = FAILED; state = FAILED;
errorMessage = "WiFi connection failed"; errorMessage = TR(WIFI_CONN_FAILED);
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
updateRequired = true; updateRequired = true;
return; return;
@ -28,7 +29,7 @@ void KOReaderAuthActivity::onWifiSelectionComplete(const bool success) {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
state = AUTHENTICATING; state = AUTHENTICATING;
statusMessage = "Authenticating..."; statusMessage = TR(AUTHENTICATING);
xSemaphoreGive(renderingMutex); xSemaphoreGive(renderingMutex);
updateRequired = true; updateRequired = true;
@ -41,7 +42,7 @@ void KOReaderAuthActivity::performAuthentication() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (result == KOReaderSyncClient::OK) { if (result == KOReaderSyncClient::OK) {
state = SUCCESS; state = SUCCESS;
statusMessage = "Successfully authenticated!"; statusMessage = TR(AUTH_SUCCESS);
} else { } else {
state = FAILED; state = FAILED;
errorMessage = KOReaderSyncClient::errorString(result); errorMessage = KOReaderSyncClient::errorString(result);
@ -68,7 +69,7 @@ void KOReaderAuthActivity::onEnter() {
// Check if already connected // Check if already connected
if (WiFi.status() == WL_CONNECTED) { if (WiFi.status() == WL_CONNECTED) {
state = AUTHENTICATING; state = AUTHENTICATING;
statusMessage = "Authenticating..."; statusMessage = TR(AUTHENTICATING);
updateRequired = true; updateRequired = true;
// Perform authentication in a separate task // Perform authentication in a separate task
@ -123,7 +124,7 @@ void KOReaderAuthActivity::render() {
} }
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Auth", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(KOREADER_AUTH), true, EpdFontFamily::BOLD);
if (state == AUTHENTICATING) { if (state == AUTHENTICATING) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, statusMessage.c_str(), true, EpdFontFamily::BOLD);
@ -132,20 +133,20 @@ void KOReaderAuthActivity::render() {
} }
if (state == SUCCESS) { if (state == SUCCESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Success!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 280, TR(AUTH_SUCCESS), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, "KOReader sync is ready to use"); renderer.drawCenteredText(UI_10_FONT_ID, 320, TR(SYNC_READY));
const auto labels = mappedInput.mapLabels("Done", "", "", ""); const auto labels = mappedInput.mapLabels(TR(DONE), "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FAILED) { if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 280, "Authentication Failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 280, TR(AUTH_FAILED), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str()); renderer.drawCenteredText(UI_10_FONT_ID, 320, errorMessage.c_str());
const auto labels = mappedInput.mapLabels("Back", "", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), "", "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;

View File

@ -1,6 +1,7 @@
#include "KOReaderSettingsActivity.h" #include "KOReaderSettingsActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <cstring> #include <cstring>
@ -10,11 +11,6 @@
#include "activities/util/KeyboardEntryActivity.h" #include "activities/util/KeyboardEntryActivity.h"
#include "fontIds.h" #include "fontIds.h"
namespace {
constexpr int MENU_ITEMS = 5;
const char* menuNames[MENU_ITEMS] = {"Username", "Password", "Sync Server URL", "Document Matching", "Authenticate"};
} // namespace
void KOReaderSettingsActivity::taskTrampoline(void* param) { void KOReaderSettingsActivity::taskTrampoline(void* param) {
auto* self = static_cast<KOReaderSettingsActivity*>(param); auto* self = static_cast<KOReaderSettingsActivity*>(param);
self->displayTaskLoop(); self->displayTaskLoop();
@ -25,7 +21,7 @@ void KOReaderSettingsActivity::onEnter() {
renderingMutex = xSemaphoreCreateMutex(); renderingMutex = xSemaphoreCreateMutex();
selectedIndex = 0; selectedIndex = 0;
updateRequired = true; updateRequired = false; // Don't trigger render immediately to avoid race with parent activity
xTaskCreate(&KOReaderSettingsActivity::taskTrampoline, "KOReaderSettingsTask", xTaskCreate(&KOReaderSettingsActivity::taskTrampoline, "KOReaderSettingsTask",
4096, // Stack size 4096, // Stack size
@ -65,23 +61,24 @@ void KOReaderSettingsActivity::loop() {
if (mappedInput.wasPressed(MappedInputManager::Button::Up) || if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) { mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex + MENU_ITEMS - 1) % MENU_ITEMS; selectedIndex = (selectedIndex + 5 - 1) % 5;
updateRequired = true; updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) || } else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) { mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % MENU_ITEMS; selectedIndex = (selectedIndex + 1) % 5;
updateRequired = true; updateRequired = true;
} }
} }
void KOReaderSettingsActivity::handleSelection() { void KOReaderSettingsActivity::handleSelection() {
xSemaphoreTake(renderingMutex, portMAX_DELAY); // Don't hold mutex while creating subactivities to avoid race conditions
// between parent and child rendering tasks
if (selectedIndex == 0) { if (selectedIndex == 0) {
// Username // Username
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "KOReader Username", KOREADER_STORE.getUsername(), 10, renderer, mappedInput, TR(KOREADER_USERNAME), KOREADER_STORE.getUsername(), 10,
64, // maxLength 64, // maxLength
false, // not password false, // not password
[this](const std::string& username) { [this](const std::string& username) {
@ -98,7 +95,7 @@ void KOReaderSettingsActivity::handleSelection() {
// Password // Password
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "KOReader Password", KOREADER_STORE.getPassword(), 10, renderer, mappedInput, TR(KOREADER_PASSWORD), KOREADER_STORE.getPassword(), 10,
64, // maxLength 64, // maxLength
false, // show characters false, // show characters
[this](const std::string& password) { [this](const std::string& password) {
@ -117,7 +114,7 @@ void KOReaderSettingsActivity::handleSelection() {
const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl; const std::string prefillUrl = currentUrl.empty() ? "https://" : currentUrl;
exitActivity(); exitActivity();
enterNewActivity(new KeyboardEntryActivity( enterNewActivity(new KeyboardEntryActivity(
renderer, mappedInput, "Sync Server URL", prefillUrl, 10, renderer, mappedInput, TR(SYNC_SERVER_URL), prefillUrl, 10,
128, // maxLength - URLs can be long 128, // maxLength - URLs can be long
false, // not password false, // not password
[this](const std::string& url) { [this](const std::string& url) {
@ -144,7 +141,6 @@ void KOReaderSettingsActivity::handleSelection() {
// Authenticate // Authenticate
if (!KOREADER_STORE.hasCredentials()) { if (!KOREADER_STORE.hasCredentials()) {
// Can't authenticate without credentials - just show message briefly // Can't authenticate without credentials - just show message briefly
xSemaphoreGive(renderingMutex);
return; return;
} }
exitActivity(); exitActivity();
@ -153,11 +149,14 @@ void KOReaderSettingsActivity::handleSelection() {
updateRequired = true; updateRequired = true;
})); }));
} }
xSemaphoreGive(renderingMutex);
} }
void KOReaderSettingsActivity::displayTaskLoop() { void KOReaderSettingsActivity::displayTaskLoop() {
// Wait for parent activity's rendering to complete (screen refresh takes ~422ms)
// Wait 500ms to be safe and avoid race conditions with parent activity
vTaskDelay(500 / portTICK_PERIOD_MS);
updateRequired = true;
while (true) { while (true) {
if (updateRequired && !subActivity) { if (updateRequired && !subActivity) {
updateRequired = false; updateRequired = false;
@ -175,13 +174,14 @@ void KOReaderSettingsActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "KOReader Sync", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(KOREADER_SYNC), true, EpdFontFamily::BOLD);
// Draw selection highlight // Draw selection highlight
renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, 60 + selectedIndex * 30 - 2, pageWidth - 1, 30);
// Draw menu items // Draw menu items
for (int i = 0; i < MENU_ITEMS; i++) { const char* menuNames[5] = {TR(USERNAME), TR(PASSWORD), TR(SYNC_SERVER_URL), TR(DOCUMENT_MATCHING), TR(AUTHENTICATE)};
for (int i = 0; i < 5; i++) {
const int settingY = 60 + i * 30; const int settingY = 60 + i * 30;
const bool isSelected = (i == selectedIndex); const bool isSelected = (i == selectedIndex);
@ -190,15 +190,15 @@ void KOReaderSettingsActivity::render() {
// Draw status for each item // Draw status for each item
const char* status = ""; const char* status = "";
if (i == 0) { if (i == 0) {
status = KOREADER_STORE.getUsername().empty() ? "[Not Set]" : "[Set]"; status = KOREADER_STORE.getUsername().empty() ? TR(NOT_SET) : TR(SET);
} else if (i == 1) { } else if (i == 1) {
status = KOREADER_STORE.getPassword().empty() ? "[Not Set]" : "[Set]"; status = KOREADER_STORE.getPassword().empty() ? TR(NOT_SET) : TR(SET);
} else if (i == 2) { } else if (i == 2) {
status = KOREADER_STORE.getServerUrl().empty() ? "[Not Set]" : "[Set]"; status = KOREADER_STORE.getServerUrl().empty() ? TR(NOT_SET) : TR(SET);
} else if (i == 3) { } else if (i == 3) {
status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? "[Filename]" : "[Binary]"; status = KOREADER_STORE.getMatchMethod() == DocumentMatchMethod::FILENAME ? TR(FILENAME) : TR(BINARY);
} else if (i == 4) { } else if (i == 4) {
status = KOREADER_STORE.hasCredentials() ? "" : "[Set credentials first]"; status = KOREADER_STORE.hasCredentials() ? "" : TR(SET_CREDENTIALS_FIRST);
} }
const auto width = renderer.getTextWidth(UI_10_FONT_ID, status); const auto width = renderer.getTextWidth(UI_10_FONT_ID, status);
@ -206,7 +206,7 @@ void KOReaderSettingsActivity::render() {
} }
// Draw button hints // Draw button hints
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), TR(SELECT), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();

View File

@ -0,0 +1,145 @@
#include "LanguageSelectActivity.h"
#include <GfxRenderer.h>
#include <I18n.h>
#include "MappedInputManager.h"
#include "fontIds.h"
void LanguageSelectActivity::taskTrampoline(void *param) {
auto *self = static_cast<LanguageSelectActivity *>(param);
self->displayTaskLoop();
}
void LanguageSelectActivity::onEnter() {
ActivityWithSubactivity::onEnter();
renderingMutex = xSemaphoreCreateMutex();
// Set current selection based on current language
selectedIndex = static_cast<int>(I18N.getLanguage());
updateRequired = false; // Don't trigger render immediately to avoid race with parent activity
xTaskCreate(&LanguageSelectActivity::taskTrampoline, "LanguageSelectTask",
4096, this, 1, &displayTaskHandle);
}
void LanguageSelectActivity::onExit() {
ActivityWithSubactivity::onExit();
xSemaphoreTake(renderingMutex, portMAX_DELAY);
if (displayTaskHandle) {
vTaskDelete(displayTaskHandle);
displayTaskHandle = nullptr;
}
vSemaphoreDelete(renderingMutex);
renderingMutex = nullptr;
}
void LanguageSelectActivity::loop() {
if (subActivity) {
subActivity->loop();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Back)) {
onBack();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Confirm)) {
handleSelection();
return;
}
if (mappedInput.wasPressed(MappedInputManager::Button::Up) ||
mappedInput.wasPressed(MappedInputManager::Button::Left)) {
selectedIndex = (selectedIndex + totalItems - 1) % totalItems;
updateRequired = true;
} else if (mappedInput.wasPressed(MappedInputManager::Button::Down) ||
mappedInput.wasPressed(MappedInputManager::Button::Right)) {
selectedIndex = (selectedIndex + 1) % totalItems;
updateRequired = true;
}
}
void LanguageSelectActivity::handleSelection() {
xSemaphoreTake(renderingMutex, portMAX_DELAY);
// Set the selected language (setLanguage internally calls saveSettings)
I18N.setLanguage(static_cast<Language>(selectedIndex));
xSemaphoreGive(renderingMutex);
// Return to previous page
onBack();
}
void LanguageSelectActivity::displayTaskLoop() {
// Wait for parent activity's rendering to complete (screen refresh takes ~422ms)
// Wait 500ms to be safe and avoid race conditions with parent activity
vTaskDelay(500 / portTICK_PERIOD_MS);
updateRequired = true;
while (true) {
if (updateRequired && !subActivity) {
updateRequired = false;
xSemaphoreTake(renderingMutex, portMAX_DELAY);
render();
xSemaphoreGive(renderingMutex);
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
void LanguageSelectActivity::render() {
renderer.clearScreen();
const auto pageWidth = renderer.getScreenWidth();
constexpr int rowHeight = 30;
// Title
renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(LANGUAGE), true,
EpdFontFamily::BOLD);
// Current language marker
const int currentLang = static_cast<int>(I18N.getLanguage());
// Language names in their native language
const char *langNames[] = {
"English",
"\xE7\xAE\x80\xE4\xBD\x93\xE4\xB8\xAD\xE6\x96\x87", // 简体中文
"\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" // 日本語
};
// Draw options
for (int i = 0; i < totalItems && i < 10; i++) { // Max 10 items
const int itemY = 60 + i * rowHeight;
const bool isSelected = (i == selectedIndex);
const bool isCurrent = (i == currentLang);
// Draw selection highlight
if (isSelected) {
renderer.fillRect(0, itemY - 2, pageWidth - 1, rowHeight);
}
// Draw language name
renderer.drawText(UI_10_FONT_ID, 20, itemY, langNames[i], !isSelected);
// Draw current selection marker
if (isCurrent) {
const char *marker = "[ON]";
const auto width = renderer.getTextWidth(UI_10_FONT_ID, marker);
renderer.drawText(UI_10_FONT_ID, pageWidth - 20 - width, itemY, marker,
!isSelected);
}
}
// Button hints
const auto labels = mappedInput.mapLabels(TR(BACK), TR(SELECT), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3,
labels.btn4);
renderer.displayBuffer();
}

View File

@ -0,0 +1,39 @@
#pragma once
#include <GfxRenderer.h>
#include <I18n.h>
#include <functional>
#include "../ActivityWithSubactivity.h"
class MappedInputManager;
/**
* Activity for selecting UI language
*/
class LanguageSelectActivity final : public ActivityWithSubactivity {
public:
explicit LanguageSelectActivity(GfxRenderer &renderer,
MappedInputManager &mappedInput,
const std::function<void()> &onBack)
: ActivityWithSubactivity("LanguageSelect", renderer, mappedInput), onBack(onBack) {}
void onEnter() override;
void onExit() override;
void loop() override;
private:
static void taskTrampoline(void *param);
void displayTaskLoop();
void render();
void handleSelection();
std::function<void()> onBack;
int selectedIndex = 0;
static constexpr int totalItems = 3; // English, 简体中文, 日本語
TaskHandle_t displayTaskHandle = nullptr;
SemaphoreHandle_t renderingMutex = nullptr;
volatile bool updateRequired = false;
};

View File

@ -1,6 +1,7 @@
#include "OtaUpdateActivity.h" #include "OtaUpdateActivity.h"
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <I18n.h>
#include <WiFi.h> #include <WiFi.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
@ -127,27 +128,27 @@ void OtaUpdateActivity::render() {
const auto pageWidth = renderer.getScreenWidth(); const auto pageWidth = renderer.getScreenWidth();
renderer.clearScreen(); renderer.clearScreen();
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Update", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(UPDATE), true, EpdFontFamily::BOLD);
if (state == CHECKING_FOR_UPDATE) { if (state == CHECKING_FOR_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Checking for update...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, TR(CHECKING_UPDATE), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == WAITING_CONFIRMATION) { if (state == WAITING_CONFIRMATION) {
renderer.drawCenteredText(UI_10_FONT_ID, 200, "New update available!", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 200, TR(NEW_UPDATE), true, EpdFontFamily::BOLD);
renderer.drawText(UI_10_FONT_ID, 20, 250, "Current Version: " CROSSPOINT_VERSION); renderer.drawText(UI_10_FONT_ID, 20, 250, (std::string(TR(CURRENT_VERSION)) + CROSSPOINT_VERSION).c_str());
renderer.drawText(UI_10_FONT_ID, 20, 270, ("New Version: " + updater.getLatestVersion()).c_str()); renderer.drawText(UI_10_FONT_ID, 20, 270, (std::string(TR(NEW_VERSION)) + updater.getLatestVersion()).c_str());
const auto labels = mappedInput.mapLabels("Cancel", "Update", "", ""); const auto labels = mappedInput.mapLabels(TR(CANCEL), TR(UPDATE), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == UPDATE_IN_PROGRESS) { if (state == UPDATE_IN_PROGRESS) {
renderer.drawCenteredText(UI_10_FONT_ID, 310, "Updating...", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 310, TR(UPDATING), true, EpdFontFamily::BOLD);
renderer.drawRect(20, 350, pageWidth - 40, 50); renderer.drawRect(20, 350, pageWidth - 40, 50);
renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42); renderer.fillRect(24, 354, static_cast<int>(updaterProgress * static_cast<float>(pageWidth - 44)), 42);
renderer.drawCenteredText(UI_10_FONT_ID, 420, renderer.drawCenteredText(UI_10_FONT_ID, 420,
@ -160,20 +161,20 @@ void OtaUpdateActivity::render() {
} }
if (state == NO_UPDATE) { if (state == NO_UPDATE) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "No update available", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, TR(NO_UPDATE), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FAILED) { if (state == FAILED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update failed", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, TR(UPDATE_FAILED), true, EpdFontFamily::BOLD);
renderer.displayBuffer(); renderer.displayBuffer();
return; return;
} }
if (state == FINISHED) { if (state == FINISHED) {
renderer.drawCenteredText(UI_10_FONT_ID, 300, "Update complete", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_10_FONT_ID, 300, TR(UPDATE_COMPLETE), true, EpdFontFamily::BOLD);
renderer.drawCenteredText(UI_10_FONT_ID, 350, "Press and hold power button to turn back on"); renderer.drawCenteredText(UI_10_FONT_ID, 350, TR(POWER_ON_HINT));
renderer.displayBuffer(); renderer.displayBuffer();
state = SHUTTING_DOWN; state = SHUTTING_DOWN;
return; return;

View File

@ -2,54 +2,55 @@
#include <GfxRenderer.h> #include <GfxRenderer.h>
#include <HardwareSerial.h> #include <HardwareSerial.h>
#include <I18n.h>
#include "CategorySettingsActivity.h" #include "CategorySettingsActivity.h"
#include "CrossPointSettings.h" #include "CrossPointSettings.h"
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "fontIds.h" #include "fontIds.h"
const char* SettingsActivity::categoryNames[categoryCount] = {"Display", "Reader", "Controls", "System"};
namespace { namespace {
constexpr int displaySettingsCount = 5; constexpr int displaySettingsCount = 6;
const SettingInfo displaySettings[displaySettingsCount] = { const SettingInfo displaySettings[displaySettingsCount] = {
// Should match with SLEEP_SCREEN_MODE // Should match with SLEEP_SCREEN_MODE
SettingInfo::Enum("Sleep Screen", &CrossPointSettings::sleepScreen, {"Dark", "Light", "Custom", "Cover", "None"}), SettingInfo::Enum(StrId::SLEEP_SCREEN, &CrossPointSettings::sleepScreen, {StrId::DARK, StrId::LIGHT, StrId::CUSTOM, StrId::COVER, StrId::NONE}),
SettingInfo::Enum("Sleep Screen Cover Mode", &CrossPointSettings::sleepScreenCoverMode, {"Fit", "Crop"}), SettingInfo::Enum(StrId::SLEEP_COVER_MODE, &CrossPointSettings::sleepScreenCoverMode, {StrId::FIT, StrId::CROP}),
SettingInfo::Enum("Status Bar", &CrossPointSettings::statusBar, {"None", "No Progress", "Full"}), SettingInfo::Enum(StrId::STATUS_BAR, &CrossPointSettings::statusBar, {StrId::NONE, StrId::NO_PROGRESS, StrId::FULL}),
SettingInfo::Enum("Hide Battery %", &CrossPointSettings::hideBatteryPercentage, {"Never", "In Reader", "Always"}), SettingInfo::Enum(StrId::HIDE_BATTERY, &CrossPointSettings::hideBatteryPercentage, {StrId::NEVER, StrId::IN_READER, StrId::ALWAYS}),
SettingInfo::Enum("Refresh Frequency", &CrossPointSettings::refreshFrequency, SettingInfo::Enum(StrId::REFRESH_FREQ, &CrossPointSettings::refreshFrequency,
{"1 page", "5 pages", "10 pages", "15 pages", "30 pages"})}; {StrId::PAGES_1, StrId::PAGES_5, StrId::PAGES_10, StrId::PAGES_15, StrId::PAGES_30}),
SettingInfo::Action(StrId::EXT_UI_FONT)};
constexpr int readerSettingsCount = 9; constexpr int readerSettingsCount = 9;
const SettingInfo readerSettings[readerSettingsCount] = { const SettingInfo readerSettings[readerSettingsCount] = {
SettingInfo::Enum("Font Family", &CrossPointSettings::fontFamily, {"Bookerly", "Noto Sans", "Open Dyslexic"}), SettingInfo::Action(StrId::EXT_READER_FONT),
SettingInfo::Enum("Font Size", &CrossPointSettings::fontSize, {"Small", "Medium", "Large", "X Large"}), SettingInfo::Enum(StrId::FONT_SIZE, &CrossPointSettings::fontSize, {StrId::SMALL, StrId::MEDIUM, StrId::LARGE, StrId::X_LARGE}),
SettingInfo::Enum("Line Spacing", &CrossPointSettings::lineSpacing, {"Tight", "Normal", "Wide"}), SettingInfo::Enum(StrId::LINE_SPACING, &CrossPointSettings::lineSpacing, {StrId::TIGHT, StrId::NORMAL, StrId::WIDE}),
SettingInfo::Value("Screen Margin", &CrossPointSettings::screenMargin, {5, 40, 5}), SettingInfo::Value(StrId::SCREEN_MARGIN, &CrossPointSettings::screenMargin, {5, 40, 5}),
SettingInfo::Enum("Paragraph Alignment", &CrossPointSettings::paragraphAlignment, SettingInfo::Enum(StrId::PARA_ALIGNMENT, &CrossPointSettings::paragraphAlignment,
{"Justify", "Left", "Center", "Right"}), {StrId::JUSTIFY, StrId::LEFT, StrId::CENTER, StrId::RIGHT}),
SettingInfo::Toggle("Hyphenation", &CrossPointSettings::hyphenationEnabled), SettingInfo::Toggle(StrId::HYPHENATION, &CrossPointSettings::hyphenationEnabled),
SettingInfo::Enum("Reading Orientation", &CrossPointSettings::orientation, SettingInfo::Enum(StrId::ORIENTATION, &CrossPointSettings::orientation,
{"Portrait", "Landscape CW", "Inverted", "Landscape CCW"}), {StrId::PORTRAIT, StrId::LANDSCAPE_CW, StrId::INVERTED, StrId::LANDSCAPE_CCW}),
SettingInfo::Toggle("Extra Paragraph Spacing", &CrossPointSettings::extraParagraphSpacing), SettingInfo::Toggle(StrId::EXTRA_SPACING, &CrossPointSettings::extraParagraphSpacing),
SettingInfo::Toggle("Text Anti-Aliasing", &CrossPointSettings::textAntiAliasing)}; SettingInfo::Toggle(StrId::TEXT_AA, &CrossPointSettings::textAntiAliasing)};
constexpr int controlsSettingsCount = 4; constexpr int controlsSettingsCount = 4;
const SettingInfo controlsSettings[controlsSettingsCount] = { const SettingInfo controlsSettings[controlsSettingsCount] = {
SettingInfo::Enum("Front Button Layout", &CrossPointSettings::frontButtonLayout, SettingInfo::Enum(StrId::FRONT_BTN_LAYOUT, &CrossPointSettings::frontButtonLayout,
{"Bck, Cnfrm, Lft, Rght", "Lft, Rght, Bck, Cnfrm", "Lft, Bck, Cnfrm, Rght"}), {StrId::FRONT_LAYOUT_BCLR, StrId::FRONT_LAYOUT_LRBC, StrId::FRONT_LAYOUT_LBCR}),
SettingInfo::Enum("Side Button Layout (reader)", &CrossPointSettings::sideButtonLayout, SettingInfo::Enum(StrId::SIDE_BTN_LAYOUT, &CrossPointSettings::sideButtonLayout,
{"Prev, Next", "Next, Prev"}), {StrId::PREV_NEXT, StrId::NEXT_PREV}),
SettingInfo::Toggle("Long-press Chapter Skip", &CrossPointSettings::longPressChapterSkip), SettingInfo::Toggle(StrId::LONG_PRESS_SKIP, &CrossPointSettings::longPressChapterSkip),
SettingInfo::Enum("Short Power Button Click", &CrossPointSettings::shortPwrBtn, {"Ignore", "Sleep", "Page Turn"})}; SettingInfo::Enum(StrId::SHORT_PWR_BTN, &CrossPointSettings::shortPwrBtn, {StrId::IGNORE, StrId::SLEEP, StrId::PAGE_TURN})};
constexpr int systemSettingsCount = 5; constexpr int systemSettingsCount = 6;
const SettingInfo systemSettings[systemSettingsCount] = { const SettingInfo systemSettings[systemSettingsCount] = {
SettingInfo::Enum("Time to Sleep", &CrossPointSettings::sleepTimeout, SettingInfo::Enum(StrId::TIME_TO_SLEEP, &CrossPointSettings::sleepTimeout,
{"1 min", "5 min", "10 min", "15 min", "30 min"}), {StrId::MIN_1, StrId::MIN_5, StrId::MIN_10, StrId::MIN_15, StrId::MIN_30}),
SettingInfo::Action("KOReader Sync"), SettingInfo::Action("Calibre Settings"), SettingInfo::Action("Clear Cache"), SettingInfo::Action(StrId::LANGUAGE),
SettingInfo::Action("Check for updates")}; SettingInfo::Action(StrId::KOREADER_SYNC), SettingInfo::Action(StrId::CALIBRE_SETTINGS), SettingInfo::Action(StrId::CLEAR_READING_CACHE),
SettingInfo::Action(StrId::CHECK_UPDATES)};
} // namespace } // namespace
void SettingsActivity::taskTrampoline(void* param) { void SettingsActivity::taskTrampoline(void* param) {
@ -131,6 +132,11 @@ void SettingsActivity::enterCategory(int categoryIndex) {
const SettingInfo* settingsList = nullptr; const SettingInfo* settingsList = nullptr;
int settingsCount = 0; int settingsCount = 0;
// Category StrIds for dynamic translation
static constexpr StrId categoryStrIds[categoryCount] = {
StrId::CAT_DISPLAY, StrId::CAT_READER, StrId::CAT_CONTROLS, StrId::CAT_SYSTEM
};
switch (categoryIndex) { switch (categoryIndex) {
case 0: // Display case 0: // Display
settingsList = displaySettings; settingsList = displaySettings;
@ -150,7 +156,7 @@ void SettingsActivity::enterCategory(int categoryIndex) {
break; break;
} }
enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, categoryNames[categoryIndex], settingsList, enterNewActivity(new CategorySettingsActivity(renderer, mappedInput, I18N.get(categoryStrIds[categoryIndex]), settingsList,
settingsCount, [this] { settingsCount, [this] {
exitActivity(); exitActivity();
updateRequired = true; updateRequired = true;
@ -177,17 +183,22 @@ void SettingsActivity::render() const {
const auto pageHeight = renderer.getScreenHeight(); const auto pageHeight = renderer.getScreenHeight();
// Draw header // Draw header
renderer.drawCenteredText(UI_12_FONT_ID, 15, "Settings", true, EpdFontFamily::BOLD); renderer.drawCenteredText(UI_12_FONT_ID, 15, TR(SETTINGS_TITLE), true, EpdFontFamily::BOLD);
// Draw selection // Draw selection
renderer.fillRect(0, 60 + selectedCategoryIndex * 30 - 2, pageWidth - 1, 30); renderer.fillRect(0, 60 + selectedCategoryIndex * 30 - 2, pageWidth - 1, 30);
// Category StrIds for dynamic translation
static constexpr StrId categoryStrIds[categoryCount] = {
StrId::CAT_DISPLAY, StrId::CAT_READER, StrId::CAT_CONTROLS, StrId::CAT_SYSTEM
};
// Draw all categories // Draw all categories
for (int i = 0; i < categoryCount; i++) { for (int i = 0; i < categoryCount; i++) {
const int categoryY = 60 + i * 30; // 30 pixels between categories const int categoryY = 60 + i * 30; // 30 pixels between categories
// Draw category name // Draw category name (dynamically translated)
renderer.drawText(UI_10_FONT_ID, 20, categoryY, categoryNames[i], i != selectedCategoryIndex); renderer.drawText(UI_10_FONT_ID, 20, categoryY, I18N.get(categoryStrIds[i]), i != selectedCategoryIndex);
} }
// Draw version text above button hints // Draw version text above button hints
@ -195,7 +206,7 @@ void SettingsActivity::render() const {
pageHeight - 60, CROSSPOINT_VERSION); pageHeight - 60, CROSSPOINT_VERSION);
// Draw help text // Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Select", "", ""); const auto labels = mappedInput.mapLabels(TR(BACK), TR(SELECT), "", "");
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Always use standard refresh for settings screen // Always use standard refresh for settings screen

View File

@ -20,7 +20,6 @@ class SettingsActivity final : public ActivityWithSubactivity {
const std::function<void()> onGoHome; const std::function<void()> onGoHome;
static constexpr int categoryCount = 4; static constexpr int categoryCount = 4;
static const char* categoryNames[categoryCount];
static void taskTrampoline(void* param); static void taskTrampoline(void* param);
[[noreturn]] void displayTaskLoop(); [[noreturn]] void displayTaskLoop();

View File

@ -1,5 +1,7 @@
#include "KeyboardEntryActivity.h" #include "KeyboardEntryActivity.h"
#include <I18n.h>
#include "MappedInputManager.h" #include "MappedInputManager.h"
#include "fontIds.h" #include "fontIds.h"
@ -342,11 +344,11 @@ void KeyboardEntryActivity::render() const {
} }
// Draw help text // Draw help text
const auto labels = mappedInput.mapLabels("« Back", "Select", "Left", "Right"); const auto labels = mappedInput.mapLabels(TR(BACK), TR(SELECT), TR(DIR_LEFT), TR(DIR_RIGHT));
renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4); renderer.drawButtonHints(UI_10_FONT_ID, labels.btn1, labels.btn2, labels.btn3, labels.btn4);
// Draw side button hints for Up/Down navigation // Draw side button hints for Up/Down navigation
renderer.drawSideButtonHints(UI_10_FONT_ID, "Up", "Down"); renderer.drawSideButtonHints(UI_10_FONT_ID, TR(DIR_UP), TR(DIR_DOWN));
renderer.displayBuffer(); renderer.displayBuffer();
} }

View File

@ -5,6 +5,8 @@
#include <InputManager.h> #include <InputManager.h>
#include <SDCardManager.h> #include <SDCardManager.h>
#include <SPI.h> #include <SPI.h>
#include <FontManager.h>
#include <I18n.h>
#include <builtinFonts/all.h> #include <builtinFonts/all.h>
#include <cstring> #include <cstring>
@ -322,6 +324,13 @@ void setup() {
SETTINGS.loadFromFile(); SETTINGS.loadFromFile();
KOREADER_STORE.loadFromFile(); KOREADER_STORE.loadFromFile();
// Initialize FontManager - scan fonts and load user's font selection
FontMgr.scanFonts();
FontMgr.loadSettings();
// Initialize I18n - load language settings
I18N.loadSettings();
if (!isWakeupAfterFlashing()) { if (!isWakeupAfterFlashing()) {
// For normal wakeups (not immediately after flashing), verify long press // For normal wakeups (not immediately after flashing), verify long press
verifyWakeupLongPress(); verifyWakeupLongPress();