Public release
This commit is contained in:
+331
@@ -0,0 +1,331 @@
|
|||||||
|
Language: Cpp
|
||||||
|
AccessModifierOffset: -1
|
||||||
|
AlignAfterOpenBracket: Align
|
||||||
|
AlignArrayOfStructures: None
|
||||||
|
AlignConsecutiveAssignments:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionDeclarations: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: true
|
||||||
|
AlignConsecutiveBitFields:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionDeclarations: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveDeclarations:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionDeclarations: true
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveMacros:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionDeclarations: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveShortCaseStatements:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCaseArrows: false
|
||||||
|
AlignCaseColons: false
|
||||||
|
AlignConsecutiveTableGenBreakingDAGArgColons:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionDeclarations: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveTableGenCondOperatorColons:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionDeclarations: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignConsecutiveTableGenDefinitionColons:
|
||||||
|
Enabled: false
|
||||||
|
AcrossEmptyLines: false
|
||||||
|
AcrossComments: false
|
||||||
|
AlignCompound: false
|
||||||
|
AlignFunctionDeclarations: false
|
||||||
|
AlignFunctionPointers: false
|
||||||
|
PadOperators: false
|
||||||
|
AlignEscapedNewlines: Left
|
||||||
|
AlignOperands: Align
|
||||||
|
AlignTrailingComments:
|
||||||
|
Kind: Always
|
||||||
|
OverEmptyLines: 0
|
||||||
|
AllowAllArgumentsOnNextLine: true
|
||||||
|
AllowAllParametersOfDeclarationOnNextLine: true
|
||||||
|
AllowBreakBeforeNoexceptSpecifier: Never
|
||||||
|
AllowShortBlocksOnASingleLine: Never
|
||||||
|
AllowShortCaseExpressionOnASingleLine: true
|
||||||
|
AllowShortCaseLabelsOnASingleLine: false
|
||||||
|
AllowShortCompoundRequirementOnASingleLine: true
|
||||||
|
AllowShortEnumsOnASingleLine: true
|
||||||
|
AllowShortFunctionsOnASingleLine: All
|
||||||
|
AllowShortIfStatementsOnASingleLine: WithoutElse
|
||||||
|
AllowShortLambdasOnASingleLine: All
|
||||||
|
AllowShortLoopsOnASingleLine: true
|
||||||
|
AllowShortNamespacesOnASingleLine: false
|
||||||
|
AlwaysBreakAfterDefinitionReturnType: None
|
||||||
|
AlwaysBreakBeforeMultilineStrings: true
|
||||||
|
AttributeMacros:
|
||||||
|
- __capability
|
||||||
|
- absl_nonnull
|
||||||
|
- absl_nullable
|
||||||
|
- absl_nullability_unknown
|
||||||
|
BinPackArguments: true
|
||||||
|
BinPackLongBracedList: true
|
||||||
|
BinPackParameters: BinPack
|
||||||
|
BitFieldColonSpacing: Both
|
||||||
|
BracedInitializerIndentWidth: -1
|
||||||
|
BraceWrapping:
|
||||||
|
AfterCaseLabel: false
|
||||||
|
AfterClass: false
|
||||||
|
AfterControlStatement: Never
|
||||||
|
AfterEnum: false
|
||||||
|
AfterExternBlock: false
|
||||||
|
AfterFunction: false
|
||||||
|
AfterNamespace: false
|
||||||
|
AfterObjCDeclaration: false
|
||||||
|
AfterStruct: false
|
||||||
|
AfterUnion: false
|
||||||
|
BeforeCatch: false
|
||||||
|
BeforeElse: false
|
||||||
|
BeforeLambdaBody: false
|
||||||
|
BeforeWhile: false
|
||||||
|
IndentBraces: false
|
||||||
|
SplitEmptyFunction: true
|
||||||
|
SplitEmptyRecord: true
|
||||||
|
SplitEmptyNamespace: true
|
||||||
|
BreakAdjacentStringLiterals: true
|
||||||
|
BreakAfterAttributes: Leave
|
||||||
|
BreakAfterJavaFieldAnnotations: false
|
||||||
|
BreakAfterReturnType: None
|
||||||
|
BreakArrays: true
|
||||||
|
BreakBeforeBinaryOperators: None
|
||||||
|
BreakBeforeConceptDeclarations: Always
|
||||||
|
BreakBeforeBraces: Attach
|
||||||
|
BreakBeforeInlineASMColon: OnlyMultiline
|
||||||
|
BreakBeforeTemplateCloser: false
|
||||||
|
BreakBeforeTernaryOperators: true
|
||||||
|
BreakBinaryOperations: Never
|
||||||
|
BreakConstructorInitializers: BeforeColon
|
||||||
|
BreakFunctionDefinitionParameters: false
|
||||||
|
BreakInheritanceList: BeforeColon
|
||||||
|
BreakStringLiterals: true
|
||||||
|
BreakTemplateDeclarations: Yes
|
||||||
|
ColumnLimit: 120
|
||||||
|
CommentPragmas: '^ IWYU pragma:'
|
||||||
|
CompactNamespaces: false
|
||||||
|
ConstructorInitializerIndentWidth: 4
|
||||||
|
ContinuationIndentWidth: 4
|
||||||
|
Cpp11BracedListStyle: true
|
||||||
|
DerivePointerAlignment: false
|
||||||
|
DisableFormat: false
|
||||||
|
EmptyLineAfterAccessModifier: Never
|
||||||
|
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||||
|
EnumTrailingComma: Leave
|
||||||
|
ExperimentalAutoDetectBinPacking: false
|
||||||
|
FixNamespaceComments: true
|
||||||
|
ForEachMacros:
|
||||||
|
- foreach
|
||||||
|
- Q_FOREACH
|
||||||
|
- BOOST_FOREACH
|
||||||
|
IfMacros:
|
||||||
|
- KJ_IF_MAYBE
|
||||||
|
IncludeBlocks: Regroup
|
||||||
|
IncludeCategories:
|
||||||
|
- Regex: '^<ext/.*\.h>'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
- Regex: '^<.*\.h>'
|
||||||
|
Priority: 1
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
- Regex: '^<.*'
|
||||||
|
Priority: 2
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
- Regex: '.*'
|
||||||
|
Priority: 3
|
||||||
|
SortPriority: 0
|
||||||
|
CaseSensitive: false
|
||||||
|
IncludeIsMainRegex: '([-_](test|unittest))?$'
|
||||||
|
IncludeIsMainSourceRegex: ''
|
||||||
|
IndentAccessModifiers: false
|
||||||
|
IndentCaseBlocks: false
|
||||||
|
IndentCaseLabels: true
|
||||||
|
IndentExportBlock: true
|
||||||
|
IndentExternBlock: AfterExternBlock
|
||||||
|
IndentGotoLabels: true
|
||||||
|
IndentPPDirectives: None
|
||||||
|
IndentRequiresClause: true
|
||||||
|
IndentWidth: 2
|
||||||
|
IndentWrappedFunctionNames: false
|
||||||
|
InsertBraces: false
|
||||||
|
InsertNewlineAtEOF: false
|
||||||
|
InsertTrailingCommas: None
|
||||||
|
IntegerLiteralSeparator:
|
||||||
|
Binary: 0
|
||||||
|
BinaryMinDigits: 0
|
||||||
|
Decimal: 0
|
||||||
|
DecimalMinDigits: 0
|
||||||
|
Hex: 0
|
||||||
|
HexMinDigits: 0
|
||||||
|
JavaScriptQuotes: Leave
|
||||||
|
JavaScriptWrapImports: true
|
||||||
|
KeepEmptyLines:
|
||||||
|
AtEndOfFile: false
|
||||||
|
AtStartOfBlock: false
|
||||||
|
AtStartOfFile: true
|
||||||
|
KeepFormFeed: false
|
||||||
|
LambdaBodyIndentation: Signature
|
||||||
|
LineEnding: DeriveLF
|
||||||
|
MacroBlockBegin: ''
|
||||||
|
MacroBlockEnd: ''
|
||||||
|
MainIncludeChar: Quote
|
||||||
|
MaxEmptyLinesToKeep: 1
|
||||||
|
NamespaceIndentation: None
|
||||||
|
ObjCBinPackProtocolList: Never
|
||||||
|
ObjCBlockIndentWidth: 2
|
||||||
|
ObjCBreakBeforeNestedBlockParam: true
|
||||||
|
ObjCSpaceAfterProperty: false
|
||||||
|
ObjCSpaceBeforeProtocolList: true
|
||||||
|
OneLineFormatOffRegex: ''
|
||||||
|
PackConstructorInitializers: NextLine
|
||||||
|
PenaltyBreakAssignment: 2
|
||||||
|
PenaltyBreakBeforeFirstCallParameter: 1
|
||||||
|
PenaltyBreakBeforeMemberAccess: 150
|
||||||
|
PenaltyBreakComment: 300
|
||||||
|
PenaltyBreakFirstLessLess: 120
|
||||||
|
PenaltyBreakOpenParenthesis: 0
|
||||||
|
PenaltyBreakScopeResolution: 500
|
||||||
|
PenaltyBreakString: 1000
|
||||||
|
PenaltyBreakTemplateDeclaration: 10
|
||||||
|
PenaltyExcessCharacter: 1000000
|
||||||
|
PenaltyIndentedWhitespace: 0
|
||||||
|
PenaltyReturnTypeOnItsOwnLine: 200
|
||||||
|
PointerAlignment: Left
|
||||||
|
PPIndentWidth: -1
|
||||||
|
QualifierAlignment: Leave
|
||||||
|
RawStringFormats:
|
||||||
|
- Language: Cpp
|
||||||
|
Delimiters:
|
||||||
|
- cc
|
||||||
|
- CC
|
||||||
|
- cpp
|
||||||
|
- Cpp
|
||||||
|
- CPP
|
||||||
|
- 'c++'
|
||||||
|
- 'C++'
|
||||||
|
CanonicalDelimiter: ''
|
||||||
|
BasedOnStyle: google
|
||||||
|
- Language: TextProto
|
||||||
|
Delimiters:
|
||||||
|
- pb
|
||||||
|
- PB
|
||||||
|
- proto
|
||||||
|
- PROTO
|
||||||
|
EnclosingFunctions:
|
||||||
|
- EqualsProto
|
||||||
|
- EquivToProto
|
||||||
|
- PARSE_PARTIAL_TEXT_PROTO
|
||||||
|
- PARSE_TEST_PROTO
|
||||||
|
- PARSE_TEXT_PROTO
|
||||||
|
- ParseTextOrDie
|
||||||
|
- ParseTextProtoOrDie
|
||||||
|
- ParseTestProto
|
||||||
|
- ParsePartialTestProto
|
||||||
|
CanonicalDelimiter: pb
|
||||||
|
BasedOnStyle: google
|
||||||
|
ReferenceAlignment: Pointer
|
||||||
|
ReflowComments: Always
|
||||||
|
RemoveBracesLLVM: false
|
||||||
|
RemoveEmptyLinesInUnwrappedLines: false
|
||||||
|
RemoveParentheses: Leave
|
||||||
|
RemoveSemicolon: false
|
||||||
|
RequiresClausePosition: OwnLine
|
||||||
|
RequiresExpressionIndentation: OuterScope
|
||||||
|
SeparateDefinitionBlocks: Leave
|
||||||
|
ShortNamespaceLines: 1
|
||||||
|
SkipMacroDefinitionBody: false
|
||||||
|
SortIncludes:
|
||||||
|
Enabled: true
|
||||||
|
IgnoreCase: false
|
||||||
|
SortJavaStaticImport: Before
|
||||||
|
SortUsingDeclarations: LexicographicNumeric
|
||||||
|
SpaceAfterCStyleCast: false
|
||||||
|
SpaceAfterLogicalNot: false
|
||||||
|
SpaceAfterOperatorKeyword: false
|
||||||
|
SpaceAfterTemplateKeyword: true
|
||||||
|
SpaceAroundPointerQualifiers: Default
|
||||||
|
SpaceBeforeAssignmentOperators: true
|
||||||
|
SpaceBeforeCaseColon: false
|
||||||
|
SpaceBeforeCpp11BracedList: false
|
||||||
|
SpaceBeforeCtorInitializerColon: true
|
||||||
|
SpaceBeforeInheritanceColon: true
|
||||||
|
SpaceBeforeJsonColon: false
|
||||||
|
SpaceBeforeParens: ControlStatements
|
||||||
|
SpaceBeforeParensOptions:
|
||||||
|
AfterControlStatements: true
|
||||||
|
AfterForeachMacros: true
|
||||||
|
AfterFunctionDefinitionName: false
|
||||||
|
AfterFunctionDeclarationName: false
|
||||||
|
AfterIfMacros: true
|
||||||
|
AfterNot: false
|
||||||
|
AfterOverloadedOperator: false
|
||||||
|
AfterPlacementOperator: true
|
||||||
|
AfterRequiresInClause: false
|
||||||
|
AfterRequiresInExpression: false
|
||||||
|
BeforeNonEmptyParentheses: false
|
||||||
|
SpaceBeforeRangeBasedForLoopColon: true
|
||||||
|
SpaceBeforeSquareBrackets: false
|
||||||
|
SpaceInEmptyBlock: false
|
||||||
|
SpacesBeforeTrailingComments: 2
|
||||||
|
SpacesInAngles: Never
|
||||||
|
SpacesInContainerLiterals: true
|
||||||
|
SpacesInLineCommentPrefix:
|
||||||
|
Minimum: 1
|
||||||
|
Maximum: -1
|
||||||
|
SpacesInParens: Never
|
||||||
|
SpacesInParensOptions:
|
||||||
|
ExceptDoubleParentheses: false
|
||||||
|
InCStyleCasts: false
|
||||||
|
InConditionalStatements: false
|
||||||
|
InEmptyParentheses: false
|
||||||
|
Other: false
|
||||||
|
SpacesInSquareBrackets: false
|
||||||
|
Standard: Auto
|
||||||
|
StatementAttributeLikeMacros:
|
||||||
|
- Q_EMIT
|
||||||
|
StatementMacros:
|
||||||
|
- Q_UNUSED
|
||||||
|
- QT_REQUIRE_VERSION
|
||||||
|
TableGenBreakInsideDAGArg: DontBreak
|
||||||
|
TabWidth: 8
|
||||||
|
UseTab: Never
|
||||||
|
VerilogBreakBetweenInstancePorts: true
|
||||||
|
WhitespaceSensitiveMacros:
|
||||||
|
- BOOST_PP_STRINGIZE
|
||||||
|
- CF_SWIFT_NAME
|
||||||
|
- NS_SWIFT_NAME
|
||||||
|
- PP_STRINGIZE
|
||||||
|
- STRINGIZE
|
||||||
|
WrapNamespaceBodyWithEmptyLines: Leave
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.pio
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "open-x4-sdk"]
|
||||||
|
path = open-x4-sdk
|
||||||
|
url = https://github.com/open-x4-epaper/community-sdk.git
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Dave Allie
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# CrossPoint Reader
|
||||||
|
|
||||||
|
Firmware for the **Xteink X4** e-paper display reader (unaffiliated with Xteink).
|
||||||
|
Built using **PlatformIO** and targeting the **ESP32-C3** microcontroller.
|
||||||
|
|
||||||
|
CrossPoint Reader is a purpose-built firmware designed to be a drop-in, fully open-source replacement for the official
|
||||||
|
Xteink firmware. It aims to match or improve upon the standard EPUB reading experience.
|
||||||
|
|
||||||
|
// TODO include some images
|
||||||
|
|
||||||
|
I look at the [**diy-esp32-epub-reader** by atomic14](https://github.com/atomic14/diy-esp32-epub-reader) project a lot
|
||||||
|
when making CrossPoint and a handful of lessons and some direct source code comes directly from that repo.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
E-paper devices are fantastic for reading, but most commercially available readers are closed systems with limited
|
||||||
|
customisation. The **Xteink X4** is an affordable, e-paper device, however the official firmware remains closed.
|
||||||
|
CrossPoint exists partly as a fun side-project and partly to open up the ecosystem and truely unlock the device's
|
||||||
|
potential.
|
||||||
|
|
||||||
|
CrossPoint Reader aims to:
|
||||||
|
* Provide a **fully open-source alternative** to the official firmware.
|
||||||
|
* Offer a **document reader** capable of handling EPUB content on constrained hardware.
|
||||||
|
* Support **customisable font, layout, and display** options.
|
||||||
|
* Run purely on the **Xteink X4 hardware**.
|
||||||
|
|
||||||
|
This project is **not affiliated with Xteink**; it's built as a community project.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [x] EPUB parsing and rendering
|
||||||
|
- [x] Saved reading position
|
||||||
|
- [ ] File explorer with file picker
|
||||||
|
- Currently CrossPoint will just open the first EPUB it finds at the root of the SD card
|
||||||
|
- [ ] Image support within EPUB
|
||||||
|
- [ ] Configurable font, layout, and display options
|
||||||
|
- [ ] WiFi connectivity
|
||||||
|
- [ ] BLE connectivity
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
* **PlatformIO Core** (`pio`) or **VS Code + PlatformIO IDE**
|
||||||
|
* Python 3.8+
|
||||||
|
* USB-C cable for flashing the ESP32-C3
|
||||||
|
* Xteink X4
|
||||||
|
|
||||||
|
### Flashing your device
|
||||||
|
|
||||||
|
#### Command line
|
||||||
|
|
||||||
|
```sh
|
||||||
|
pio run --target upload
|
||||||
|
```
|
||||||
|
|
||||||
|
## Internals
|
||||||
|
|
||||||
|
CrossPoint Reader is pretty aggressive about caching data down to the SD card to minimise RAM usage. The ESP32-C3 only
|
||||||
|
has ~380KB of usable RAM, so we have to be careful. A lot of the decisions made in the design of the firmware were based
|
||||||
|
on this constraint.
|
||||||
|
|
||||||
|
### EPUB caching
|
||||||
|
|
||||||
|
The first time chapters of an EPUB are loaded, they are cached to the SD card. Subsequent loads are served from the
|
||||||
|
cache. This cache directory exists at `.crosspoint` on the SD card. The structure is as follows:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
.crosspoint/
|
||||||
|
├── epub_12471232/ # Each EPUB is cached to a subdirectory named `epub_<hash>`
|
||||||
|
│ ├── progress.bin # Stores reading progress (chapter, page, etc.)
|
||||||
|
│ ├── 0/ # Each chapter is stored in a subdirectory named by its index (based on the spine order)
|
||||||
|
│ │ ├── section.bin # Section metadata (page count)
|
||||||
|
│ │ ├── page_0.bin # Each page is stored in a separate file, it
|
||||||
|
│ │ ├── page_1.bin # contains the position (x, y) and text for each word
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── 1/
|
||||||
|
│ │ ├── section.bin
|
||||||
|
│ │ ├── page_0.bin
|
||||||
|
│ │ ├── page_1.bin
|
||||||
|
│ │ └── ...
|
||||||
|
│ └── ...
|
||||||
|
│
|
||||||
|
└── epub_189013891/
|
||||||
|
```
|
||||||
|
|
||||||
|
Deleting the `.crosspoint` directory will clear the cache.
|
||||||
|
|
||||||
|
Due the way it's currently implemented, the cache is not automatically cleared when the EPUB is deleted and moving an
|
||||||
|
EPUB file will reset the reading progress.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Contributions are very welcome!
|
||||||
|
|
||||||
|
### To submit a contribution:
|
||||||
|
|
||||||
|
1. Fork the repo
|
||||||
|
2. Create a branch (`feature/dithering-improvement`)
|
||||||
|
3. Make changes
|
||||||
|
4. Submit a PR
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
CrossPoint Reader is **not affiliated with Xteink or any manufacturer of the X4 hardware**.
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
find src lib \( -name "*.c" -o -name "*.cpp" -o -name "*.h" -o -name "*.hpp" \) -exec clang-format -style=file -i {} +
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
|
||||||
|
This directory is intended for project header files.
|
||||||
|
|
||||||
|
A header file is a file containing C declarations and macro definitions
|
||||||
|
to be shared between several project source files. You request the use of a
|
||||||
|
header file in your project source file (C, C++, etc) located in `src` folder
|
||||||
|
by including it, with the C preprocessing directive `#include'.
|
||||||
|
|
||||||
|
```src/main.c
|
||||||
|
|
||||||
|
#include "header.h"
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Including a header file produces the same results as copying the header file
|
||||||
|
into each source file that needs it. Such copying would be time-consuming
|
||||||
|
and error-prone. With a header file, the related declarations appear
|
||||||
|
in only one place. If they need to be changed, they can be changed in one
|
||||||
|
place, and programs that include the header file will automatically use the
|
||||||
|
new version when next recompiled. The header file eliminates the labor of
|
||||||
|
finding and changing all the copies as well as the risk that a failure to
|
||||||
|
find one copy will result in inconsistencies within a program.
|
||||||
|
|
||||||
|
In C, the convention is to give header files names that end with `.h'.
|
||||||
|
|
||||||
|
Read more about using header files in official GCC documentation:
|
||||||
|
|
||||||
|
* Include Syntax
|
||||||
|
* Include Operation
|
||||||
|
* Once-Only Headers
|
||||||
|
* Computed Includes
|
||||||
|
|
||||||
|
https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
#include "EpdFont.h"
|
||||||
|
|
||||||
|
#include <Utf8.h>
|
||||||
|
|
||||||
|
EpdFont::EpdFont(const EpdFontData* data) { this->data = data; }
|
||||||
|
|
||||||
|
inline int min(const int a, const int b) { return a < b ? a : b; }
|
||||||
|
inline int max(const int a, const int b) { return a < b ? b : a; }
|
||||||
|
|
||||||
|
// TODO: Text properties??
|
||||||
|
void EpdFont::getTextBounds(const char* string, const int startX, const int startY, int* minX, int* minY, int* maxX,
|
||||||
|
int* maxY) const {
|
||||||
|
*minX = startX;
|
||||||
|
*minY = startY;
|
||||||
|
*maxX = startX;
|
||||||
|
*maxY = startY;
|
||||||
|
|
||||||
|
if (*string == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cursorX = startX;
|
||||||
|
const int cursorY = startY;
|
||||||
|
uint32_t cp;
|
||||||
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||||
|
const EpdGlyph* glyph = getGlyph(cp);
|
||||||
|
|
||||||
|
if (!glyph) {
|
||||||
|
// TODO: Replace with fallback glyph property?
|
||||||
|
glyph = getGlyph('?');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!glyph) {
|
||||||
|
// TODO: Better handle this?
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
*minX = min(*minX, cursorX + glyph->left);
|
||||||
|
*maxX = max(*maxX, cursorX + glyph->left + glyph->width);
|
||||||
|
*minY = min(*minY, cursorY + glyph->top - glyph->height);
|
||||||
|
*maxY = max(*maxY, cursorY + glyph->top);
|
||||||
|
cursorX += glyph->advanceX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpdFont::getTextDimensions(const char* string, int* w, int* h) const {
|
||||||
|
int minX = 0, minY = 0, maxX = 0, maxY = 0;
|
||||||
|
|
||||||
|
getTextBounds(string, 0, 0, &minX, &minY, &maxX, &maxY);
|
||||||
|
|
||||||
|
*w = maxX - minX;
|
||||||
|
*h = maxY - minY;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EpdFont::hasPrintableChars(const char* string) const {
|
||||||
|
int w = 0, h = 0;
|
||||||
|
|
||||||
|
getTextDimensions(string, &w, &h);
|
||||||
|
|
||||||
|
return w > 0 || h > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EpdGlyph* EpdFont::getGlyph(const uint32_t cp) const {
|
||||||
|
const EpdUnicodeInterval* intervals = data->intervals;
|
||||||
|
for (int i = 0; i < data->intervalCount; i++) {
|
||||||
|
const EpdUnicodeInterval* interval = &intervals[i];
|
||||||
|
if (cp >= interval->first && cp <= interval->last) {
|
||||||
|
return &data->glyph[interval->offset + (cp - interval->first)];
|
||||||
|
}
|
||||||
|
if (cp < interval->first) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "EpdFontData.h"
|
||||||
|
|
||||||
|
class EpdFont {
|
||||||
|
void getTextBounds(const char* string, int startX, int startY, int* minX, int* minY, int* maxX, int* maxY) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
const EpdFontData* data;
|
||||||
|
explicit EpdFont(const EpdFontData* data);
|
||||||
|
~EpdFont() = default;
|
||||||
|
void getTextDimensions(const char* string, int* w, int* h) const;
|
||||||
|
bool hasPrintableChars(const char* string) const;
|
||||||
|
|
||||||
|
const EpdGlyph* getGlyph(uint32_t cp) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// From
|
||||||
|
// https://github.com/vroland/epdiy/blob/c61e9e923ce2418150d54f88cea5d196cdc40c54/src/epd_internals.h
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
/// Font data stored PER GLYPH
|
||||||
|
typedef struct {
|
||||||
|
uint8_t width; ///< Bitmap dimensions in pixels
|
||||||
|
uint8_t height; ///< Bitmap dimensions in pixels
|
||||||
|
uint8_t advanceX; ///< Distance to advance cursor (x axis)
|
||||||
|
int16_t left; ///< X dist from cursor pos to UL corner
|
||||||
|
int16_t top; ///< Y dist from cursor pos to UL corner
|
||||||
|
uint16_t compressedSize; ///< Size of the zlib-compressed font data.
|
||||||
|
uint32_t dataOffset; ///< Pointer into EpdFont->bitmap
|
||||||
|
} EpdGlyph;
|
||||||
|
|
||||||
|
/// Glyph interval structure
|
||||||
|
typedef struct {
|
||||||
|
uint32_t first; ///< The first unicode code point of the interval
|
||||||
|
uint32_t last; ///< The last unicode code point of the interval
|
||||||
|
uint32_t offset; ///< Index of the first code point into the glyph array
|
||||||
|
} EpdUnicodeInterval;
|
||||||
|
|
||||||
|
/// Data stored for FONT AS A WHOLE
|
||||||
|
typedef struct {
|
||||||
|
const uint8_t* bitmap; ///< Glyph bitmaps, concatenated
|
||||||
|
const EpdGlyph* glyph; ///< Glyph array
|
||||||
|
const EpdUnicodeInterval* intervals; ///< Valid unicode intervals for this font
|
||||||
|
uint32_t intervalCount; ///< Number of unicode intervals.
|
||||||
|
bool compressed; ///< Does this font use compressed glyph bitmaps?
|
||||||
|
uint8_t advanceY; ///< Newline distance (y axis)
|
||||||
|
int ascender; ///< Maximal height of a glyph above the base line
|
||||||
|
int descender; ///< Maximal height of a glyph below the base line
|
||||||
|
} EpdFontData;
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Executable
+237
@@ -0,0 +1,237 @@
|
|||||||
|
#!python3
|
||||||
|
import freetype
|
||||||
|
import zlib
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import math
|
||||||
|
import argparse
|
||||||
|
from collections import namedtuple
|
||||||
|
|
||||||
|
# From: https://github.com/vroland/epdiy
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="Generate a header file from a font to be used with epdiy.")
|
||||||
|
parser.add_argument("name", action="store", help="name of the font.")
|
||||||
|
parser.add_argument("size", type=int, help="font size to use.")
|
||||||
|
parser.add_argument("fontstack", action="store", nargs='+', help="list of font files, ordered by descending priority.")
|
||||||
|
parser.add_argument("--compress", dest="compress", action="store_true", help="compress glyph bitmaps.")
|
||||||
|
parser.add_argument("--additional-intervals", dest="additional_intervals", action="append", help="Additional code point intervals to export as min,max. This argument can be repeated.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
GlyphProps = namedtuple("GlyphProps", ["width", "height", "advance_x", "left", "top", "compressed_size", "data_offset", "code_point"])
|
||||||
|
|
||||||
|
font_stack = [freetype.Face(f) for f in args.fontstack]
|
||||||
|
compress = args.compress
|
||||||
|
size = args.size
|
||||||
|
font_name = args.name
|
||||||
|
|
||||||
|
# inclusive unicode code point intervals
|
||||||
|
# must not overlap and be in ascending order
|
||||||
|
intervals = [
|
||||||
|
### Basic Latin ###
|
||||||
|
# ASCII letters, digits, punctuation, control characters
|
||||||
|
(0x0000, 0x007F),
|
||||||
|
### Latin-1 Supplement ###
|
||||||
|
# Accented characters for Western European languages
|
||||||
|
(0x0080, 0x00FF),
|
||||||
|
### Latin Extended-A ###
|
||||||
|
# Eastern European and Baltic languages
|
||||||
|
(0x0100, 0x017F),
|
||||||
|
### General Punctuation (core subset) ###
|
||||||
|
# Smart quotes, en dash, em dash, ellipsis, NO-BREAK SPACE
|
||||||
|
(0x2000, 0x206F),
|
||||||
|
### Basic Symbols From "Latin-1 + Misc" ###
|
||||||
|
# dashes, quotes, prime marks
|
||||||
|
(0x2010, 0x203A),
|
||||||
|
# misc punctuation
|
||||||
|
(0x2040, 0x205F),
|
||||||
|
# common currency symbols
|
||||||
|
(0x20A0, 0x20CF),
|
||||||
|
### Combining Diacritical Marks (minimal subset) ###
|
||||||
|
# Needed for proper rendering of many extended Latin languages
|
||||||
|
(0x0300, 0x036F),
|
||||||
|
### Greek & Coptic ###
|
||||||
|
# Used in science, maths, philosophy, some academic texts
|
||||||
|
# (0x0370, 0x03FF),
|
||||||
|
### Cyrillic ###
|
||||||
|
# Russian, Ukrainian, Bulgarian, etc.
|
||||||
|
# (0x0400, 0x04FF),
|
||||||
|
### Math Symbols (common subset) ###
|
||||||
|
# General math operators
|
||||||
|
(0x2200, 0x22FF),
|
||||||
|
# Arrows
|
||||||
|
(0x2190, 0x21FF),
|
||||||
|
### CJK ###
|
||||||
|
# Core Unified Ideographs
|
||||||
|
# (0x4E00, 0x9FFF),
|
||||||
|
# # Extension A
|
||||||
|
# (0x3400, 0x4DBF),
|
||||||
|
# # Extension B
|
||||||
|
# (0x20000, 0x2A6DF),
|
||||||
|
# # Extension C–F
|
||||||
|
# (0x2A700, 0x2EBEF),
|
||||||
|
# # Extension G
|
||||||
|
# (0x30000, 0x3134F),
|
||||||
|
# # Hiragana
|
||||||
|
# (0x3040, 0x309F),
|
||||||
|
# # Katakana
|
||||||
|
# (0x30A0, 0x30FF),
|
||||||
|
# # Katakana Phonetic Extensions
|
||||||
|
# (0x31F0, 0x31FF),
|
||||||
|
# # Halfwidth Katakana
|
||||||
|
# (0xFF60, 0xFF9F),
|
||||||
|
# # Hangul Syllables
|
||||||
|
# (0xAC00, 0xD7AF),
|
||||||
|
# # Hangul Jamo
|
||||||
|
# (0x1100, 0x11FF),
|
||||||
|
# # Hangul Compatibility Jamo
|
||||||
|
# (0x3130, 0x318F),
|
||||||
|
# # Hangul Jamo Extended-A
|
||||||
|
# (0xA960, 0xA97F),
|
||||||
|
# # Hangul Jamo Extended-B
|
||||||
|
# (0xD7B0, 0xD7FF),
|
||||||
|
# # CJK Radicals Supplement
|
||||||
|
# (0x2E80, 0x2EFF),
|
||||||
|
# # Kangxi Radicals
|
||||||
|
# (0x2F00, 0x2FDF),
|
||||||
|
# # CJK Symbols and Punctuation
|
||||||
|
# (0x3000, 0x303F),
|
||||||
|
# # CJK Compatibility Forms
|
||||||
|
# (0xFE30, 0xFE4F),
|
||||||
|
# # CJK Compatibility Ideographs
|
||||||
|
# (0xF900, 0xFAFF),
|
||||||
|
]
|
||||||
|
|
||||||
|
add_ints = []
|
||||||
|
if args.additional_intervals:
|
||||||
|
add_ints = [tuple([int(n, base=0) for n in i.split(",")]) for i in args.additional_intervals]
|
||||||
|
|
||||||
|
def norm_floor(val):
|
||||||
|
return int(math.floor(val / (1 << 6)))
|
||||||
|
|
||||||
|
def norm_ceil(val):
|
||||||
|
return int(math.ceil(val / (1 << 6)))
|
||||||
|
|
||||||
|
def chunks(l, n):
|
||||||
|
for i in range(0, len(l), n):
|
||||||
|
yield l[i:i + n]
|
||||||
|
|
||||||
|
def load_glyph(code_point):
|
||||||
|
face_index = 0
|
||||||
|
while face_index < len(font_stack):
|
||||||
|
face = font_stack[face_index]
|
||||||
|
glyph_index = face.get_char_index(code_point)
|
||||||
|
if glyph_index > 0:
|
||||||
|
face.load_glyph(glyph_index, freetype.FT_LOAD_RENDER)
|
||||||
|
return face
|
||||||
|
face_index += 1
|
||||||
|
print(f"code point {code_point} ({hex(code_point)}) not found in font stack!", file=sys.stderr)
|
||||||
|
return None
|
||||||
|
|
||||||
|
unmerged_intervals = sorted(intervals + add_ints)
|
||||||
|
intervals = []
|
||||||
|
unvalidated_intervals = []
|
||||||
|
for i_start, i_end in unmerged_intervals:
|
||||||
|
if len(unvalidated_intervals) > 0 and i_start + 1 <= unvalidated_intervals[-1][1]:
|
||||||
|
unvalidated_intervals[-1] = (unvalidated_intervals[-1][0], max(unvalidated_intervals[-1][1], i_end))
|
||||||
|
continue
|
||||||
|
unvalidated_intervals.append((i_start, i_end))
|
||||||
|
|
||||||
|
for i_start, i_end in unvalidated_intervals:
|
||||||
|
start = i_start
|
||||||
|
for code_point in range(i_start, i_end + 1):
|
||||||
|
face = load_glyph(code_point)
|
||||||
|
if face is None:
|
||||||
|
if start < code_point:
|
||||||
|
intervals.append((start, code_point - 1))
|
||||||
|
start = code_point + 1
|
||||||
|
if start != i_end + 1:
|
||||||
|
intervals.append((start, i_end))
|
||||||
|
|
||||||
|
for face in font_stack:
|
||||||
|
# shift by 6 bytes, because sizes are given as 6-bit fractions
|
||||||
|
# the display has about 150 dpi.
|
||||||
|
face.set_char_size(size << 6, size << 6, 150, 150)
|
||||||
|
|
||||||
|
total_size = 0
|
||||||
|
total_packed = 0
|
||||||
|
all_glyphs = []
|
||||||
|
|
||||||
|
for i_start, i_end in intervals:
|
||||||
|
for code_point in range(i_start, i_end + 1):
|
||||||
|
face = load_glyph(code_point)
|
||||||
|
bitmap = face.glyph.bitmap
|
||||||
|
pixels = []
|
||||||
|
px = 0
|
||||||
|
for i, v in enumerate(bitmap.buffer):
|
||||||
|
y = i / bitmap.width
|
||||||
|
x = i % bitmap.width
|
||||||
|
if x % 2 == 0:
|
||||||
|
px = (v >> 4)
|
||||||
|
else:
|
||||||
|
px = px | (v & 0xF0)
|
||||||
|
pixels.append(px);
|
||||||
|
px = 0
|
||||||
|
# eol
|
||||||
|
if x == bitmap.width - 1 and bitmap.width % 2 > 0:
|
||||||
|
pixels.append(px)
|
||||||
|
px = 0
|
||||||
|
|
||||||
|
packed = bytes(pixels);
|
||||||
|
total_packed += len(packed)
|
||||||
|
compressed = packed
|
||||||
|
if compress:
|
||||||
|
compressed = zlib.compress(packed)
|
||||||
|
|
||||||
|
glyph = GlyphProps(
|
||||||
|
width = bitmap.width,
|
||||||
|
height = bitmap.rows,
|
||||||
|
advance_x = norm_floor(face.glyph.advance.x),
|
||||||
|
left = face.glyph.bitmap_left,
|
||||||
|
top = face.glyph.bitmap_top,
|
||||||
|
compressed_size = len(compressed),
|
||||||
|
data_offset = total_size,
|
||||||
|
code_point = code_point,
|
||||||
|
)
|
||||||
|
total_size += len(compressed)
|
||||||
|
all_glyphs.append((glyph, compressed))
|
||||||
|
|
||||||
|
# pipe seems to be a good heuristic for the "real" descender
|
||||||
|
face = load_glyph(ord('|'))
|
||||||
|
|
||||||
|
glyph_data = []
|
||||||
|
glyph_props = []
|
||||||
|
for index, glyph in enumerate(all_glyphs):
|
||||||
|
props, compressed = glyph
|
||||||
|
glyph_data.extend([b for b in compressed])
|
||||||
|
glyph_props.append(props)
|
||||||
|
|
||||||
|
print(f"/**\n * generated by fontconvert.py\n * name: {font_name}\n * size: {size}\n * compressed: {compress}\n */")
|
||||||
|
print("#pragma once")
|
||||||
|
print("#include \"EpdFontData.h\"\n")
|
||||||
|
print(f"static const uint8_t {font_name}Bitmaps[{len(glyph_data)}] = {{")
|
||||||
|
for c in chunks(glyph_data, 16):
|
||||||
|
print (" " + " ".join(f"0x{b:02X}," for b in c))
|
||||||
|
print ("};\n");
|
||||||
|
|
||||||
|
print(f"static const EpdGlyph {font_name}Glyphs[] = {{")
|
||||||
|
for i, g in enumerate(glyph_props):
|
||||||
|
print (" { " + ", ".join([f"{a}" for a in list(g[:-1])]),"},", f"// {chr(g.code_point) if g.code_point != 92 else '<backslash>'}")
|
||||||
|
print ("};\n");
|
||||||
|
|
||||||
|
print(f"static const EpdUnicodeInterval {font_name}Intervals[] = {{")
|
||||||
|
offset = 0
|
||||||
|
for i_start, i_end in intervals:
|
||||||
|
print (f" {{ 0x{i_start:X}, 0x{i_end:X}, 0x{offset:X} }},")
|
||||||
|
offset += i_end - i_start + 1
|
||||||
|
print ("};\n");
|
||||||
|
|
||||||
|
print(f"static const EpdFontData {font_name} = {{")
|
||||||
|
print(f" {font_name}Bitmaps,")
|
||||||
|
print(f" {font_name}Glyphs,")
|
||||||
|
print(f" {font_name}Intervals,")
|
||||||
|
print(f" {len(intervals)},")
|
||||||
|
print(f" {1 if compress else 0},")
|
||||||
|
print(f" {norm_ceil(face.size.height)},")
|
||||||
|
print(f" {norm_ceil(face.size.ascender)},")
|
||||||
|
print(f" {norm_floor(face.size.descender)},")
|
||||||
|
print("};")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
freetype-py==2.5.1
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <EpdFont.h>
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <Utf8.h>
|
||||||
|
#include <miniz.h>
|
||||||
|
|
||||||
|
inline int min(const int a, const int b) { return a < b ? a : b; }
|
||||||
|
inline int max(const int a, const int b) { return a > b ? a : b; }
|
||||||
|
|
||||||
|
static tinfl_decompressor decomp;
|
||||||
|
|
||||||
|
template <typename Renderable>
|
||||||
|
class EpdFontRenderer {
|
||||||
|
Renderable* renderer;
|
||||||
|
void renderChar(uint32_t cp, int* x, const int* y, uint16_t color);
|
||||||
|
|
||||||
|
public:
|
||||||
|
const EpdFont* font;
|
||||||
|
explicit EpdFontRenderer(const EpdFont* font, Renderable* renderer);
|
||||||
|
~EpdFontRenderer() = default;
|
||||||
|
void renderString(const char* string, int* x, int* y, uint16_t color);
|
||||||
|
};
|
||||||
|
|
||||||
|
inline int uncompress(uint8_t* dest, size_t uncompressedSize, const uint8_t* source, size_t sourceSize) {
|
||||||
|
if (uncompressedSize == 0 || dest == nullptr || sourceSize == 0 || source == nullptr) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
tinfl_init(&decomp);
|
||||||
|
|
||||||
|
// we know everything will fit into the buffer.
|
||||||
|
const tinfl_status decomp_status =
|
||||||
|
tinfl_decompress(&decomp, source, &sourceSize, dest, dest, &uncompressedSize,
|
||||||
|
TINFL_FLAG_PARSE_ZLIB_HEADER | TINFL_FLAG_USING_NON_WRAPPING_OUTPUT_BUF);
|
||||||
|
if (decomp_status != TINFL_STATUS_DONE) {
|
||||||
|
return decomp_status;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Renderable>
|
||||||
|
EpdFontRenderer<Renderable>::EpdFontRenderer(const EpdFont* font, Renderable* renderer) {
|
||||||
|
this->font = font;
|
||||||
|
this->renderer = renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Renderable>
|
||||||
|
void EpdFontRenderer<Renderable>::renderString(const char* string, int* x, int* y, const uint16_t color) {
|
||||||
|
// cannot draw a NULL / empty string
|
||||||
|
if (string == nullptr || *string == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no printable characters
|
||||||
|
if (!font->hasPrintableChars(string)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t cp;
|
||||||
|
while ((cp = utf8NextCodepoint(reinterpret_cast<const uint8_t**>(&string)))) {
|
||||||
|
renderChar(cp, x, y, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
*y += font->data->advanceY;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename Renderable>
|
||||||
|
void EpdFontRenderer<Renderable>::renderChar(const uint32_t cp, int* x, const int* y, uint16_t color) {
|
||||||
|
const EpdGlyph* glyph = font->getGlyph(cp);
|
||||||
|
if (!glyph) {
|
||||||
|
// TODO: Replace with fallback glyph property?
|
||||||
|
glyph = font->getGlyph('?');
|
||||||
|
}
|
||||||
|
|
||||||
|
// no glyph?
|
||||||
|
if (!glyph) {
|
||||||
|
Serial.printf("No glyph for codepoint %d\n", cp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t offset = glyph->dataOffset;
|
||||||
|
const uint8_t width = glyph->width;
|
||||||
|
const uint8_t height = glyph->height;
|
||||||
|
const int left = glyph->left;
|
||||||
|
|
||||||
|
const int byteWidth = width / 2 + width % 2;
|
||||||
|
const unsigned long bitmapSize = byteWidth * height;
|
||||||
|
const uint8_t* bitmap = nullptr;
|
||||||
|
|
||||||
|
if (font->data->compressed) {
|
||||||
|
auto* tmpBitmap = static_cast<uint8_t*>(malloc(bitmapSize));
|
||||||
|
if (tmpBitmap == nullptr && bitmapSize) {
|
||||||
|
// ESP_LOGE("font", "malloc failed.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uncompress(tmpBitmap, bitmapSize, &font->data->bitmap[offset], glyph->compressedSize);
|
||||||
|
bitmap = tmpBitmap;
|
||||||
|
} else {
|
||||||
|
bitmap = &font->data->bitmap[offset];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmap != nullptr) {
|
||||||
|
for (int localY = 0; localY < height; localY++) {
|
||||||
|
int yy = *y - glyph->top + localY;
|
||||||
|
const int startPos = *x + left;
|
||||||
|
bool byteComplete = startPos % 2;
|
||||||
|
int localX = max(0, -startPos);
|
||||||
|
const int maxX = startPos + width;
|
||||||
|
|
||||||
|
for (int xx = startPos; xx < maxX; xx++) {
|
||||||
|
uint8_t bm = bitmap[localY * byteWidth + localX / 2];
|
||||||
|
if ((localX & 1) == 0) {
|
||||||
|
bm = bm & 0xF;
|
||||||
|
} else {
|
||||||
|
bm = bm >> 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bm) {
|
||||||
|
renderer->drawPixel(xx, yy, color);
|
||||||
|
}
|
||||||
|
byteComplete = !byteComplete;
|
||||||
|
localX++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (font->data->compressed) {
|
||||||
|
free(const_cast<uint8_t*>(bitmap));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*x += glyph->advanceX;
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#include "EpdRenderer.h"
|
||||||
|
|
||||||
|
#include "builtinFonts/babyblue.h"
|
||||||
|
#include "builtinFonts/bookerly.h"
|
||||||
|
#include "builtinFonts/bookerly_bold.h"
|
||||||
|
#include "builtinFonts/bookerly_bold_italic.h"
|
||||||
|
#include "builtinFonts/bookerly_italic.h"
|
||||||
|
|
||||||
|
EpdRenderer::EpdRenderer(XteinkDisplay* display) {
|
||||||
|
this->display = display;
|
||||||
|
this->regularFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly), display);
|
||||||
|
this->boldFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly_bold), display);
|
||||||
|
this->italicFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly_italic), display);
|
||||||
|
this->bold_italicFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&bookerly_bold_italic), display);
|
||||||
|
this->smallFont = new EpdFontRenderer<XteinkDisplay>(new EpdFont(&babyblue), display);
|
||||||
|
|
||||||
|
this->marginTop = 11;
|
||||||
|
this->marginBottom = 30;
|
||||||
|
this->marginLeft = 10;
|
||||||
|
this->marginRight = 10;
|
||||||
|
this->lineCompression = 0.95f;
|
||||||
|
}
|
||||||
|
|
||||||
|
EpdFontRenderer<XteinkDisplay>* EpdRenderer::getFontRenderer(const bool bold, const bool italic) const {
|
||||||
|
if (bold && italic) {
|
||||||
|
return bold_italicFont;
|
||||||
|
}
|
||||||
|
if (bold) {
|
||||||
|
return boldFont;
|
||||||
|
}
|
||||||
|
if (italic) {
|
||||||
|
return italicFont;
|
||||||
|
}
|
||||||
|
return regularFont;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EpdRenderer::getTextWidth(const char* text, const bool bold, const bool italic) const {
|
||||||
|
int w = 0, h = 0;
|
||||||
|
|
||||||
|
getFontRenderer(bold, italic)->font->getTextDimensions(text, &w, &h);
|
||||||
|
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
int EpdRenderer::getSmallTextWidth(const char* text) const {
|
||||||
|
int w = 0, h = 0;
|
||||||
|
|
||||||
|
smallFont->font->getTextDimensions(text, &w, &h);
|
||||||
|
|
||||||
|
return w;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::drawText(const int x, const int y, const char* text, const bool bold, const bool italic,
|
||||||
|
const uint16_t color) const {
|
||||||
|
int ypos = y + getLineHeight() + marginTop;
|
||||||
|
int xpos = x + marginLeft;
|
||||||
|
getFontRenderer(bold, italic)->renderString(text, &xpos, &ypos, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::drawSmallText(const int x, const int y, const char* text) const {
|
||||||
|
int ypos = y + smallFont->font->data->advanceY + marginTop;
|
||||||
|
int xpos = x + marginLeft;
|
||||||
|
smallFont->renderString(text, &xpos, &ypos, GxEPD_BLACK);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::drawTextBox(const int x, const int y, const std::string& text, const int width, const int height,
|
||||||
|
const bool bold, const bool italic) const {
|
||||||
|
const size_t length = text.length();
|
||||||
|
// fit the text into the box
|
||||||
|
int start = 0;
|
||||||
|
int end = 1;
|
||||||
|
int ypos = 0;
|
||||||
|
while (true) {
|
||||||
|
if (end >= length) {
|
||||||
|
drawText(x, y + ypos, text.substr(start, length - start).c_str(), bold, italic);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ypos + getLineHeight() >= height) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text[end - 1] == '\n') {
|
||||||
|
drawText(x, y + ypos, text.substr(start, end - start).c_str(), bold, italic);
|
||||||
|
ypos += getLineHeight();
|
||||||
|
start = end;
|
||||||
|
end = start + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getTextWidth(text.substr(start, end - start).c_str(), bold, italic) > width) {
|
||||||
|
drawText(x, y + ypos, text.substr(start, end - start - 1).c_str(), bold, italic);
|
||||||
|
ypos += getLineHeight();
|
||||||
|
start = end - 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
end++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::drawLine(int x1, int y1, int x2, int y2, uint16_t color) const {
|
||||||
|
display->drawLine(x1 + marginLeft, y1 + marginTop, x2 + marginLeft, y2 + marginTop,
|
||||||
|
color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::drawRect(const int x, const int y, const int width, const int height, const uint16_t color) const {
|
||||||
|
display->drawRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::fillRect(const int x, const int y, const int width, const int height,
|
||||||
|
const uint16_t color = 0) const {
|
||||||
|
display->fillRect(x + marginLeft, y + marginTop, width, height, color > 0 ? GxEPD_BLACK : GxEPD_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::clearScreen(const bool black) const {
|
||||||
|
Serial.println("Clearing screen");
|
||||||
|
display->fillScreen(black ? GxEPD_BLACK : GxEPD_WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpdRenderer::flushDisplay() const { display->display(true); }
|
||||||
|
|
||||||
|
void EpdRenderer::flushArea(int x, int y, int width, int height) const {
|
||||||
|
// TODO: Fix
|
||||||
|
display->display(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
int EpdRenderer::getPageWidth() const { return display->width() - marginLeft - marginRight; }
|
||||||
|
|
||||||
|
int EpdRenderer::getPageHeight() const { return display->height() - marginTop - marginBottom; }
|
||||||
|
|
||||||
|
int EpdRenderer::getSpaceWidth() const { return regularFont->font->getGlyph(' ')->advanceX; }
|
||||||
|
|
||||||
|
int EpdRenderer::getLineHeight() const { return regularFont->font->data->advanceY * lineCompression; }
|
||||||
|
|
||||||
|
// deep sleep helper - persist any state to disk that may be needed on wake
|
||||||
|
bool EpdRenderer::dehydrate() {
|
||||||
|
// TODO: Implement
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// deep sleep helper - retrieve any state from disk after wake
|
||||||
|
bool EpdRenderer::hydrate() {
|
||||||
|
// TODO: Implement
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// really really clear the screen
|
||||||
|
void EpdRenderer::reset() {
|
||||||
|
// TODO: Implement
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <GxEPD2_BW.h>
|
||||||
|
|
||||||
|
#include <EpdFontRenderer.hpp>
|
||||||
|
|
||||||
|
#define XteinkDisplay GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT>
|
||||||
|
|
||||||
|
class EpdRenderer {
|
||||||
|
XteinkDisplay* display;
|
||||||
|
EpdFontRenderer<XteinkDisplay>* regularFont;
|
||||||
|
EpdFontRenderer<XteinkDisplay>* boldFont;
|
||||||
|
EpdFontRenderer<XteinkDisplay>* italicFont;
|
||||||
|
EpdFontRenderer<XteinkDisplay>* bold_italicFont;
|
||||||
|
EpdFontRenderer<XteinkDisplay>* smallFont;
|
||||||
|
int marginTop;
|
||||||
|
int marginBottom;
|
||||||
|
int marginLeft;
|
||||||
|
int marginRight;
|
||||||
|
float lineCompression;
|
||||||
|
EpdFontRenderer<XteinkDisplay>* getFontRenderer(bool bold, bool italic) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit EpdRenderer(XteinkDisplay* display);
|
||||||
|
~EpdRenderer() = default;
|
||||||
|
int getTextWidth(const char* text, bool bold = false, bool italic = false) const;
|
||||||
|
int getSmallTextWidth(const char* text) const;
|
||||||
|
void drawText(int x, int y, const char* text, bool bold = false, bool italic = false, uint16_t color = 1) const;
|
||||||
|
void drawSmallText(int x, int y, const char* text) const;
|
||||||
|
void drawTextBox(int x, int y, const std::string& text, int width, int height, bool bold = false,
|
||||||
|
bool italic = false) const;
|
||||||
|
void drawLine(int x1, int y1, int x2, int y2, uint16_t color) const;
|
||||||
|
void drawRect(int x, int y, int width, int height, uint16_t color) const;
|
||||||
|
void fillRect(int x, int y, int width, int height, uint16_t color) const;
|
||||||
|
void clearScreen(bool black = false) const;
|
||||||
|
void flushDisplay() const;
|
||||||
|
void flushArea(int x, int y, int width, int height) const;
|
||||||
|
|
||||||
|
int getPageWidth() const;
|
||||||
|
int getPageHeight() const;
|
||||||
|
int getSpaceWidth() const;
|
||||||
|
int getLineHeight() const;
|
||||||
|
// set margins
|
||||||
|
void setMarginTop(const int newMarginTop) { this->marginTop = newMarginTop; }
|
||||||
|
void setMarginBottom(const int newMarginBottom) { this->marginBottom = newMarginBottom; }
|
||||||
|
void setMarginLeft(const int newMarginLeft) { this->marginLeft = newMarginLeft; }
|
||||||
|
void setMarginRight(const int newMarginRight) { this->marginRight = newMarginRight; }
|
||||||
|
// deep sleep helper - persist any state to disk that may be needed on wake
|
||||||
|
bool dehydrate();
|
||||||
|
// deep sleep helper - retrieve any state from disk after wake
|
||||||
|
bool hydrate();
|
||||||
|
// really really clear the screen
|
||||||
|
void reset();
|
||||||
|
|
||||||
|
uint8_t temperature = 20;
|
||||||
|
};
|
||||||
@@ -0,0 +1,383 @@
|
|||||||
|
#include "Epub.h"
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <SD.h>
|
||||||
|
#include <ZipFile.h>
|
||||||
|
#include <tinyxml2.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
bool Epub::findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile) {
|
||||||
|
// open up the meta data to find where the content.opf file lives
|
||||||
|
size_t s;
|
||||||
|
const auto metaInfo = zip.readTextFileToMemory("META-INF/container.xml", &s);
|
||||||
|
if (!metaInfo) {
|
||||||
|
Serial.println("Could not find META-INF/container.xml");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse the meta data
|
||||||
|
tinyxml2::XMLDocument metaDataDoc;
|
||||||
|
const auto result = metaDataDoc.Parse(metaInfo);
|
||||||
|
free(metaInfo);
|
||||||
|
|
||||||
|
if (result != tinyxml2::XML_SUCCESS) {
|
||||||
|
Serial.printf("Could not parse META-INF/container.xml. Error: %d\n", result);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto container = metaDataDoc.FirstChildElement("container");
|
||||||
|
if (!container) {
|
||||||
|
Serial.println("Could not find container element in META-INF/container.xml");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto rootfiles = container->FirstChildElement("rootfiles");
|
||||||
|
if (!rootfiles) {
|
||||||
|
Serial.println("Could not find rootfiles element in META-INF/container.xml");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the root file that has the media-type="application/oebps-package+xml"
|
||||||
|
auto rootfile = rootfiles->FirstChildElement("rootfile");
|
||||||
|
while (rootfile) {
|
||||||
|
const char* mediaType = rootfile->Attribute("media-type");
|
||||||
|
if (mediaType && strcmp(mediaType, "application/oebps-package+xml") == 0) {
|
||||||
|
const char* full_path = rootfile->Attribute("full-path");
|
||||||
|
if (full_path) {
|
||||||
|
contentOpfFile = full_path;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootfile = rootfile->NextSiblingElement("rootfile");
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Could not get path to content.opf file");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::parseContentOpf(ZipFile& zip, std::string& content_opf_file) {
|
||||||
|
// read in the content.opf file and parse it
|
||||||
|
auto contents = zip.readTextFileToMemory(content_opf_file.c_str());
|
||||||
|
|
||||||
|
// parse the contents
|
||||||
|
tinyxml2::XMLDocument doc;
|
||||||
|
auto result = doc.Parse(contents);
|
||||||
|
free(contents);
|
||||||
|
|
||||||
|
if (result != tinyxml2::XML_SUCCESS) {
|
||||||
|
Serial.printf("Error parsing content.opf - %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto package = doc.FirstChildElement("package");
|
||||||
|
if (!package) package = doc.FirstChildElement("opf:package");
|
||||||
|
|
||||||
|
if (!package) {
|
||||||
|
Serial.println("Could not find package element in content.opf");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the metadata - title and cover image
|
||||||
|
auto metadata = package->FirstChildElement("metadata");
|
||||||
|
if (!metadata) metadata = package->FirstChildElement("opf:metadata");
|
||||||
|
if (!metadata) {
|
||||||
|
Serial.println("Missing metadata");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto titleEl = metadata->FirstChildElement("dc:title");
|
||||||
|
if (!titleEl) {
|
||||||
|
Serial.println("Missing title");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this->title = titleEl->GetText();
|
||||||
|
|
||||||
|
auto cover = metadata->FirstChildElement("meta");
|
||||||
|
if (!cover) cover = metadata->FirstChildElement("opf:meta");
|
||||||
|
while (cover && cover->Attribute("name") && strcmp(cover->Attribute("name"), "cover") != 0) {
|
||||||
|
cover = cover->NextSiblingElement("meta");
|
||||||
|
}
|
||||||
|
if (!cover) {
|
||||||
|
Serial.println("Missing cover");
|
||||||
|
}
|
||||||
|
auto coverItem = cover ? cover->Attribute("content") : nullptr;
|
||||||
|
|
||||||
|
// read the manifest and spine
|
||||||
|
// the manifest gives us the names of the files
|
||||||
|
// the spine gives us the order of the files
|
||||||
|
// we can then read the files in the order they are in the spine
|
||||||
|
auto manifest = package->FirstChildElement("manifest");
|
||||||
|
if (!manifest) manifest = package->FirstChildElement("opf:manifest");
|
||||||
|
if (!manifest) {
|
||||||
|
Serial.println("Missing manifest");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a mapping from id to file name
|
||||||
|
auto item = manifest->FirstChildElement("item");
|
||||||
|
if (!item) item = manifest->FirstChildElement("opf:item");
|
||||||
|
std::map<std::string, std::string> items;
|
||||||
|
|
||||||
|
while (item) {
|
||||||
|
std::string itemId = item->Attribute("id");
|
||||||
|
std::string href = contentBasePath + item->Attribute("href");
|
||||||
|
|
||||||
|
// grab the cover image
|
||||||
|
if (coverItem && itemId == coverItem) {
|
||||||
|
coverImageItem = href;
|
||||||
|
}
|
||||||
|
|
||||||
|
// grab the ncx file
|
||||||
|
if (itemId == "ncx" || itemId == "ncxtoc") {
|
||||||
|
tocNcxItem = href;
|
||||||
|
}
|
||||||
|
|
||||||
|
items[itemId] = href;
|
||||||
|
auto nextItem = item->NextSiblingElement("item");
|
||||||
|
if (!nextItem) nextItem = item->NextSiblingElement("opf:item");
|
||||||
|
item = nextItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the spine
|
||||||
|
auto spineEl = package->FirstChildElement("spine");
|
||||||
|
if (!spineEl) spineEl = package->FirstChildElement("opf:spine");
|
||||||
|
if (!spineEl) {
|
||||||
|
Serial.println("Missing spine");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the spine
|
||||||
|
auto itemref = spineEl->FirstChildElement("itemref");
|
||||||
|
if (!itemref) itemref = spineEl->FirstChildElement("opf:itemref");
|
||||||
|
while (itemref) {
|
||||||
|
auto id = itemref->Attribute("idref");
|
||||||
|
if (items.find(id) != items.end()) {
|
||||||
|
spine.emplace_back(id, items[id]);
|
||||||
|
}
|
||||||
|
auto nextItemRef = itemref->NextSiblingElement("itemref");
|
||||||
|
if (!nextItemRef) nextItemRef = itemref->NextSiblingElement("opf:itemref");
|
||||||
|
itemref = nextItemRef;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Epub::parseTocNcxFile(ZipFile& zip) {
|
||||||
|
// the ncx file should have been specified in the content.opf file
|
||||||
|
if (tocNcxItem.empty()) {
|
||||||
|
Serial.println("No ncx file specified");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ncxData = zip.readTextFileToMemory(tocNcxItem.c_str());
|
||||||
|
if (!ncxData) {
|
||||||
|
Serial.printf("Could not find %s\n", tocNcxItem.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the Toc contents
|
||||||
|
tinyxml2::XMLDocument doc;
|
||||||
|
auto result = doc.Parse(ncxData);
|
||||||
|
free(ncxData);
|
||||||
|
|
||||||
|
if (result != tinyxml2::XML_SUCCESS) {
|
||||||
|
Serial.printf("Error parsing toc %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ncx = doc.FirstChildElement("ncx");
|
||||||
|
if (!ncx) {
|
||||||
|
Serial.println("Could not find first child ncx in toc");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto navMap = ncx->FirstChildElement("navMap");
|
||||||
|
if (!navMap) {
|
||||||
|
Serial.println("Could not find navMap child in ncx");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto navPoint = navMap->FirstChildElement("navPoint");
|
||||||
|
|
||||||
|
// Fills toc map
|
||||||
|
while (navPoint) {
|
||||||
|
std::string navTitle = navPoint->FirstChildElement("navLabel")->FirstChildElement("text")->FirstChild()->Value();
|
||||||
|
auto content = navPoint->FirstChildElement("content");
|
||||||
|
std::string href = contentBasePath + content->Attribute("src");
|
||||||
|
// split the href on the # to get the href and the anchor
|
||||||
|
size_t pos = href.find('#');
|
||||||
|
std::string anchor;
|
||||||
|
|
||||||
|
if (pos != std::string::npos) {
|
||||||
|
anchor = href.substr(pos + 1);
|
||||||
|
href = href.substr(0, pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
toc.emplace_back(navTitle, href, anchor, 0);
|
||||||
|
navPoint = navPoint->NextSiblingElement("navPoint");
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// load in the meta data for the epub file
|
||||||
|
bool Epub::load() {
|
||||||
|
ZipFile zip("/sd" + filepath);
|
||||||
|
|
||||||
|
std::string contentOpfFile;
|
||||||
|
if (!findContentOpfFile(zip, contentOpfFile)) {
|
||||||
|
Serial.println("Could not open ePub");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentBasePath = contentOpfFile.substr(0, contentOpfFile.find_last_of('/') + 1);
|
||||||
|
|
||||||
|
if (!parseContentOpf(zip, contentOpfFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseTocNcxFile(zip)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Epub::clearCache() const { SD.rmdir(cachePath.c_str()); }
|
||||||
|
|
||||||
|
void Epub::setupCacheDir() const {
|
||||||
|
if (SD.exists(cachePath.c_str())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop over each segment of the cache path and create directories as needed
|
||||||
|
for (size_t i = 1; i < cachePath.length(); i++) {
|
||||||
|
if (cachePath[i] == '/') {
|
||||||
|
SD.mkdir(cachePath.substr(0, i).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SD.mkdir(cachePath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string& Epub::getCachePath() const { return cachePath; }
|
||||||
|
|
||||||
|
const std::string& Epub::getPath() const { return filepath; }
|
||||||
|
|
||||||
|
const std::string& Epub::getTitle() const { return title; }
|
||||||
|
|
||||||
|
const std::string& Epub::getCoverImageItem() const { return coverImageItem; }
|
||||||
|
|
||||||
|
std::string normalisePath(const std::string& path) {
|
||||||
|
std::vector<std::string> components;
|
||||||
|
std::string component;
|
||||||
|
|
||||||
|
for (const auto c : path) {
|
||||||
|
if (c == '/') {
|
||||||
|
if (!component.empty()) {
|
||||||
|
if (component == "..") {
|
||||||
|
if (!components.empty()) {
|
||||||
|
components.pop_back();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
components.push_back(component);
|
||||||
|
}
|
||||||
|
component.clear();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
component += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!component.empty()) {
|
||||||
|
components.push_back(component);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string result;
|
||||||
|
for (const auto& c : components) {
|
||||||
|
if (!result.empty()) {
|
||||||
|
result += "/";
|
||||||
|
}
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* Epub::getItemContents(const std::string& itemHref, size_t* size) const {
|
||||||
|
const ZipFile zip("/sd" + filepath);
|
||||||
|
const std::string path = normalisePath(itemHref);
|
||||||
|
|
||||||
|
const auto content = zip.readFileToMemory(path.c_str(), size);
|
||||||
|
if (!content) {
|
||||||
|
Serial.printf("Failed to read item %s\n", path.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* Epub::getTextItemContents(const std::string& itemHref, size_t* size) const {
|
||||||
|
const ZipFile zip("/sd" + filepath);
|
||||||
|
const std::string path = normalisePath(itemHref);
|
||||||
|
|
||||||
|
const auto content = zip.readTextFileToMemory(path.c_str(), size);
|
||||||
|
if (!content) {
|
||||||
|
Serial.printf("Failed to read item %s\n", path.c_str());
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::getSpineItemsCount() const { return spine.size(); }
|
||||||
|
|
||||||
|
std::string& Epub::getSpineItem(const int spineIndex) {
|
||||||
|
if (spineIndex < 0 || spineIndex >= spine.size()) {
|
||||||
|
Serial.printf("getSpineItem index:%d is out of range\n", spineIndex);
|
||||||
|
return spine.at(0).second;
|
||||||
|
}
|
||||||
|
|
||||||
|
return spine.at(spineIndex).second;
|
||||||
|
}
|
||||||
|
|
||||||
|
EpubTocEntry& Epub::getTocItem(const int tocTndex) {
|
||||||
|
if (tocTndex < 0 || tocTndex >= toc.size()) {
|
||||||
|
Serial.printf("getTocItem index:%d is out of range\n", tocTndex);
|
||||||
|
return toc.at(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toc.at(tocTndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::getTocItemsCount() const { return toc.size(); }
|
||||||
|
|
||||||
|
// work out the section index for a toc index
|
||||||
|
int Epub::getSpineIndexForTocIndex(const int tocIndex) const {
|
||||||
|
// the toc entry should have an href that matches the spine item
|
||||||
|
// so we can find the spine index by looking for the href
|
||||||
|
for (int i = 0; i < spine.size(); i++) {
|
||||||
|
if (spine[i].second == toc[tocIndex].href) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Section not found");
|
||||||
|
// not found - default to the start of the book
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int Epub::getTocIndexForSpineIndex(const int spineIndex) const {
|
||||||
|
// the toc entry should have an href that matches the spine item
|
||||||
|
// so we can find the toc index by looking for the href
|
||||||
|
Serial.printf("Looking for %s\n", spine[spineIndex].second.c_str());
|
||||||
|
for (int i = 0; i < toc.size(); i++) {
|
||||||
|
Serial.printf("Looking at %s\n", toc[i].href.c_str());
|
||||||
|
if (toc[i].href == spine[spineIndex].second) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("TOC item not found");
|
||||||
|
// not found - default to first item
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class ZipFile;
|
||||||
|
|
||||||
|
class EpubTocEntry {
|
||||||
|
public:
|
||||||
|
std::string title;
|
||||||
|
std::string href;
|
||||||
|
std::string anchor;
|
||||||
|
int level;
|
||||||
|
EpubTocEntry(std::string title, std::string href, std::string anchor, const int level)
|
||||||
|
: title(std::move(title)), href(std::move(href)), anchor(std::move(anchor)), level(level) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Epub {
|
||||||
|
// the title read from the EPUB meta data
|
||||||
|
std::string title;
|
||||||
|
// the cover image
|
||||||
|
std::string coverImageItem;
|
||||||
|
// the ncx file
|
||||||
|
std::string tocNcxItem;
|
||||||
|
// where is the EPUBfile?
|
||||||
|
std::string filepath;
|
||||||
|
// the spine of the EPUB file
|
||||||
|
std::vector<std::pair<std::string, std::string>> spine;
|
||||||
|
// the toc of the EPUB file
|
||||||
|
std::vector<EpubTocEntry> toc;
|
||||||
|
// the base path for items in the EPUB file
|
||||||
|
std::string contentBasePath;
|
||||||
|
// Uniq cache key based on filepath
|
||||||
|
std::string cachePath;
|
||||||
|
|
||||||
|
// find the path for the content.opf file
|
||||||
|
static bool findContentOpfFile(const ZipFile& zip, std::string& contentOpfFile);
|
||||||
|
bool parseContentOpf(ZipFile& zip, std::string& content_opf_file);
|
||||||
|
bool parseTocNcxFile(ZipFile& zip);
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Epub(std::string filepath, const std::string& cacheDir) : filepath(std::move(filepath)) {
|
||||||
|
// create a cache key based on the filepath
|
||||||
|
|
||||||
|
cachePath = cacheDir + "/epub_" + std::to_string(std::hash<std::string>{}(this->filepath));
|
||||||
|
}
|
||||||
|
~Epub() = default;
|
||||||
|
std::string& getBasePath() { return contentBasePath; }
|
||||||
|
bool load();
|
||||||
|
|
||||||
|
void clearCache() const;
|
||||||
|
|
||||||
|
void setupCacheDir() const;
|
||||||
|
|
||||||
|
const std::string& getCachePath() const;
|
||||||
|
const std::string& getPath() const;
|
||||||
|
const std::string& getTitle() const;
|
||||||
|
const std::string& getCoverImageItem() const;
|
||||||
|
uint8_t* getItemContents(const std::string& itemHref, size_t* size = nullptr) const;
|
||||||
|
char* getTextItemContents(const std::string& itemHref, size_t* size = nullptr) const;
|
||||||
|
|
||||||
|
std::string& getSpineItem(int spineIndex);
|
||||||
|
int getSpineItemsCount() const;
|
||||||
|
|
||||||
|
EpubTocEntry& getTocItem(int tocTndex);
|
||||||
|
int getTocItemsCount() const;
|
||||||
|
// work out the section index for a toc index
|
||||||
|
int getSpineIndexForTocIndex(int tocIndex) const;
|
||||||
|
|
||||||
|
int getTocIndexForSpineIndex(int spineIndex) const;
|
||||||
|
};
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
#include "EpubHtmlParser.h"
|
||||||
|
|
||||||
|
#include <EpdRenderer.h>
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
|
||||||
|
#include "Page.h"
|
||||||
|
#include "htmlEntities.h"
|
||||||
|
|
||||||
|
const char* HEADER_TAGS[] = {"h1", "h2", "h3", "h4", "h5", "h6"};
|
||||||
|
constexpr int NUM_HEADER_TAGS = sizeof(HEADER_TAGS) / sizeof(HEADER_TAGS[0]);
|
||||||
|
|
||||||
|
const char* BLOCK_TAGS[] = {"p", "li", "div", "br"};
|
||||||
|
constexpr int NUM_BLOCK_TAGS = sizeof(BLOCK_TAGS) / sizeof(BLOCK_TAGS[0]);
|
||||||
|
|
||||||
|
const char* BOLD_TAGS[] = {"b"};
|
||||||
|
constexpr int NUM_BOLD_TAGS = sizeof(BOLD_TAGS) / sizeof(BOLD_TAGS[0]);
|
||||||
|
|
||||||
|
const char* ITALIC_TAGS[] = {"i"};
|
||||||
|
constexpr int NUM_ITALIC_TAGS = sizeof(ITALIC_TAGS) / sizeof(ITALIC_TAGS[0]);
|
||||||
|
|
||||||
|
const char* IMAGE_TAGS[] = {"img"};
|
||||||
|
constexpr int NUM_IMAGE_TAGS = sizeof(IMAGE_TAGS) / sizeof(IMAGE_TAGS[0]);
|
||||||
|
|
||||||
|
const char* SKIP_TAGS[] = {"head", "table"};
|
||||||
|
constexpr int NUM_SKIP_TAGS = sizeof(SKIP_TAGS) / sizeof(SKIP_TAGS[0]);
|
||||||
|
|
||||||
|
// given the start and end of a tag, check to see if it matches a known tag
|
||||||
|
bool matches(const char* tag_name, const char* possible_tags[], const int possible_tag_count) {
|
||||||
|
for (int i = 0; i < possible_tag_count; i++) {
|
||||||
|
if (strcmp(tag_name, possible_tags[i]) == 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start a new text block if needed
|
||||||
|
void EpubHtmlParser::startNewTextBlock(const BLOCK_STYLE style) {
|
||||||
|
if (currentTextBlock) {
|
||||||
|
// already have a text block running and it is empty - just reuse it
|
||||||
|
if (currentTextBlock->isEmpty()) {
|
||||||
|
currentTextBlock->set_style(style);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentTextBlock->finish();
|
||||||
|
makePages();
|
||||||
|
delete currentTextBlock;
|
||||||
|
}
|
||||||
|
currentTextBlock = new TextBlock(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EpubHtmlParser::VisitEnter(const tinyxml2::XMLElement& element, const tinyxml2::XMLAttribute* firstAttribute) {
|
||||||
|
const char* tag_name = element.Name();
|
||||||
|
if (matches(tag_name, IMAGE_TAGS, NUM_IMAGE_TAGS)) {
|
||||||
|
const char* src = element.Attribute("src");
|
||||||
|
if (src) {
|
||||||
|
// don't leave an empty text block in the list
|
||||||
|
// const BLOCK_STYLE style = currentTextBlock->get_style();
|
||||||
|
if (currentTextBlock->isEmpty()) {
|
||||||
|
delete currentTextBlock;
|
||||||
|
currentTextBlock = nullptr;
|
||||||
|
}
|
||||||
|
// TODO: Fix this
|
||||||
|
// blocks.push_back(new ImageBlock(m_base_path + src));
|
||||||
|
// start a new text block - with the same style as before
|
||||||
|
// startNewTextBlock(style);
|
||||||
|
} else {
|
||||||
|
// ESP_LOGE(TAG, "Could not find src attribute");
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches(tag_name, SKIP_TAGS, NUM_SKIP_TAGS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serial.printf("Text: %s\n", element.GetText());
|
||||||
|
|
||||||
|
if (matches(tag_name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||||
|
insideBoldTag = true;
|
||||||
|
startNewTextBlock(CENTER_ALIGN);
|
||||||
|
} else if (matches(tag_name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||||
|
if (strcmp(tag_name, "br") == 0) {
|
||||||
|
startNewTextBlock(currentTextBlock->get_style());
|
||||||
|
} else {
|
||||||
|
startNewTextBlock(JUSTIFIED);
|
||||||
|
}
|
||||||
|
} else if (matches(tag_name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||||
|
insideBoldTag = true;
|
||||||
|
} else if (matches(tag_name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||||
|
insideItalicTag = true;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
/// Visit a text node.
|
||||||
|
bool EpubHtmlParser::Visit(const tinyxml2::XMLText& text) {
|
||||||
|
const char* content = text.Value();
|
||||||
|
currentTextBlock->addSpan(replaceHtmlEntities(content), insideBoldTag, insideItalicTag);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EpubHtmlParser::VisitExit(const tinyxml2::XMLElement& element) {
|
||||||
|
const char* tag_name = element.Name();
|
||||||
|
if (matches(tag_name, HEADER_TAGS, NUM_HEADER_TAGS)) {
|
||||||
|
insideBoldTag = false;
|
||||||
|
} else if (matches(tag_name, BLOCK_TAGS, NUM_BLOCK_TAGS)) {
|
||||||
|
// nothing to do
|
||||||
|
} else if (matches(tag_name, BOLD_TAGS, NUM_BOLD_TAGS)) {
|
||||||
|
insideBoldTag = false;
|
||||||
|
} else if (matches(tag_name, ITALIC_TAGS, NUM_ITALIC_TAGS)) {
|
||||||
|
insideItalicTag = false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EpubHtmlParser::parseAndBuildPages() {
|
||||||
|
startNewTextBlock(JUSTIFIED);
|
||||||
|
tinyxml2::XMLDocument doc(false, tinyxml2::COLLAPSE_WHITESPACE);
|
||||||
|
|
||||||
|
const tinyxml2::XMLError result = doc.LoadFile(filepath);
|
||||||
|
if (result != tinyxml2::XML_SUCCESS) {
|
||||||
|
Serial.printf("Failed to load file, Error: %s\n", tinyxml2::XMLDocument::ErrorIDToName(result));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.Accept(this);
|
||||||
|
if (currentTextBlock) {
|
||||||
|
makePages();
|
||||||
|
completePageFn(currentPage);
|
||||||
|
currentPage = nullptr;
|
||||||
|
delete currentTextBlock;
|
||||||
|
currentTextBlock = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubHtmlParser::makePages() {
|
||||||
|
if (!currentTextBlock) {
|
||||||
|
Serial.println("!! No text block to make pages for !!");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentPage) {
|
||||||
|
currentPage = new Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
const int lineHeight = renderer->getLineHeight();
|
||||||
|
const int pageHeight = renderer->getPageHeight();
|
||||||
|
|
||||||
|
// Long running task, make sure to let other things happen
|
||||||
|
vTaskDelay(1);
|
||||||
|
|
||||||
|
if (currentTextBlock->getType() == TEXT_BLOCK) {
|
||||||
|
const auto lines = currentTextBlock->splitIntoLines(renderer);
|
||||||
|
|
||||||
|
for (const auto line : lines) {
|
||||||
|
if (currentPage->nextY + lineHeight > pageHeight) {
|
||||||
|
completePageFn(currentPage);
|
||||||
|
currentPage = new Page();
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPage->elements.push_back(new PageLine(line, currentPage->nextY));
|
||||||
|
currentPage->nextY += lineHeight;
|
||||||
|
}
|
||||||
|
// TODO: Fix spacing between paras
|
||||||
|
// add some extra line between blocks
|
||||||
|
currentPage->nextY += lineHeight / 2;
|
||||||
|
}
|
||||||
|
// TODO: Image block support
|
||||||
|
// if (block->getType() == BlockType::IMAGE_BLOCK) {
|
||||||
|
// ImageBlock *imageBlock = (ImageBlock *)block;
|
||||||
|
// if (y + imageBlock->height > page_height) {
|
||||||
|
// pages.push_back(new Page());
|
||||||
|
// y = 0;
|
||||||
|
// }
|
||||||
|
// pages.back()->elements.push_back(new PageImage(imageBlock, y));
|
||||||
|
// y += imageBlock->height;
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <tinyxml2.h>
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "blocks/TextBlock.h"
|
||||||
|
|
||||||
|
class Page;
|
||||||
|
class EpdRenderer;
|
||||||
|
|
||||||
|
class EpubHtmlParser final : public tinyxml2::XMLVisitor {
|
||||||
|
const char* filepath;
|
||||||
|
EpdRenderer* renderer;
|
||||||
|
std::function<void(Page*)> completePageFn;
|
||||||
|
|
||||||
|
bool insideBoldTag = false;
|
||||||
|
bool insideItalicTag = false;
|
||||||
|
TextBlock* currentTextBlock = nullptr;
|
||||||
|
Page* currentPage = nullptr;
|
||||||
|
|
||||||
|
void startNewTextBlock(BLOCK_STYLE style);
|
||||||
|
void makePages();
|
||||||
|
|
||||||
|
// xml parser callbacks
|
||||||
|
bool VisitEnter(const tinyxml2::XMLElement& element, const tinyxml2::XMLAttribute* firstAttribute) override;
|
||||||
|
bool Visit(const tinyxml2::XMLText& text) override;
|
||||||
|
bool VisitExit(const tinyxml2::XMLElement& element) override;
|
||||||
|
// xml parser callbacks
|
||||||
|
public:
|
||||||
|
explicit EpubHtmlParser(const char* filepath, EpdRenderer* renderer, const std::function<void(Page*)>& completePageFn)
|
||||||
|
: filepath(filepath), renderer(renderer), completePageFn(completePageFn) {}
|
||||||
|
~EpubHtmlParser() override = default;
|
||||||
|
bool parseAndBuildPages();
|
||||||
|
};
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#include "Page.h"
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
void PageLine::render(EpdRenderer* renderer) { block->render(renderer, 0, yPos); }
|
||||||
|
|
||||||
|
void PageLine::serialize(std::ostream& os) {
|
||||||
|
serialization::writePod(os, yPos);
|
||||||
|
|
||||||
|
// serialize TextBlock pointed to by PageLine
|
||||||
|
block->serialize(os);
|
||||||
|
}
|
||||||
|
|
||||||
|
PageLine* PageLine::deserialize(std::istream& is) {
|
||||||
|
int32_t yPos;
|
||||||
|
serialization::readPod(is, yPos);
|
||||||
|
|
||||||
|
const auto tb = TextBlock::deserialize(is);
|
||||||
|
return new PageLine(tb, yPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Page::render(EpdRenderer* renderer) const {
|
||||||
|
const auto start = millis();
|
||||||
|
for (const auto element : elements) {
|
||||||
|
element->render(renderer);
|
||||||
|
}
|
||||||
|
Serial.printf("Rendered page elements (%u) in %dms\n", elements.size(), millis() - start);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Page::serialize(std::ostream& os) const {
|
||||||
|
serialization::writePod(os, nextY);
|
||||||
|
|
||||||
|
const uint32_t count = elements.size();
|
||||||
|
serialization::writePod(os, count);
|
||||||
|
|
||||||
|
for (auto* el : elements) {
|
||||||
|
// Only PageLine exists currently
|
||||||
|
serialization::writePod(os, static_cast<uint8_t>(TAG_PageLine));
|
||||||
|
static_cast<PageLine*>(el)->serialize(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Page* Page::deserialize(std::istream& is) {
|
||||||
|
auto* page = new Page();
|
||||||
|
|
||||||
|
serialization::readPod(is, page->nextY);
|
||||||
|
|
||||||
|
uint32_t count;
|
||||||
|
serialization::readPod(is, count);
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < count; i++) {
|
||||||
|
uint8_t tag;
|
||||||
|
serialization::readPod(is, tag);
|
||||||
|
|
||||||
|
if (tag == TAG_PageLine) {
|
||||||
|
auto* pl = PageLine::deserialize(is);
|
||||||
|
page->elements.push_back(pl);
|
||||||
|
} else {
|
||||||
|
throw std::runtime_error("Unknown PageElement tag");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return page;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "blocks/TextBlock.h"
|
||||||
|
|
||||||
|
enum PageElementTag : uint8_t {
|
||||||
|
TAG_PageLine = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// represents something that has been added to a page
|
||||||
|
class PageElement {
|
||||||
|
public:
|
||||||
|
int yPos;
|
||||||
|
explicit PageElement(const int yPos) : yPos(yPos) {}
|
||||||
|
virtual ~PageElement() = default;
|
||||||
|
virtual void render(EpdRenderer* renderer) = 0;
|
||||||
|
virtual void serialize(std::ostream& os) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// a line from a block element
|
||||||
|
class PageLine final : public PageElement {
|
||||||
|
const TextBlock* block;
|
||||||
|
|
||||||
|
public:
|
||||||
|
PageLine(const TextBlock* block, const int yPos) : PageElement(yPos), block(block) {}
|
||||||
|
~PageLine() override { delete block; }
|
||||||
|
void render(EpdRenderer* renderer) override;
|
||||||
|
void serialize(std::ostream& os) override;
|
||||||
|
static PageLine* deserialize(std::istream& is);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Page {
|
||||||
|
public:
|
||||||
|
int nextY = 0;
|
||||||
|
// the list of block index and line numbers on this page
|
||||||
|
std::vector<PageElement*> elements;
|
||||||
|
void render(EpdRenderer* renderer) const;
|
||||||
|
~Page() {
|
||||||
|
for (const auto element : elements) {
|
||||||
|
delete element;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
void serialize(std::ostream& os) const;
|
||||||
|
static Page* deserialize(std::istream& is);
|
||||||
|
};
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#include "Section.h"
|
||||||
|
|
||||||
|
#include <EpdRenderer.h>
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
#include "EpubHtmlParser.h"
|
||||||
|
#include "Page.h"
|
||||||
|
|
||||||
|
void Section::onPageComplete(const Page* page) {
|
||||||
|
Serial.printf("Page %d complete\n", pageCount);
|
||||||
|
|
||||||
|
const auto filePath = cachePath + "/page_" + std::to_string(pageCount) + ".bin";
|
||||||
|
// TODO can this be removed?
|
||||||
|
SD.open(filePath.c_str(), FILE_WRITE).close();
|
||||||
|
|
||||||
|
std::ofstream outputFile("/sd" + filePath);
|
||||||
|
page->serialize(outputFile);
|
||||||
|
outputFile.close();
|
||||||
|
|
||||||
|
pageCount++;
|
||||||
|
delete page;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Section::hasCache() {
|
||||||
|
if (!SD.exists(cachePath.c_str())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto sectionFilePath = cachePath + "/section.bin";
|
||||||
|
if (!SD.exists(sectionFilePath.c_str())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File sectionFile = SD.open(sectionFilePath.c_str(), FILE_READ);
|
||||||
|
uint8_t pageCountBytes[2] = {0, 0};
|
||||||
|
sectionFile.read(pageCountBytes, 2);
|
||||||
|
sectionFile.close();
|
||||||
|
|
||||||
|
pageCount = pageCountBytes[0] + (pageCountBytes[1] << 8);
|
||||||
|
Serial.printf("Loaded cache: %d pages\n", pageCount);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Section::setupCacheDir() const {
|
||||||
|
epub->setupCacheDir();
|
||||||
|
SD.mkdir(cachePath.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Section::clearCache() const { SD.rmdir(cachePath.c_str()); }
|
||||||
|
|
||||||
|
bool Section::persistPageDataToSD() {
|
||||||
|
size_t size = 0;
|
||||||
|
auto localPath = epub->getSpineItem(spineIndex);
|
||||||
|
|
||||||
|
const auto html = epub->getItemContents(epub->getSpineItem(spineIndex), &size);
|
||||||
|
if (!html) {
|
||||||
|
Serial.println("Failed to read item contents");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Would love to stream this through an XML visitor
|
||||||
|
const auto tmpHtmlPath = epub->getCachePath() + "/.tmp_" + std::to_string(spineIndex) + ".html";
|
||||||
|
File f = SD.open(tmpHtmlPath.c_str(), FILE_WRITE);
|
||||||
|
const auto written = f.write(html, size);
|
||||||
|
f.close();
|
||||||
|
free(html);
|
||||||
|
|
||||||
|
Serial.printf("Wrote %d bytes to %s\n", written, tmpHtmlPath.c_str());
|
||||||
|
|
||||||
|
if (size != written) {
|
||||||
|
Serial.println("Failed to inflate section contents to SD");
|
||||||
|
SD.remove(tmpHtmlPath.c_str());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto sdTmpHtmlPath = "/sd" + tmpHtmlPath;
|
||||||
|
auto visitor =
|
||||||
|
EpubHtmlParser(sdTmpHtmlPath.c_str(), renderer, [this](const Page* page) { this->onPageComplete(page); });
|
||||||
|
|
||||||
|
// TODO: Come back and see if mem used here can be lowered?
|
||||||
|
const bool success = visitor.parseAndBuildPages();
|
||||||
|
SD.remove(tmpHtmlPath.c_str());
|
||||||
|
if (!success) {
|
||||||
|
Serial.println("Failed to parse and build pages");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
File sectionFile = SD.open((cachePath + "/section.bin").c_str(), FILE_WRITE, true);
|
||||||
|
const uint8_t pageCountBytes[2] = {static_cast<uint8_t>(pageCount & 0xFF),
|
||||||
|
static_cast<uint8_t>((pageCount >> 8) & 0xFF)};
|
||||||
|
sectionFile.write(pageCountBytes, 2);
|
||||||
|
sectionFile.close();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Section::renderPage() {
|
||||||
|
if (0 <= currentPage && currentPage < pageCount) {
|
||||||
|
const auto filePath = "/sd" + cachePath + "/page_" + std::to_string(currentPage) + ".bin";
|
||||||
|
std::ifstream inputFile(filePath);
|
||||||
|
const Page* p = Page::deserialize(inputFile);
|
||||||
|
inputFile.close();
|
||||||
|
p->render(renderer);
|
||||||
|
delete p;
|
||||||
|
} else if (pageCount == 0) {
|
||||||
|
Serial.println("No pages to render");
|
||||||
|
const int width = renderer->getTextWidth("Empty chapter", true);
|
||||||
|
renderer->drawText((renderer->getPageWidth() - width) / 2, 300, "Empty chapter", true);
|
||||||
|
} else {
|
||||||
|
Serial.printf("Page out of bounds: %d (max %d)\n", currentPage, pageCount);
|
||||||
|
const int width = renderer->getTextWidth("Out of bounds", true);
|
||||||
|
renderer->drawText((renderer->getPageWidth() - width) / 2, 300, "Out of bounds", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "Epub.h"
|
||||||
|
|
||||||
|
class Page;
|
||||||
|
class EpdRenderer;
|
||||||
|
|
||||||
|
class Section {
|
||||||
|
Epub* epub;
|
||||||
|
const int spineIndex;
|
||||||
|
EpdRenderer* renderer;
|
||||||
|
std::string cachePath;
|
||||||
|
|
||||||
|
void onPageComplete(const Page* page);
|
||||||
|
|
||||||
|
public:
|
||||||
|
int pageCount = 0;
|
||||||
|
int currentPage = 0;
|
||||||
|
|
||||||
|
explicit Section(Epub* epub, const int spineIndex, EpdRenderer* renderer)
|
||||||
|
: epub(epub), spineIndex(spineIndex), renderer(renderer) {
|
||||||
|
cachePath = epub->getCachePath() + "/" + std::to_string(spineIndex);
|
||||||
|
}
|
||||||
|
~Section() = default;
|
||||||
|
bool hasCache();
|
||||||
|
void setupCacheDir() const;
|
||||||
|
void clearCache() const;
|
||||||
|
bool persistPageDataToSD();
|
||||||
|
void renderPage();
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
class EpdRenderer;
|
||||||
|
|
||||||
|
typedef enum { TEXT_BLOCK, IMAGE_BLOCK } BlockType;
|
||||||
|
|
||||||
|
// a block of content in the html - either a paragraph or an image
|
||||||
|
class Block {
|
||||||
|
public:
|
||||||
|
virtual ~Block() = default;
|
||||||
|
virtual void layout(EpdRenderer* renderer) = 0;
|
||||||
|
virtual BlockType getType() = 0;
|
||||||
|
virtual bool isEmpty() = 0;
|
||||||
|
virtual void finish() {}
|
||||||
|
};
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
#include "TextBlock.h"
|
||||||
|
|
||||||
|
#include <EpdRenderer.h>
|
||||||
|
#include <Serialization.h>
|
||||||
|
|
||||||
|
static bool isWhitespace(const char c) { return c == ' ' || c == '\r' || c == '\n'; }
|
||||||
|
|
||||||
|
// move past anything that should be considered part of a work
|
||||||
|
static int skipWord(const std::string& text, int index, const int length) {
|
||||||
|
while (index < length && !isWhitespace(text[index])) {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip past any white space characters
|
||||||
|
static int skipWhitespace(const std::string& html, int index, const int length) {
|
||||||
|
while (index < length && isWhitespace(html[index])) {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBlock::addSpan(const std::string& span, const bool is_bold, const bool is_italic) {
|
||||||
|
// adding a span to text block
|
||||||
|
// make a copy of the text as we'll modify it
|
||||||
|
const int length = span.length();
|
||||||
|
// const auto text = new char[length + 1];
|
||||||
|
// strcpy(text, span);
|
||||||
|
// work out where each word is in the span
|
||||||
|
int index = 0;
|
||||||
|
while (index < length) {
|
||||||
|
// skip past any whitespace to the start of a word
|
||||||
|
index = skipWhitespace(span, index, length);
|
||||||
|
const int wordStart = index;
|
||||||
|
// find the end of the word
|
||||||
|
index = skipWord(span, index, length);
|
||||||
|
const int wordLength = index - wordStart;
|
||||||
|
if (wordLength > 0) {
|
||||||
|
words.push_back(span.substr(wordStart, wordLength));
|
||||||
|
wordStyles.push_back((is_bold ? BOLD_SPAN : 0) | (is_italic ? ITALIC_SPAN : 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::list<TextBlock*> TextBlock::splitIntoLines(const EpdRenderer* renderer) {
|
||||||
|
const int totalWordCount = words.size();
|
||||||
|
const int pageWidth = renderer->getPageWidth();
|
||||||
|
const int spaceWidth = renderer->getSpaceWidth();
|
||||||
|
|
||||||
|
words.shrink_to_fit();
|
||||||
|
wordStyles.shrink_to_fit();
|
||||||
|
wordXpos.reserve(totalWordCount);
|
||||||
|
|
||||||
|
// measure each word
|
||||||
|
uint16_t wordWidths[totalWordCount];
|
||||||
|
for (int i = 0; i < words.size(); i++) {
|
||||||
|
// measure the word
|
||||||
|
const int width = renderer->getTextWidth(words[i].c_str(), wordStyles[i] & BOLD_SPAN, wordStyles[i] & ITALIC_SPAN);
|
||||||
|
wordWidths[i] = width;
|
||||||
|
}
|
||||||
|
|
||||||
|
// now apply the dynamic programming algorithm to find the best line breaks
|
||||||
|
// DP table in which dp[i] represents cost of line starting with word words[i]
|
||||||
|
int dp[totalWordCount];
|
||||||
|
|
||||||
|
// Array in which ans[i] store index of last word in line starting with word
|
||||||
|
// word[i]
|
||||||
|
size_t ans[totalWordCount];
|
||||||
|
|
||||||
|
// If only one word is present then only one line is required. Cost of last
|
||||||
|
// line is zero. Hence cost of this line is zero. Ending point is also n-1 as
|
||||||
|
// single word is present
|
||||||
|
dp[totalWordCount - 1] = 0;
|
||||||
|
ans[totalWordCount - 1] = totalWordCount - 1;
|
||||||
|
|
||||||
|
// Make each word first word of line by iterating over each index in arr.
|
||||||
|
for (int i = totalWordCount - 2; i >= 0; i--) {
|
||||||
|
int currlen = -1;
|
||||||
|
dp[i] = INT_MAX;
|
||||||
|
|
||||||
|
// Variable to store possible minimum cost of line.
|
||||||
|
int cost;
|
||||||
|
|
||||||
|
// Keep on adding words in current line by iterating from starting word upto
|
||||||
|
// last word in arr.
|
||||||
|
for (int j = i; j < totalWordCount; j++) {
|
||||||
|
// Update the width of the words in current line + the space between two
|
||||||
|
// words.
|
||||||
|
currlen += wordWidths[j] + spaceWidth;
|
||||||
|
|
||||||
|
// If we're bigger than the current pagewidth then we can't add more words
|
||||||
|
if (currlen > pageWidth) break;
|
||||||
|
|
||||||
|
// if we've run out of words then this is last line and the cost should be
|
||||||
|
// 0 Otherwise the cost is the sqaure of the left over space + the costs
|
||||||
|
// of all the previous lines
|
||||||
|
if (j == totalWordCount - 1)
|
||||||
|
cost = 0;
|
||||||
|
else
|
||||||
|
cost = (pageWidth - currlen) * (pageWidth - currlen) + dp[j + 1];
|
||||||
|
|
||||||
|
// Check if this arrangement gives minimum cost for line starting with
|
||||||
|
// word words[i].
|
||||||
|
if (cost < dp[i]) {
|
||||||
|
dp[i] = cost;
|
||||||
|
ans[i] = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can now iterate through the answer to find the line break positions
|
||||||
|
std::list<uint16_t> lineBreaks;
|
||||||
|
for (size_t i = 0; i < totalWordCount;) {
|
||||||
|
i = ans[i] + 1;
|
||||||
|
if (i > totalWordCount) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
lineBreaks.push_back(i);
|
||||||
|
// Text too big, just exit
|
||||||
|
if (lineBreaks.size() > 1000) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::list<TextBlock*> lines;
|
||||||
|
|
||||||
|
// With the line breaks calculated we can now position the words along the
|
||||||
|
// line
|
||||||
|
int startWord = 0;
|
||||||
|
for (const auto lineBreak : lineBreaks) {
|
||||||
|
const int lineWordCount = lineBreak - startWord;
|
||||||
|
|
||||||
|
int lineWordWidthSum = 0;
|
||||||
|
for (int i = startWord; i < lineBreak; i++) {
|
||||||
|
lineWordWidthSum += wordWidths[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate spacing between words
|
||||||
|
const uint16_t spareSpace = pageWidth - lineWordWidthSum;
|
||||||
|
uint16_t spacing = spaceWidth;
|
||||||
|
// evenly space words if using justified style, not the last line, and at
|
||||||
|
// least 2 words
|
||||||
|
if (style == JUSTIFIED && lineBreak != lineBreaks.back() && lineWordCount >= 2) {
|
||||||
|
spacing = spareSpace / (lineWordCount - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t xpos = 0;
|
||||||
|
if (style == RIGHT_ALIGN) {
|
||||||
|
xpos = spareSpace - (lineWordCount - 1) * spaceWidth;
|
||||||
|
} else if (style == CENTER_ALIGN) {
|
||||||
|
xpos = (spareSpace - (lineWordCount - 1) * spaceWidth) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = startWord; i < lineBreak; i++) {
|
||||||
|
wordXpos[i] = xpos;
|
||||||
|
xpos += wordWidths[i] + spacing;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> lineWords;
|
||||||
|
std::vector<uint16_t> lineXPos;
|
||||||
|
std::vector<uint8_t> lineWordStyles;
|
||||||
|
lineWords.reserve(lineWordCount);
|
||||||
|
lineXPos.reserve(lineWordCount);
|
||||||
|
lineWordStyles.reserve(lineWordCount);
|
||||||
|
|
||||||
|
for (int i = startWord; i < lineBreak; i++) {
|
||||||
|
lineWords.push_back(words[i]);
|
||||||
|
lineXPos.push_back(wordXpos[i]);
|
||||||
|
lineWordStyles.push_back(wordStyles[i]);
|
||||||
|
}
|
||||||
|
const auto textLine = new TextBlock(lineWords, lineXPos, lineWordStyles, style);
|
||||||
|
lines.push_back(textLine);
|
||||||
|
startWord = lineBreak;
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBlock::render(const EpdRenderer* renderer, const int x, const int y) const {
|
||||||
|
for (int i = 0; i < words.size(); i++) {
|
||||||
|
// get the style
|
||||||
|
const uint8_t wordStyle = wordStyles[i];
|
||||||
|
// render the word
|
||||||
|
renderer->drawText(x + wordXpos[i], y, words[i].c_str(), wordStyle & BOLD_SPAN, wordStyle & ITALIC_SPAN);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TextBlock::serialize(std::ostream& os) const {
|
||||||
|
// words
|
||||||
|
const uint32_t wc = words.size();
|
||||||
|
serialization::writePod(os, wc);
|
||||||
|
for (const auto& w : words) serialization::writeString(os, w);
|
||||||
|
|
||||||
|
// wordXpos
|
||||||
|
const uint32_t xc = wordXpos.size();
|
||||||
|
serialization::writePod(os, xc);
|
||||||
|
for (auto x : wordXpos) serialization::writePod(os, x);
|
||||||
|
|
||||||
|
// wordStyles
|
||||||
|
const uint32_t sc = wordStyles.size();
|
||||||
|
serialization::writePod(os, sc);
|
||||||
|
for (auto s : wordStyles) serialization::writePod(os, s);
|
||||||
|
|
||||||
|
// style
|
||||||
|
serialization::writePod(os, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
TextBlock* TextBlock::deserialize(std::istream& is) {
|
||||||
|
uint32_t wc, xc, sc;
|
||||||
|
std::vector<std::string> words;
|
||||||
|
std::vector<uint16_t> wordXpos;
|
||||||
|
std::vector<uint8_t> wordStyles;
|
||||||
|
BLOCK_STYLE style;
|
||||||
|
|
||||||
|
// words
|
||||||
|
serialization::readPod(is, wc);
|
||||||
|
words.resize(wc);
|
||||||
|
for (auto& w : words) serialization::readString(is, w);
|
||||||
|
|
||||||
|
// wordXpos
|
||||||
|
serialization::readPod(is, xc);
|
||||||
|
wordXpos.resize(xc);
|
||||||
|
for (auto& x : wordXpos) serialization::readPod(is, x);
|
||||||
|
|
||||||
|
// wordStyles
|
||||||
|
serialization::readPod(is, sc);
|
||||||
|
wordStyles.resize(sc);
|
||||||
|
for (auto& s : wordStyles) serialization::readPod(is, s);
|
||||||
|
|
||||||
|
// style
|
||||||
|
serialization::readPod(is, style);
|
||||||
|
|
||||||
|
return new TextBlock(words, wordXpos, wordStyles, style);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <list>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Block.h"
|
||||||
|
|
||||||
|
enum SPAN_STYLE : uint8_t {
|
||||||
|
BOLD_SPAN = 1,
|
||||||
|
ITALIC_SPAN = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum BLOCK_STYLE : uint8_t {
|
||||||
|
JUSTIFIED = 0,
|
||||||
|
LEFT_ALIGN = 1,
|
||||||
|
CENTER_ALIGN = 2,
|
||||||
|
RIGHT_ALIGN = 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// represents a block of words in the html document
|
||||||
|
class TextBlock final : public Block {
|
||||||
|
// pointer to each word
|
||||||
|
std::vector<std::string> words;
|
||||||
|
// x position of each word
|
||||||
|
std::vector<uint16_t> wordXpos;
|
||||||
|
// the styles of each word
|
||||||
|
std::vector<uint8_t> wordStyles;
|
||||||
|
|
||||||
|
// the style of the block - left, center, right aligned
|
||||||
|
BLOCK_STYLE style;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void addSpan(const std::string& span, bool is_bold, bool is_italic);
|
||||||
|
explicit TextBlock(const BLOCK_STYLE style) : style(style) {}
|
||||||
|
explicit TextBlock(const std::vector<std::string>& words, const std::vector<uint16_t>& word_xpos,
|
||||||
|
// the styles of each word
|
||||||
|
const std::vector<uint8_t>& word_styles, const BLOCK_STYLE style)
|
||||||
|
: words(words), wordXpos(word_xpos), wordStyles(word_styles), style(style) {}
|
||||||
|
~TextBlock() override = default;
|
||||||
|
void set_style(const BLOCK_STYLE style) { this->style = style; }
|
||||||
|
BLOCK_STYLE get_style() const { return style; }
|
||||||
|
bool isEmpty() override { return words.empty(); }
|
||||||
|
void layout(EpdRenderer* renderer) override {};
|
||||||
|
// given a renderer works out where to break the words into lines
|
||||||
|
std::list<TextBlock*> splitIntoLines(const EpdRenderer* renderer);
|
||||||
|
void render(const EpdRenderer* renderer, int x, int y) const;
|
||||||
|
BlockType getType() override { return TEXT_BLOCK; }
|
||||||
|
void serialize(std::ostream& os) const;
|
||||||
|
static TextBlock* deserialize(std::istream& is);
|
||||||
|
};
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
// from
|
||||||
|
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||||
|
|
||||||
|
#include "htmlEntities.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
const int MAX_ENTITY_LENGTH = 10;
|
||||||
|
|
||||||
|
// Use book: entities_ww2.epub to test this (Page 7: Entities parser test)
|
||||||
|
// Note the supported keys are only in lowercase
|
||||||
|
// Store the mappings in a unordered hash map
|
||||||
|
static std::unordered_map<std::string, std::string> entity_lookup(
|
||||||
|
{{""", "\""}, {"⁄", "⁄"}, {"&", "&"}, {"<", "<"}, {">", ">"},
|
||||||
|
{"À", "À"}, {"Á", "Á"}, {"Â", "Â"}, {"Ã", "Ã"}, {"Ä", "Ä"},
|
||||||
|
{"Å", "Å"}, {"Æ", "Æ"}, {"Ç", "Ç"}, {"È", "È"}, {"É", "É"},
|
||||||
|
{"Ê", "Ê"}, {"Ë", "Ë"}, {"Ì", "Ì"}, {"Í", "Í"}, {"Î", "Î"},
|
||||||
|
{"Ï", "Ï"}, {"Ð", "Ð"}, {"Ñ", "Ñ"}, {"Ò", "Ò"}, {"Ó", "Ó"},
|
||||||
|
{"Ô", "Ô"}, {"Õ", "Õ"}, {"Ö", "Ö"}, {"Ø", "Ø"}, {"Ù", "Ù"},
|
||||||
|
{"Ú", "Ú"}, {"Û", "Û"}, {"Ü", "Ü"}, {"Ý", "Ý"}, {"Þ", "Þ"},
|
||||||
|
{"ß", "ß"}, {"à", "à"}, {"á", "á"}, {"â", "â"}, {"ã", "ã"},
|
||||||
|
{"ä", "ä"}, {"å", "å"}, {"æ", "æ"}, {"ç", "ç"}, {"è", "è"},
|
||||||
|
{"é", "é"}, {"ê", "ê"}, {"ë", "ë"}, {"ì", "ì"}, {"í", "í"},
|
||||||
|
{"î", "î"}, {"ï", "ï"}, {"ð", "ð"}, {"ñ", "ñ"}, {"ò", "ò"},
|
||||||
|
{"ó", "ó"}, {"ô", "ô"}, {"õ", "õ"}, {"ö", "ö"}, {"ø", "ø"},
|
||||||
|
{"ù", "ù"}, {"ú", "ú"}, {"û", "û"}, {"ü", "ü"}, {"ý", "ý"},
|
||||||
|
{"þ", "þ"}, {"ÿ", "ÿ"}, {" ", " "}, {"¡", "¡"}, {"¢", "¢"},
|
||||||
|
{"£", "£"}, {"¤", "¤"}, {"¥", "¥"}, {"¦", "¦"}, {"§", "§"},
|
||||||
|
{"¨", "¨"}, {"©", "©"}, {"ª", "ª"}, {"«", "«"}, {"¬", "¬"},
|
||||||
|
{"­", ""}, {"®", "®"}, {"¯", "¯"}, {"°", "°"}, {"±", "±"},
|
||||||
|
{"²", "²"}, {"³", "³"}, {"´", "´"}, {"µ", "µ"}, {"¶", "¶"},
|
||||||
|
{"¸", "¸"}, {"¹", "¹"}, {"º", "º"}, {"»", "»"}, {"¼", "¼"},
|
||||||
|
{"½", "½"}, {"¾", "¾"}, {"¿", "¿"}, {"×", "×"}, {"÷", "÷"},
|
||||||
|
{"∀", "∀"}, {"∂", "∂"}, {"∃", "∃"}, {"∅", "∅"}, {"∇", "∇"},
|
||||||
|
{"∈", "∈"}, {"∉", "∉"}, {"∋", "∋"}, {"∏", "∏"}, {"∑", "∑"},
|
||||||
|
{"−", "−"}, {"∗", "∗"}, {"√", "√"}, {"∝", "∝"}, {"∞", "∞"},
|
||||||
|
{"∠", "∠"}, {"∧", "∧"}, {"∨", "∨"}, {"∩", "∩"}, {"∪", "∪"},
|
||||||
|
{"∫", "∫"}, {"∴", "∴"}, {"∼", "∼"}, {"≅", "≅"}, {"≈", "≈"},
|
||||||
|
{"≠", "≠"}, {"≡", "≡"}, {"≤", "≤"}, {"≥", "≥"}, {"⊂", "⊂"},
|
||||||
|
{"⊃", "⊃"}, {"⊄", "⊄"}, {"⊆", "⊆"}, {"⊇", "⊇"}, {"⊕", "⊕"},
|
||||||
|
{"⊗", "⊗"}, {"⊥", "⊥"}, {"⋅", "⋅"}, {"Α", "Α"}, {"Β", "Β"},
|
||||||
|
{"Γ", "Γ"}, {"Δ", "Δ"}, {"Ε", "Ε"}, {"Ζ", "Ζ"}, {"Η", "Η"},
|
||||||
|
{"Θ", "Θ"}, {"Ι", "Ι"}, {"Κ", "Κ"}, {"Λ", "Λ"}, {"Μ", "Μ"},
|
||||||
|
{"Ν", "Ν"}, {"Ξ", "Ξ"}, {"Ο", "Ο"}, {"Π", "Π"}, {"Ρ", "Ρ"},
|
||||||
|
{"Σ", "Σ"}, {"Τ", "Τ"}, {"Υ", "Υ"}, {"Φ", "Φ"}, {"Χ", "Χ"},
|
||||||
|
{"Ψ", "Ψ"}, {"Ω", "Ω"}, {"α", "α"}, {"β", "β"}, {"γ", "γ"},
|
||||||
|
{"δ", "δ"}, {"ε", "ε"}, {"ζ", "ζ"}, {"η", "η"}, {"θ", "θ"},
|
||||||
|
{"ι", "ι"}, {"κ", "κ"}, {"λ", "λ"}, {"μ", "μ"}, {"ν", "ν"},
|
||||||
|
{"ξ", "ξ"}, {"ο", "ο"}, {"π", "π"}, {"ρ", "ρ"}, {"ς", "ς"},
|
||||||
|
{"σ", "σ"}, {"τ", "τ"}, {"υ", "υ"}, {"φ", "φ"}, {"χ", "χ"},
|
||||||
|
{"ψ", "ψ"}, {"ω", "ω"}, {"ϑ", "ϑ"}, {"ϒ", "ϒ"}, {"ϖ", "ϖ"},
|
||||||
|
{"Œ", "Œ"}, {"œ", "œ"}, {"Š", "Š"}, {"š", "š"}, {"Ÿ", "Ÿ"},
|
||||||
|
{"ƒ", "ƒ"}, {"ˆ", "ˆ"}, {"˜", "˜"}, {" ", ""}, {" ", ""},
|
||||||
|
{" ", ""}, {"‌", ""}, {"‍", ""}, {"‎", ""}, {"‏", ""},
|
||||||
|
{"–", "–"}, {"—", "—"}, {"‘", "‘"}, {"’", "’"}, {"‚", "‚"},
|
||||||
|
{"“", "“"}, {"”", "”"}, {"„", "„"}, {"†", "†"}, {"‡", "‡"},
|
||||||
|
{"•", "•"}, {"…", "…"}, {"‰", "‰"}, {"′", "′"}, {"″", "″"},
|
||||||
|
{"‹", "‹"}, {"›", "›"}, {"‾", "‾"}, {"€", "€"}, {"™", "™"},
|
||||||
|
{"←", "←"}, {"↑", "↑"}, {"→", "→"}, {"↓", "↓"}, {"↔", "↔"},
|
||||||
|
{"↵", "↵"}, {"⌈", "⌈"}, {"⌉", "⌉"}, {"⌊", "⌊"}, {"⌋", "⌋"},
|
||||||
|
{"◊", "◊"}, {"♠", "♠"}, {"♣", "♣"}, {"♥", "♥"}, {"♦", "♦"}});
|
||||||
|
|
||||||
|
// converts from a unicode code point to the utf8 equivalent
|
||||||
|
void convert_to_utf8(const int code, std::string& res) {
|
||||||
|
// convert to a utf8 sequence
|
||||||
|
if (code < 0x80) {
|
||||||
|
res += static_cast<char>(code);
|
||||||
|
} else if (code < 0x800) {
|
||||||
|
res += static_cast<char>(0xc0 | (code >> 6));
|
||||||
|
res += static_cast<char>(0x80 | (code & 0x3f));
|
||||||
|
} else if (code < 0x10000) {
|
||||||
|
res += static_cast<char>(0xe0 | (code >> 12));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
||||||
|
res += static_cast<char>(0x80 | (code & 0x3f));
|
||||||
|
} else if (code < 0x200000) {
|
||||||
|
res += static_cast<char>(0xf0 | (code >> 18));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 12) & 0x3f));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
||||||
|
res += static_cast<char>(0x80 | (code & 0x3f));
|
||||||
|
} else if (code < 0x4000000) {
|
||||||
|
res += static_cast<char>(0xf8 | (code >> 24));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 18) & 0x3f));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 12) & 0x3f));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
||||||
|
res += static_cast<char>(0x80 | (code & 0x3f));
|
||||||
|
} else if (code < 0x80000000) {
|
||||||
|
res += static_cast<char>(0xfc | (code >> 30));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 24) & 0x3f));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 18) & 0x3f));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 12) & 0x3f));
|
||||||
|
res += static_cast<char>(0x80 | ((code >> 6) & 0x3f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles numeric entities - e.g. Ӓ or ሴ
|
||||||
|
bool process_numeric_entity(const std::string& entity, std::string& res) {
|
||||||
|
int code = 0;
|
||||||
|
// is it hex?
|
||||||
|
if (entity[2] == 'x' || entity[2] == 'X') {
|
||||||
|
// parse the hex code
|
||||||
|
code = strtol(entity.substr(3, entity.size() - 3).c_str(), nullptr, 16);
|
||||||
|
} else {
|
||||||
|
code = strtol(entity.substr(2, entity.size() - 3).c_str(), nullptr, 10);
|
||||||
|
}
|
||||||
|
if (code != 0) {
|
||||||
|
// special handling for nbsp
|
||||||
|
if (code == 0xA0) {
|
||||||
|
res += " ";
|
||||||
|
} else {
|
||||||
|
convert_to_utf8(code, res);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles named entities - e.g. &
|
||||||
|
bool process_string_entity(const std::string& entity, std::string& res) {
|
||||||
|
// it's a named entity - find it in the lookup table
|
||||||
|
// find it in the map
|
||||||
|
const auto it = entity_lookup.find(entity);
|
||||||
|
if (it != entity_lookup.end()) {
|
||||||
|
res += it->second;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace all the entities in the string
|
||||||
|
std::string replaceHtmlEntities(const char* text) {
|
||||||
|
std::string res;
|
||||||
|
res.reserve(strlen(text));
|
||||||
|
for (int i = 0; i < strlen(text); ++i) {
|
||||||
|
bool flag = false;
|
||||||
|
// do we have a potential entity?
|
||||||
|
if (text[i] == '&') {
|
||||||
|
// find the end of the entity
|
||||||
|
int j = i + 1;
|
||||||
|
while (j < strlen(text) && text[j] != ';' && j - i < MAX_ENTITY_LENGTH) {
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
if (j - i > 2) {
|
||||||
|
char entity[j - i + 1];
|
||||||
|
strncpy(entity, text + i, j - i);
|
||||||
|
// is it a numeric code?
|
||||||
|
if (entity[1] == '#') {
|
||||||
|
flag = process_numeric_entity(entity, res);
|
||||||
|
} else {
|
||||||
|
flag = process_string_entity(entity, res);
|
||||||
|
}
|
||||||
|
// skip past the entity if we successfully decoded it
|
||||||
|
if (flag) {
|
||||||
|
i = j;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!flag) {
|
||||||
|
res += text[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// from
|
||||||
|
// https://github.com/atomic14/diy-esp32-epub-reader/blob/2c2f57fdd7e2a788d14a0bcb26b9e845a47aac42/lib/Epub/RubbishHtmlParser/htmlEntities.cpp
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
std::string replaceHtmlEntities(const char* text);
|
||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
|
||||||
|
This directory is intended for project specific (private) libraries.
|
||||||
|
PlatformIO will compile them to static libraries and link into the executable file.
|
||||||
|
|
||||||
|
The source code of each library should be placed in a separate directory
|
||||||
|
("lib/your_library_name/[Code]").
|
||||||
|
|
||||||
|
For example, see the structure of the following example libraries `Foo` and `Bar`:
|
||||||
|
|
||||||
|
|--lib
|
||||||
|
| |
|
||||||
|
| |--Bar
|
||||||
|
| | |--docs
|
||||||
|
| | |--examples
|
||||||
|
| | |--src
|
||||||
|
| | |- Bar.c
|
||||||
|
| | |- Bar.h
|
||||||
|
| | |- library.json (optional. for custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html
|
||||||
|
| |
|
||||||
|
| |--Foo
|
||||||
|
| | |- Foo.c
|
||||||
|
| | |- Foo.h
|
||||||
|
| |
|
||||||
|
| |- README --> THIS FILE
|
||||||
|
|
|
||||||
|
|- platformio.ini
|
||||||
|
|--src
|
||||||
|
|- main.c
|
||||||
|
|
||||||
|
Example contents of `src/main.c` using Foo and Bar:
|
||||||
|
```
|
||||||
|
#include <Foo.h>
|
||||||
|
#include <Bar.h>
|
||||||
|
|
||||||
|
int main (void)
|
||||||
|
{
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
The PlatformIO Library Dependency Finder will find automatically dependent
|
||||||
|
libraries by scanning project source files.
|
||||||
|
|
||||||
|
More information about PlatformIO Library Dependency Finder
|
||||||
|
- https://docs.platformio.org/page/librarymanager/ldf.html
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
namespace serialization {
|
||||||
|
template <typename T>
|
||||||
|
static void writePod(std::ostream& os, const T& value) {
|
||||||
|
os.write(reinterpret_cast<const char*>(&value), sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
template <typename T>
|
||||||
|
static void readPod(std::istream& is, T& value) {
|
||||||
|
is.read(reinterpret_cast<char*>(&value), sizeof(T));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void writeString(std::ostream& os, const std::string& s) {
|
||||||
|
const uint32_t len = s.size();
|
||||||
|
writePod(os, len);
|
||||||
|
os.write(s.data(), len);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void readString(std::istream& is, std::string& s) {
|
||||||
|
uint32_t len;
|
||||||
|
readPod(is, len);
|
||||||
|
s.resize(len);
|
||||||
|
is.read(&s[0], len);
|
||||||
|
}
|
||||||
|
} // namespace serialization
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#include "Utf8.h"
|
||||||
|
|
||||||
|
int utf8CodepointLen(const unsigned char c) {
|
||||||
|
if (c < 0x80) return 1; // 0xxxxxxx
|
||||||
|
if ((c >> 5) == 0x6) return 2; // 110xxxxx
|
||||||
|
if ((c >> 4) == 0xE) return 3; // 1110xxxx
|
||||||
|
if ((c >> 3) == 0x1E) return 4; // 11110xxx
|
||||||
|
return 1; // fallback for invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t utf8NextCodepoint(const unsigned char** string) {
|
||||||
|
if (**string == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int bytes = utf8CodepointLen(**string);
|
||||||
|
const uint8_t* chr = *string;
|
||||||
|
*string += bytes;
|
||||||
|
|
||||||
|
if (bytes == 1) {
|
||||||
|
return chr[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t cp = chr[0] & ((1 << (7 - bytes)) - 1); // mask header bits
|
||||||
|
|
||||||
|
for (int i = 1; i < bytes; i++) {
|
||||||
|
cp = (cp << 6) | (chr[i] & 0x3F);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cp;
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
uint32_t utf8NextCodepoint(const unsigned char** string);
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#include "ZipFile.h"
|
||||||
|
|
||||||
|
#include <HardwareSerial.h>
|
||||||
|
#include <miniz.h>
|
||||||
|
|
||||||
|
int libzInflateOneShot(const uint8_t* inputBuff, const size_t compSize, uint8_t* outputBuff, const size_t uncompSize) {
|
||||||
|
mz_stream pStream = {
|
||||||
|
.next_in = inputBuff,
|
||||||
|
.avail_in = compSize,
|
||||||
|
.total_in = 0,
|
||||||
|
.next_out = outputBuff,
|
||||||
|
.avail_out = uncompSize,
|
||||||
|
.total_out = 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
int status = 0;
|
||||||
|
status = mz_inflateInit2(&pStream, -MZ_DEFAULT_WINDOW_BITS);
|
||||||
|
|
||||||
|
if (status != MZ_OK) {
|
||||||
|
Serial.printf("inflateInit2 failed: %d\n", status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = mz_inflate(&pStream, MZ_FINISH);
|
||||||
|
if (status != MZ_STREAM_END) {
|
||||||
|
Serial.printf("inflate failed: %d\n", status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
status = mz_inflateEnd(&pStream);
|
||||||
|
if (status != MZ_OK) {
|
||||||
|
Serial.printf("inflateEnd failed: %d\n", status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
char* ZipFile::readTextFileToMemory(const char* filename, size_t* size) const {
|
||||||
|
const auto data = readFileToMemory(filename, size, true);
|
||||||
|
return data ? reinterpret_cast<char*>(data) : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* ZipFile::readFileToMemory(const char* filename, size_t* size, bool trailingNullByte) const {
|
||||||
|
mz_zip_archive zipArchive = {};
|
||||||
|
const bool status = mz_zip_reader_init_file(&zipArchive, filePath.c_str(), 0);
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
Serial.printf("mz_zip_reader_init_file() failed!\nError %s\n", mz_zip_get_error_string(zipArchive.m_last_error));
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// find the file
|
||||||
|
mz_uint32 fileIndex = 0;
|
||||||
|
if (!mz_zip_reader_locate_file_v2(&zipArchive, filename, nullptr, 0, &fileIndex)) {
|
||||||
|
Serial.printf("Could not find file %s\n", filename);
|
||||||
|
mz_zip_reader_end(&zipArchive);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
mz_zip_archive_file_stat fileStat;
|
||||||
|
if (!mz_zip_reader_file_stat(&zipArchive, fileIndex, &fileStat)) {
|
||||||
|
Serial.printf("mz_zip_reader_file_stat() failed!\nError %s\n", mz_zip_get_error_string(zipArchive.m_last_error));
|
||||||
|
mz_zip_reader_end(&zipArchive);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
mz_zip_reader_end(&zipArchive);
|
||||||
|
|
||||||
|
uint8_t pLocalHeader[30];
|
||||||
|
uint64_t fileOffset = fileStat.m_local_header_ofs;
|
||||||
|
|
||||||
|
// Reopen the file to manual read out delated bytes
|
||||||
|
FILE* file = fopen(filePath.c_str(), "rb");
|
||||||
|
fseek(file, fileOffset, SEEK_SET);
|
||||||
|
|
||||||
|
const size_t read = fread(pLocalHeader, 1, 30, file);
|
||||||
|
if (read != 30) {
|
||||||
|
Serial.println("Something went wrong reading the local header");
|
||||||
|
fclose(file);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pLocalHeader[0] + (pLocalHeader[1] << 8) + (pLocalHeader[2] << 16) + (pLocalHeader[3] << 24) !=
|
||||||
|
0x04034b50 /* MZ_ZIP_LOCAL_DIR_HEADER_SIG */) {
|
||||||
|
Serial.println("Not a valid zip file header");
|
||||||
|
fclose(file);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint16_t filenameLength = pLocalHeader[26] + (pLocalHeader[27] << 8);
|
||||||
|
const uint16_t extraOffset = pLocalHeader[28] + (pLocalHeader[29] << 8);
|
||||||
|
fileOffset += 30 + filenameLength + extraOffset;
|
||||||
|
fseek(file, fileOffset, SEEK_SET);
|
||||||
|
|
||||||
|
const auto deflatedDataSize = static_cast<size_t>(fileStat.m_comp_size);
|
||||||
|
const auto inflatedDataSize = static_cast<size_t>(fileStat.m_uncomp_size);
|
||||||
|
const auto dataSize = trailingNullByte ? inflatedDataSize + 1 : inflatedDataSize;
|
||||||
|
const auto data = static_cast<uint8_t*>(malloc(dataSize));
|
||||||
|
|
||||||
|
if (!fileStat.m_method) {
|
||||||
|
// no deflation, just read content
|
||||||
|
const size_t dataRead = fread(data, 1, inflatedDataSize, file);
|
||||||
|
fclose(file);
|
||||||
|
if (dataRead != inflatedDataSize) {
|
||||||
|
Serial.println("Failed to read data");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Read out deflated content from file
|
||||||
|
const auto deflatedData = static_cast<uint8_t*>(malloc(deflatedDataSize));
|
||||||
|
if (deflatedData == nullptr) {
|
||||||
|
Serial.println("Failed to allocate memory for decompression buffer");
|
||||||
|
fclose(file);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t dataRead = fread(deflatedData, 1, deflatedDataSize, file);
|
||||||
|
fclose(file);
|
||||||
|
if (dataRead != deflatedDataSize) {
|
||||||
|
Serial.printf("Failed to read data, expected %d got %d\n", deflatedDataSize, dataRead);
|
||||||
|
free(deflatedData);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int result = libzInflateOneShot(deflatedData, deflatedDataSize, data, inflatedDataSize);
|
||||||
|
free(deflatedData);
|
||||||
|
if (result != MZ_OK) {
|
||||||
|
Serial.println("Failed to inflate file");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trailingNullByte) {
|
||||||
|
data[inflatedDataSize] = '\0';
|
||||||
|
}
|
||||||
|
if (size) {
|
||||||
|
*size = inflatedDataSize;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
class ZipFile {
|
||||||
|
std::string filePath;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ZipFile(std::string filePath) : filePath(std::move(filePath)) {}
|
||||||
|
~ZipFile() = default;
|
||||||
|
char* readTextFileToMemory(const char* filename, size_t* size = nullptr) const;
|
||||||
|
uint8_t* readFileToMemory(const char* filename, size_t* size = nullptr, bool trailingNullByte = false) const;
|
||||||
|
};
|
||||||
+7072
File diff suppressed because it is too large
Load Diff
+1709
File diff suppressed because it is too large
Load Diff
Submodule
+1
Submodule open-x4-sdk added at be0cb2bb34
@@ -0,0 +1,7 @@
|
|||||||
|
# Name, Type, SubType, Offset, Size, Flags
|
||||||
|
nvs, data, nvs, 0x9000, 0x5000,
|
||||||
|
otadata, data, ota, 0xe000, 0x2000,
|
||||||
|
app0, app, ota_0, 0x10000, 0x640000,
|
||||||
|
app1, app, ota_1, 0x650000,0x640000,
|
||||||
|
spiffs, data, spiffs, 0xc90000,0x360000,
|
||||||
|
coredump, data, coredump,0xFF0000,0x10000,
|
||||||
|
@@ -0,0 +1,36 @@
|
|||||||
|
; PlatformIO Project Configuration File
|
||||||
|
;
|
||||||
|
; Build options: build flags, source filter
|
||||||
|
; Upload options: custom upload port, speed and extra flags
|
||||||
|
; Library options: dependencies, extra library storages
|
||||||
|
; Advanced options: extra scripting
|
||||||
|
;
|
||||||
|
; Please visit documentation for the other options and examples
|
||||||
|
; https://docs.platformio.org/page/projectconf.html
|
||||||
|
|
||||||
|
[env:esp32-c3-devkitm-1]
|
||||||
|
platform = espressif32
|
||||||
|
board = esp32-c3-devkitm-1
|
||||||
|
framework = arduino
|
||||||
|
monitor_speed = 115200
|
||||||
|
upload_speed = 921600
|
||||||
|
|
||||||
|
board_upload.flash_size = 16MB
|
||||||
|
board_upload.maximum_size = 16777216
|
||||||
|
board_upload.offset_address = 0x10000
|
||||||
|
|
||||||
|
; Board configuration
|
||||||
|
board_build.flash_mode = dio
|
||||||
|
board_build.flash_size = 16MB
|
||||||
|
board_build.partitions = partitions.csv
|
||||||
|
|
||||||
|
build_flags =
|
||||||
|
-DARDUINO_USB_MODE=1
|
||||||
|
-DARDUINO_USB_CDC_ON_BOOT=1
|
||||||
|
-DMINIZ_NO_ZLIB_COMPATIBLE_NAMES=1
|
||||||
|
|
||||||
|
; Libraries
|
||||||
|
lib_deps =
|
||||||
|
zinggjm/GxEPD2@^1.6.5
|
||||||
|
https://github.com/leethomason/tinyxml2.git#11.0.0
|
||||||
|
BatteryMonitor=symlink://open-x4-sdk/libs/hardware/BatteryMonitor
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <BatteryMonitor.h>
|
||||||
|
|
||||||
|
#define BAT_GPIO0 0 // Battery voltage
|
||||||
|
|
||||||
|
static BatteryMonitor battery(BAT_GPIO0);
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#include "Input.h"
|
||||||
|
|
||||||
|
#include <esp32-hal-adc.h>
|
||||||
|
|
||||||
|
void setupInputPinModes() {
|
||||||
|
pinMode(BTN_GPIO1, INPUT);
|
||||||
|
pinMode(BTN_GPIO2, INPUT);
|
||||||
|
pinMode(BTN_GPIO3, INPUT_PULLUP); // Power button
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get currently pressed button by reading ADC values (and digital for power
|
||||||
|
// button)
|
||||||
|
Button getPressedButton() {
|
||||||
|
// Check BTN_GPIO3 (Power button) - digital read
|
||||||
|
if (digitalRead(BTN_GPIO3) == LOW) return POWER;
|
||||||
|
|
||||||
|
// Check BTN_GPIO1 (4 buttons on resistor ladder)
|
||||||
|
const int btn1 = analogRead(BTN_GPIO1);
|
||||||
|
if (btn1 < BTN_RIGHT_VAL + BTN_THRESHOLD) return RIGHT;
|
||||||
|
if (btn1 < BTN_LEFT_VAL + BTN_THRESHOLD) return LEFT;
|
||||||
|
if (btn1 < BTN_CONFIRM_VAL + BTN_THRESHOLD) return CONFIRM;
|
||||||
|
if (btn1 < BTN_BACK_VAL + BTN_THRESHOLD) return BACK;
|
||||||
|
|
||||||
|
// Check BTN_GPIO2 (2 buttons on resistor ladder)
|
||||||
|
const int btn2 = analogRead(BTN_GPIO2);
|
||||||
|
if (btn2 < BTN_VOLUME_DOWN_VAL + BTN_THRESHOLD) return VOLUME_DOWN;
|
||||||
|
if (btn2 < BTN_VOLUME_UP_VAL + BTN_THRESHOLD) return VOLUME_UP;
|
||||||
|
|
||||||
|
return NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Input getInput(const bool skipWait) {
|
||||||
|
const Button button = getPressedButton();
|
||||||
|
if (button == NONE) return {NONE, 0};
|
||||||
|
|
||||||
|
if (skipWait) {
|
||||||
|
return {button, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto start = millis();
|
||||||
|
while (getPressedButton() == button) delay(50);
|
||||||
|
return {button, millis() - start};
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
// 4 buttons on ADC resistor ladder: Back, Confirm, Left, Right
|
||||||
|
#define BTN_GPIO1 1
|
||||||
|
// 2 buttons on ADC resistor ladder: Volume Up, Volume Down
|
||||||
|
#define BTN_GPIO2 2
|
||||||
|
// Power button (digital)
|
||||||
|
#define BTN_GPIO3 3
|
||||||
|
|
||||||
|
// Button ADC thresholds
|
||||||
|
#define BTN_THRESHOLD 100 // Threshold tolerance
|
||||||
|
#define BTN_RIGHT_VAL 3
|
||||||
|
#define BTN_LEFT_VAL 1470
|
||||||
|
#define BTN_CONFIRM_VAL 2655
|
||||||
|
#define BTN_BACK_VAL 3470
|
||||||
|
#define BTN_VOLUME_DOWN_VAL 3
|
||||||
|
#define BTN_VOLUME_UP_VAL 2305
|
||||||
|
|
||||||
|
enum Button { NONE = 0, RIGHT, LEFT, CONFIRM, BACK, VOLUME_UP, VOLUME_DOWN, POWER };
|
||||||
|
|
||||||
|
struct Input {
|
||||||
|
Button button;
|
||||||
|
unsigned long pressTime;
|
||||||
|
};
|
||||||
|
|
||||||
|
void setupInputPinModes();
|
||||||
|
Button getPressedButton();
|
||||||
|
Input getInput(bool skipWait = false);
|
||||||
+182
@@ -0,0 +1,182 @@
|
|||||||
|
#include <Arduino.h>
|
||||||
|
#include <EpdRenderer.h>
|
||||||
|
#include <Epub.h>
|
||||||
|
#include <GxEPD2_BW.h>
|
||||||
|
#include <SD.h>
|
||||||
|
#include <SPI.h>
|
||||||
|
|
||||||
|
#include "Battery.h"
|
||||||
|
#include "Input.h"
|
||||||
|
#include "screens/EpubReaderScreen.h"
|
||||||
|
#include "screens/FullScreenMessageScreen.h"
|
||||||
|
|
||||||
|
#define SPI_FQ 40000000
|
||||||
|
// Display SPI pins (custom pins for XteinkX4, not hardware SPI defaults)
|
||||||
|
#define EPD_SCLK 8 // SPI Clock
|
||||||
|
#define EPD_MOSI 10 // SPI MOSI (Master Out Slave In)
|
||||||
|
#define EPD_CS 21 // Chip Select
|
||||||
|
#define EPD_DC 4 // Data/Command
|
||||||
|
#define EPD_RST 5 // Reset
|
||||||
|
#define EPD_BUSY 6 // Busy
|
||||||
|
|
||||||
|
#define UART0_RXD 20 // Used for USB connection detection
|
||||||
|
|
||||||
|
#define SD_SPI_CS 12
|
||||||
|
#define SD_SPI_MISO 7
|
||||||
|
|
||||||
|
GxEPD2_BW<GxEPD2_426_GDEQ0426T82, GxEPD2_426_GDEQ0426T82::HEIGHT> display(GxEPD2_426_GDEQ0426T82(EPD_CS, EPD_DC,
|
||||||
|
EPD_RST, EPD_BUSY));
|
||||||
|
auto renderer = new EpdRenderer(&display);
|
||||||
|
Screen* currentScreen;
|
||||||
|
|
||||||
|
// Power button timing
|
||||||
|
// Time required to confirm boot from sleep
|
||||||
|
constexpr unsigned long POWER_BUTTON_WAKEUP_MS = 1500;
|
||||||
|
// Time required to enter sleep mode
|
||||||
|
constexpr unsigned long POWER_BUTTON_SLEEP_MS = 1000;
|
||||||
|
|
||||||
|
Epub* loadEpub(const std::string& path) {
|
||||||
|
if (!SD.exists(path.c_str())) {
|
||||||
|
Serial.println("File does not exist");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto epub = new Epub(path, "/.crosspoint");
|
||||||
|
if (epub->load()) {
|
||||||
|
return epub;
|
||||||
|
}
|
||||||
|
|
||||||
|
Serial.println("Failed to load epub");
|
||||||
|
free(epub);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enterNewScreen(Screen* screen) {
|
||||||
|
if (currentScreen) {
|
||||||
|
currentScreen->onExit();
|
||||||
|
delete currentScreen;
|
||||||
|
}
|
||||||
|
currentScreen = screen;
|
||||||
|
currentScreen->onEnter();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify long press on wake-up from deep sleep
|
||||||
|
void verifyWakeupLongPress() {
|
||||||
|
const auto input = getInput();
|
||||||
|
|
||||||
|
if (input.button == POWER && input.pressTime > POWER_BUTTON_WAKEUP_MS) {
|
||||||
|
// Button released too early. Returning to sleep.
|
||||||
|
// IMPORTANT: Re-arm the wakeup trigger before sleeping again
|
||||||
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||||
|
esp_deep_sleep_start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter deep sleep mode
|
||||||
|
void enterDeepSleep() {
|
||||||
|
enterNewScreen(new FullScreenMessageScreen(renderer, "Sleeping", true, false, true));
|
||||||
|
|
||||||
|
Serial.println("Power button released after a long press. Entering deep sleep.");
|
||||||
|
delay(2000); // Allow Serial buffer to empty and display to update
|
||||||
|
|
||||||
|
// Enable Wakeup on LOW (button press)
|
||||||
|
esp_deep_sleep_enable_gpio_wakeup(1ULL << BTN_GPIO3, ESP_GPIO_WAKEUP_GPIO_LOW);
|
||||||
|
|
||||||
|
display.hibernate();
|
||||||
|
|
||||||
|
// Enter Deep Sleep
|
||||||
|
esp_deep_sleep_start();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupSerial() {
|
||||||
|
Serial.begin(115200);
|
||||||
|
// Wait for serial monitor
|
||||||
|
const unsigned long start = millis();
|
||||||
|
while (!Serial && (millis() - start) < 3000) {
|
||||||
|
delay(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Serial) {
|
||||||
|
// delay for monitor to start reading
|
||||||
|
delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void setup() {
|
||||||
|
setupInputPinModes();
|
||||||
|
|
||||||
|
// Check if boot was triggered by the Power Button (Deep Sleep Wakeup)
|
||||||
|
// If triggered by RST pin or Battery insertion, this will be false, allowing
|
||||||
|
// normal boot.
|
||||||
|
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_GPIO) {
|
||||||
|
verifyWakeupLongPress();
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSerial();
|
||||||
|
|
||||||
|
// Initialize pins
|
||||||
|
pinMode(BAT_GPIO0, INPUT);
|
||||||
|
|
||||||
|
// Initialize SPI with custom pins
|
||||||
|
SPI.begin(EPD_SCLK, SD_SPI_MISO, EPD_MOSI, EPD_CS);
|
||||||
|
|
||||||
|
// Initialize display
|
||||||
|
const SPISettings spi_settings(SPI_FQ, MSBFIRST, SPI_MODE0);
|
||||||
|
display.init(115200, true, 2, false, SPI, spi_settings);
|
||||||
|
display.setRotation(3); // 270 degrees
|
||||||
|
display.setTextColor(GxEPD_BLACK);
|
||||||
|
Serial.println("Display initialized");
|
||||||
|
|
||||||
|
enterNewScreen(new FullScreenMessageScreen(renderer, "Loading...", true));
|
||||||
|
|
||||||
|
// SD Card Initialization
|
||||||
|
SD.begin(SD_SPI_CS, SPI, SPI_FQ);
|
||||||
|
|
||||||
|
// TODO: Add a file selection screen, for now just load the first file
|
||||||
|
File root = SD.open("/");
|
||||||
|
String filename;
|
||||||
|
while (true) {
|
||||||
|
filename = root.getNextFileName();
|
||||||
|
if (!filename) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filename.substring(filename.length() - 5) == ".epub") {
|
||||||
|
Serial.printf("Found epub: %s\n", filename.c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filename) {
|
||||||
|
enterNewScreen(new FullScreenMessageScreen(renderer, "Could not find epub"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Epub* epub = loadEpub(std::string(filename.c_str()));
|
||||||
|
if (epub) {
|
||||||
|
enterNewScreen(new EpubReaderScreen(renderer, epub));
|
||||||
|
} else {
|
||||||
|
enterNewScreen(new FullScreenMessageScreen(renderer, "Failed to load epub"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loop() {
|
||||||
|
delay(50);
|
||||||
|
|
||||||
|
const Input input = getInput();
|
||||||
|
|
||||||
|
if (input.button == NONE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.button == POWER && input.pressTime > POWER_BUTTON_SLEEP_MS) {
|
||||||
|
enterDeepSleep();
|
||||||
|
// This should never be hit as `enterDeepSleep` calls esp_deep_sleep_start
|
||||||
|
delay(1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentScreen) {
|
||||||
|
currentScreen->handleInput(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
#include "EpubReaderScreen.h"
|
||||||
|
|
||||||
|
#include <EpdRenderer.h>
|
||||||
|
#include <SD.h>
|
||||||
|
|
||||||
|
#include "Battery.h"
|
||||||
|
|
||||||
|
constexpr unsigned long SKIP_CHAPTER_MS = 700;
|
||||||
|
|
||||||
|
void EpubReaderScreen::taskTrampoline(void* param) {
|
||||||
|
auto* self = static_cast<EpubReaderScreen*>(param);
|
||||||
|
self->displayTaskLoop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderScreen::onEnter() {
|
||||||
|
sectionMutex = xSemaphoreCreateMutex();
|
||||||
|
|
||||||
|
epub->setupCacheDir();
|
||||||
|
|
||||||
|
// TODO: Move this to a state object
|
||||||
|
if (SD.exists((epub->getCachePath() + "/progress.bin").c_str())) {
|
||||||
|
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str());
|
||||||
|
uint8_t data[4];
|
||||||
|
f.read(data, 4);
|
||||||
|
currentSpineIndex = data[0] + (data[1] << 8);
|
||||||
|
nextPageNumber = data[2] + (data[3] << 8);
|
||||||
|
Serial.printf("Loaded cache: %d, %d\n", currentSpineIndex, nextPageNumber);
|
||||||
|
f.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trigger first update
|
||||||
|
updateRequired = true;
|
||||||
|
|
||||||
|
xTaskCreate(&EpubReaderScreen::taskTrampoline, "EpubReaderScreenTask",
|
||||||
|
8192, // Stack size
|
||||||
|
this, // Parameters
|
||||||
|
1, // Priority
|
||||||
|
&displayTaskHandle // Task handle
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderScreen::onExit() {
|
||||||
|
vTaskDelete(displayTaskHandle);
|
||||||
|
xSemaphoreTake(sectionMutex, portMAX_DELAY);
|
||||||
|
vSemaphoreDelete(sectionMutex);
|
||||||
|
sectionMutex = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderScreen::handleInput(const Input input) {
|
||||||
|
if (input.button == VOLUME_UP || input.button == VOLUME_DOWN) {
|
||||||
|
const bool skipChapter = input.pressTime > SKIP_CHAPTER_MS;
|
||||||
|
|
||||||
|
// No current section, attempt to rerender the book
|
||||||
|
if (!section) {
|
||||||
|
updateRequired = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.button == VOLUME_UP && skipChapter) {
|
||||||
|
nextPageNumber = 0;
|
||||||
|
currentSpineIndex--;
|
||||||
|
delete section;
|
||||||
|
section = nullptr;
|
||||||
|
} else if (input.button == VOLUME_DOWN && skipChapter) {
|
||||||
|
nextPageNumber = 0;
|
||||||
|
currentSpineIndex++;
|
||||||
|
delete section;
|
||||||
|
section = nullptr;
|
||||||
|
} else if (input.button == VOLUME_UP) {
|
||||||
|
if (section->currentPage > 0) {
|
||||||
|
section->currentPage--;
|
||||||
|
} else {
|
||||||
|
xSemaphoreTake(sectionMutex, portMAX_DELAY);
|
||||||
|
nextPageNumber = UINT16_MAX;
|
||||||
|
currentSpineIndex--;
|
||||||
|
delete section;
|
||||||
|
section = nullptr;
|
||||||
|
xSemaphoreGive(sectionMutex);
|
||||||
|
}
|
||||||
|
} else if (input.button == VOLUME_DOWN) {
|
||||||
|
if (section->currentPage < section->pageCount - 1) {
|
||||||
|
section->currentPage++;
|
||||||
|
} else {
|
||||||
|
xSemaphoreTake(sectionMutex, portMAX_DELAY);
|
||||||
|
nextPageNumber = 0;
|
||||||
|
currentSpineIndex++;
|
||||||
|
delete section;
|
||||||
|
section = nullptr;
|
||||||
|
xSemaphoreGive(sectionMutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRequired = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderScreen::displayTaskLoop() {
|
||||||
|
while (true) {
|
||||||
|
if (updateRequired) {
|
||||||
|
updateRequired = false;
|
||||||
|
renderPage();
|
||||||
|
}
|
||||||
|
vTaskDelay(10 / portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderScreen::renderPage() {
|
||||||
|
if (!epub) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSpineIndex >= epub->getSpineItemsCount() || currentSpineIndex < 0) {
|
||||||
|
currentSpineIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(sectionMutex, portMAX_DELAY);
|
||||||
|
if (!section) {
|
||||||
|
const auto filepath = epub->getSpineItem(currentSpineIndex);
|
||||||
|
Serial.printf("Loading file: %s, index: %d\n", filepath.c_str(), currentSpineIndex);
|
||||||
|
section = new Section(epub, currentSpineIndex, renderer);
|
||||||
|
if (!section->hasCache()) {
|
||||||
|
Serial.println("Cache not found, building...");
|
||||||
|
section->setupCacheDir();
|
||||||
|
if (!section->persistPageDataToSD()) {
|
||||||
|
Serial.println("Failed to persist page data to SD");
|
||||||
|
free(section);
|
||||||
|
section = nullptr;
|
||||||
|
xSemaphoreGive(sectionMutex);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Serial.println("Cache found, skipping build...");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextPageNumber == UINT16_MAX) {
|
||||||
|
section->currentPage = section->pageCount - 1;
|
||||||
|
} else {
|
||||||
|
section->currentPage = nextPageNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer->clearScreen();
|
||||||
|
section->renderPage();
|
||||||
|
renderStatusBar();
|
||||||
|
renderer->flushDisplay();
|
||||||
|
|
||||||
|
File f = SD.open((epub->getCachePath() + "/progress.bin").c_str(), FILE_WRITE);
|
||||||
|
uint8_t data[4];
|
||||||
|
data[0] = currentSpineIndex & 0xFF;
|
||||||
|
data[1] = (currentSpineIndex >> 8) & 0xFF;
|
||||||
|
data[2] = section->currentPage & 0xFF;
|
||||||
|
data[3] = (section->currentPage >> 8) & 0xFF;
|
||||||
|
f.write(data, 4);
|
||||||
|
f.close();
|
||||||
|
|
||||||
|
xSemaphoreGive(sectionMutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EpubReaderScreen::renderStatusBar() const {
|
||||||
|
const auto pageWidth = renderer->getPageWidth();
|
||||||
|
|
||||||
|
std::string progress = std::to_string(currentPage + 1) + " / " + std::to_string(section->pageCount);
|
||||||
|
const auto progressTextWidth = renderer->getSmallTextWidth(progress.c_str());
|
||||||
|
renderer->drawSmallText(pageWidth - progressTextWidth, 765, progress.c_str());
|
||||||
|
|
||||||
|
const uint16_t percentage = battery.readPercentage();
|
||||||
|
auto percentageText = std::to_string(percentage) + "%";
|
||||||
|
const auto percentageTextWidth = renderer->getSmallTextWidth(percentageText.c_str());
|
||||||
|
renderer->drawSmallText(20, 765, percentageText.c_str());
|
||||||
|
|
||||||
|
// 1 column on left, 2 columns on right, 5 columns of battery body
|
||||||
|
constexpr int batteryWidth = 15;
|
||||||
|
constexpr int batteryHeight = 10;
|
||||||
|
const int x = 0;
|
||||||
|
const int y = 772;
|
||||||
|
|
||||||
|
// Top line
|
||||||
|
renderer->drawLine(x, y, x + batteryWidth - 4, y, 1);
|
||||||
|
// Bottom line
|
||||||
|
renderer->drawLine(x, y + batteryHeight - 1, x + batteryWidth - 4, y + batteryHeight - 1, 1);
|
||||||
|
// Left line
|
||||||
|
renderer->drawLine(x, y, x, y + batteryHeight - 1, 1);
|
||||||
|
// Battery end
|
||||||
|
renderer->drawLine(x + batteryWidth - 4, y, x + batteryWidth - 4, y + batteryHeight - 1, 1);
|
||||||
|
renderer->drawLine(x + batteryWidth - 3, y + 2, x + batteryWidth - 3, y + batteryHeight - 3, 1);
|
||||||
|
renderer->drawLine(x + batteryWidth - 2, y + 2, x + batteryWidth - 2, y + batteryHeight - 3, 1);
|
||||||
|
renderer->drawLine(x + batteryWidth - 1, y + 2, x + batteryWidth - 1, y + batteryHeight - 3, 1);
|
||||||
|
|
||||||
|
// The +1 is to round up, so that we always fill at least one pixel
|
||||||
|
int filledWidth = percentage * (batteryWidth - 5) / 100 + 1;
|
||||||
|
if (filledWidth > batteryWidth - 5) {
|
||||||
|
filledWidth = batteryWidth - 5; // Ensure we don't overflow
|
||||||
|
}
|
||||||
|
renderer->fillRect(x + 1, y + 1, filledWidth, batteryHeight - 2, 1);
|
||||||
|
|
||||||
|
// Page width minus existing content with 30px padding on each side
|
||||||
|
const int leftMargin = 20 + percentageTextWidth + 30;
|
||||||
|
const int rightMargin = progressTextWidth + 30;
|
||||||
|
const int availableTextWidth = pageWidth - leftMargin - rightMargin;
|
||||||
|
const auto tocItem = epub->getTocItem(epub->getTocIndexForSpineIndex(currentSpineIndex));
|
||||||
|
auto title = tocItem.title;
|
||||||
|
int titleWidth = renderer->getSmallTextWidth(title.c_str());
|
||||||
|
while (titleWidth > availableTextWidth) {
|
||||||
|
title = title.substr(0, title.length() - 8) + "...";
|
||||||
|
titleWidth = renderer->getSmallTextWidth(title.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer->drawSmallText(leftMargin + (availableTextWidth - titleWidth) / 2, 765, title.c_str());
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <Epub.h>
|
||||||
|
#include <Epub/Section.h>
|
||||||
|
#include <freertos/FreeRTOS.h>
|
||||||
|
#include <freertos/semphr.h>
|
||||||
|
#include <freertos/task.h>
|
||||||
|
|
||||||
|
#include "Screen.h"
|
||||||
|
|
||||||
|
class EpubReaderScreen final : public Screen {
|
||||||
|
Epub* epub;
|
||||||
|
Section* section = nullptr;
|
||||||
|
TaskHandle_t displayTaskHandle = nullptr;
|
||||||
|
SemaphoreHandle_t sectionMutex = nullptr;
|
||||||
|
int currentSpineIndex = 0;
|
||||||
|
int nextPageNumber = 0;
|
||||||
|
int currentPage = 0;
|
||||||
|
bool updateRequired = false;
|
||||||
|
|
||||||
|
static void taskTrampoline(void* param);
|
||||||
|
[[noreturn]] void displayTaskLoop();
|
||||||
|
void renderPage();
|
||||||
|
void renderStatusBar() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit EpubReaderScreen(EpdRenderer* renderer, Epub* epub) : Screen(renderer), epub(epub) {}
|
||||||
|
~EpubReaderScreen() override { free(section); }
|
||||||
|
void onEnter() override;
|
||||||
|
void onExit() override;
|
||||||
|
void handleInput(Input input) override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#include "FullScreenMessageScreen.h"
|
||||||
|
|
||||||
|
#include <EpdRenderer.h>
|
||||||
|
|
||||||
|
void FullScreenMessageScreen::onEnter() {
|
||||||
|
const auto width = renderer->getTextWidth(text.c_str(), bold, italic);
|
||||||
|
const auto height = renderer->getLineHeight();
|
||||||
|
const auto left = (renderer->getPageWidth() - width) / 2;
|
||||||
|
const auto top = (renderer->getPageHeight() - height) / 2;
|
||||||
|
|
||||||
|
renderer->clearScreen(invert);
|
||||||
|
renderer->drawText(left, top, text.c_str(), bold, italic, invert ? 0 : 1);
|
||||||
|
renderer->flushDisplay();
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "Screen.h"
|
||||||
|
|
||||||
|
class FullScreenMessageScreen final : public Screen {
|
||||||
|
std::string text;
|
||||||
|
bool bold;
|
||||||
|
bool italic;
|
||||||
|
bool invert;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit FullScreenMessageScreen(EpdRenderer* renderer, std::string text, const bool bold = false,
|
||||||
|
const bool italic = false, const bool invert = false)
|
||||||
|
: Screen(renderer), text(std::move(text)), bold(bold), italic(italic), invert(invert) {}
|
||||||
|
void onEnter() override;
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "Input.h"
|
||||||
|
|
||||||
|
class EpdRenderer;
|
||||||
|
|
||||||
|
class Screen {
|
||||||
|
protected:
|
||||||
|
EpdRenderer* renderer;
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit Screen(EpdRenderer* renderer) : renderer(renderer) {}
|
||||||
|
virtual ~Screen() = default;
|
||||||
|
virtual void onEnter() {}
|
||||||
|
virtual void onExit() {}
|
||||||
|
virtual void handleInput(Input input) {}
|
||||||
|
};
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
This directory is intended for PlatformIO Test Runner and project tests.
|
||||||
|
|
||||||
|
Unit Testing is a software testing method by which individual units of
|
||||||
|
source code, sets of one or more MCU program modules together with associated
|
||||||
|
control data, usage procedures, and operating procedures, are tested to
|
||||||
|
determine whether they are fit for use. Unit testing finds problems early
|
||||||
|
in the development cycle.
|
||||||
|
|
||||||
|
More information about PlatformIO Unit Testing:
|
||||||
|
- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html
|
||||||
Reference in New Issue
Block a user