Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
/**
|
|
|
|
|
* generated by fontconvert.py
|
|
|
|
|
* name: ubuntu_12_regular
|
|
|
|
|
* size: 12
|
|
|
|
|
* mode: 1-bit
|
2026-01-27 12:19:19 +01:00
|
|
|
* Command used: fontconvert.py ubuntu_12_regular 12 ../builtinFonts/source/Ubuntu/Ubuntu-Regular.ttf
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
*/
|
|
|
|
|
#pragma once
|
|
|
|
|
#include "EpdFontData.h"
|
|
|
|
|
|
2026-02-25 03:56:11 -06:00
|
|
|
static const uint8_t ubuntu_12_regularBitmaps[21226] = {
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x9C, 0xE0, 0x01, 0xDE, 0x70, 0xF7, 0xF7, 0xF7, 0x67, 0x67,
|
|
|
|
|
0x67, 0x0E, 0x18, 0x1C, 0x70, 0x38, 0xE0, 0x71, 0xC0, 0xC3, 0x9F, 0xFF, 0xFF, 0xFF, 0x8E, 0x38,
|
|
|
|
|
0x1C, 0x70, 0x30, 0xE3, 0xFF, 0xFF, 0xFF, 0xF3, 0x8E, 0x07, 0x1C, 0x0E, 0x38, 0x38, 0x60, 0x71,
|
|
|
|
|
0xC0, 0x06, 0x00, 0x60, 0x06, 0x01, 0xFE, 0x7F, 0xE7, 0x8E, 0x70, 0x07, 0x00, 0x70, 0x07, 0x80,
|
|
|
|
|
0x3E, 0x01, 0xF8, 0x03, 0xC0, 0x1E, 0x00, 0xF0, 0x07, 0x00, 0xF7, 0x1F, 0xFF, 0xE7, 0xFC, 0x06,
|
|
|
|
|
0x00, 0x60, 0x06, 0x00, 0x3E, 0x07, 0x07, 0xF0, 0xE0, 0xF7, 0x0E, 0x0E, 0x31, 0xC0, 0xE3, 0xBC,
|
|
|
|
|
0x0E, 0x3B, 0x80, 0xE3, 0x70, 0x0F, 0x77, 0x7C, 0x7F, 0xEF, 0xE3, 0xEF, 0xEE, 0x01, 0xDC, 0x60,
|
|
|
|
|
0x3D, 0xC7, 0x03, 0x9C, 0x70, 0x71, 0xC6, 0x07, 0x1E, 0xE0, 0xE0, 0xFE, 0x0E, 0x07, 0xC0, 0x0F,
|
|
|
|
|
0x80, 0x3F, 0xC0, 0x3D, 0xE0, 0x38, 0xE0, 0x38, 0xE0, 0x38, 0xE0, 0x3B, 0xC0, 0x1F, 0x80, 0x3F,
|
|
|
|
|
0x00, 0x7F, 0x0C, 0xF3, 0x9C, 0xE1, 0xDC, 0xE0, 0xF8, 0xE0, 0x78, 0xF9, 0xF8, 0x7F, 0xFE, 0x3F,
|
|
|
|
|
0x8E, 0xFF, 0xF6, 0x66, 0x06, 0x1C, 0x70, 0xE3, 0x87, 0x1E, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E,
|
|
|
|
|
0x1C, 0x38, 0x70, 0xE1, 0xE1, 0xC3, 0x83, 0x87, 0x07, 0x06, 0xE1, 0xC1, 0xC3, 0x83, 0x87, 0x07,
|
|
|
|
|
0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xE3, 0x87, 0x0E, 0x1C, 0x38, 0x71, 0xC3, 0x8E, 0x1C, 0x70, 0xE0,
|
|
|
|
|
0x06, 0x00, 0x60, 0x66, 0x67, 0xFE, 0x7F, 0xE0, 0xF0, 0x1F, 0x81, 0xF8, 0x39, 0xC1, 0x98, 0x06,
|
|
|
|
|
0x00, 0x60, 0x06, 0x00, 0x60, 0x06, 0x0F, 0xFF, 0xFF, 0xF0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00,
|
|
|
|
|
0x60, 0x39, 0xDE, 0xE7, 0x39, 0x80, 0xFF, 0xFC, 0xFF, 0xF0, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x1C,
|
|
|
|
|
0x01, 0xC0, 0x1C, 0x03, 0x80, 0x38, 0x03, 0x80, 0x70, 0x07, 0x00, 0x70, 0x0E, 0x00, 0xE0, 0x0E,
|
|
|
|
|
0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x38, 0x03, 0x80, 0x38, 0x07, 0x00, 0x70, 0x07, 0x00, 0xE0, 0x00,
|
|
|
|
|
0x1F, 0x83, 0xFC, 0x79, 0xE7, 0x0E, 0xF0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0,
|
|
|
|
|
0x7E, 0x07, 0xF0, 0x77, 0x0E, 0x79, 0xE3, 0xFC, 0x1F, 0x80, 0x0F, 0x1F, 0x7F, 0xFF, 0xE7, 0x07,
|
|
|
|
|
0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x3F, 0x87, 0xFC, 0xF3, 0xE4,
|
|
|
|
|
0x0E, 0x00, 0xE0, 0x0E, 0x01, 0xE0, 0x3C, 0x07, 0x80, 0xF0, 0x1E, 0x01, 0xC0, 0x38, 0x07, 0x00,
|
|
|
|
|
0x70, 0x0F, 0xFF, 0xFF, 0xF0, 0x3F, 0x87, 0xFC, 0x73, 0xC0, 0x1E, 0x00, 0xE0, 0x1E, 0x03, 0xC1,
|
|
|
|
|
0xF8, 0x1F, 0xC0, 0x7E, 0x00, 0xE0, 0x0F, 0x00, 0xF0, 0x0E, 0xE3, 0xEF, 0xFC, 0xFF, 0x80, 0x01,
|
|
|
|
|
0xC0, 0x1E, 0x01, 0xF0, 0x1F, 0x80, 0xFC, 0x0E, 0xE0, 0xE7, 0x07, 0x38, 0x71, 0xC3, 0x0E, 0x38,
|
|
|
|
|
0x71, 0xFF, 0xEF, 0xFF, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x3F, 0xE3, 0xFE, 0x38, 0x03,
|
|
|
|
|
0x80, 0x30, 0x03, 0x00, 0x7F, 0x07, 0xFC, 0x0F, 0xE0, 0x1E, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x0E,
|
|
|
|
|
0x63, 0xEF, 0xFC, 0x7F, 0x80, 0x03, 0xE0, 0xFE, 0x1F, 0xC3, 0xC0, 0x78, 0x07, 0x00, 0x7F, 0xCF,
|
|
|
|
|
0xFE, 0xF9, 0xFE, 0x07, 0xE0, 0x7F, 0x07, 0xF0, 0x77, 0x0F, 0x7D, 0xE3, 0xFE, 0x1F, 0x80, 0xFF,
|
|
|
|
|
0xFF, 0xFF, 0x00, 0xF0, 0x1E, 0x01, 0xC0, 0x3C, 0x03, 0x80, 0x70, 0x07, 0x00, 0x70, 0x0E, 0x00,
|
|
|
|
|
0xE0, 0x0E, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0x1F, 0x83, 0xFE, 0x79, 0xE7, 0x0F, 0xF0,
|
|
|
|
|
0x77, 0x0E, 0x78, 0xE7, 0xFC, 0x3F, 0xC7, 0xFE, 0x71, 0xFE, 0x07, 0xE0, 0x7E, 0x07, 0xF9, 0xF7,
|
|
|
|
|
0xFE, 0x3F, 0xC0, 0x1F, 0x87, 0xFC, 0x79, 0xEF, 0x0E, 0xE0, 0xFE, 0x07, 0xE0, 0x7F, 0x9F, 0x7F,
|
|
|
|
|
0xF3, 0xFF, 0x00, 0xE0, 0x0E, 0x01, 0xE0, 0x3C, 0x1F, 0x87, 0xF0, 0x7C, 0x00, 0xFF, 0xF0, 0x00,
|
|
|
|
|
0x00, 0x00, 0xFF, 0xF0, 0x7B, 0xDE, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x77, 0xB9, 0xCE, 0x60, 0x00,
|
|
|
|
|
0x70, 0x1F, 0x0F, 0xE3, 0xF8, 0xFC, 0x0F, 0x00, 0xFC, 0x03, 0xF8, 0x0F, 0xE0, 0x1F, 0x00, 0x70,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0xE0, 0x0F, 0x80, 0x7F, 0x00,
|
|
|
|
|
0xFC, 0x03, 0xF0, 0x0F, 0x03, 0xF0, 0xFC, 0x7F, 0x0F, 0x80, 0xE0, 0x00, 0x7F, 0x3F, 0xE6, 0x7C,
|
|
|
|
|
0x0F, 0x01, 0xC0, 0xE0, 0x78, 0x3C, 0x0E, 0x07, 0x01, 0xC0, 0x60, 0x00, 0x00, 0x01, 0xC0, 0xF0,
|
|
|
|
|
0x1C, 0x00, 0x01, 0xFE, 0x00, 0x1F, 0xFE, 0x00, 0xFC, 0xFC, 0x07, 0x80, 0x78, 0x3C, 0x00, 0x71,
|
|
|
|
|
0xE3, 0xFD, 0xE7, 0x1F, 0xF3, 0x9C, 0xF9, 0xCE, 0xE3, 0x87, 0x3B, 0x8E, 0x1C, 0x6E, 0x78, 0x71,
|
|
|
|
|
0xB9, 0xE1, 0xCE, 0xE3, 0x87, 0x3B, 0x8E, 0x1C, 0xE7, 0x3C, 0xF7, 0x9C, 0x7F, 0xFC, 0x78, 0xFF,
|
|
|
|
|
0xE0, 0xF0, 0x00, 0x01, 0xE0, 0x00, 0x07, 0xF0, 0x80, 0x07, 0xFF, 0x00, 0x07, 0xFC, 0x00, 0x01,
|
|
|
|
|
0xC0, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0xFC, 0x00, 0x77, 0x00, 0x73, 0x80, 0x39, 0xE0, 0x3C, 0x70,
|
|
|
|
|
0x1C, 0x3C, 0x0E, 0x0E, 0x0F, 0x07, 0x07, 0xFF, 0xC3, 0xFF, 0xE3, 0x80, 0x71, 0xC0, 0x3D, 0xE0,
|
|
|
|
|
0x0E, 0xE0, 0x07, 0x80, 0xFF, 0xC7, 0xFF, 0x38, 0xFD, 0xC0, 0xEE, 0x07, 0x70, 0x3B, 0x87, 0xDF,
|
|
|
|
|
0xFC, 0xFF, 0xF7, 0x0F, 0xB8, 0x1F, 0xC0, 0x7E, 0x03, 0xF0, 0x3F, 0x87, 0xDF, 0xFE, 0xFF, 0xC0,
|
|
|
|
|
0x07, 0xF8, 0x7F, 0xF3, 0xF3, 0xDE, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0x00, 0x38, 0x00, 0xE0, 0x03,
|
|
|
|
|
0x80, 0x0F, 0x00, 0x3C, 0x00, 0x70, 0x01, 0xE0, 0x03, 0xF1, 0xC7, 0xFF, 0x07, 0xF8, 0xFF, 0xC1,
|
|
|
|
|
0xFF, 0xC3, 0x87, 0xE7, 0x03, 0xCE, 0x03, 0xDC, 0x03, 0xB8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0xC0,
|
|
|
|
|
0x3F, 0x80, 0x7F, 0x00, 0xEE, 0x03, 0xDC, 0x0F, 0x38, 0x7E, 0x7F, 0xF0, 0xFF, 0xC0, 0xFF, 0xEF,
|
|
|
|
|
0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFC, 0xFF, 0xCE, 0x00, 0xE0, 0x0E, 0x00,
|
|
|
|
|
0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0x80, 0x70, 0x0E, 0x01, 0xC0,
|
|
|
|
|
0x38, 0x07, 0xFE, 0xFF, 0xDC, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x00,
|
|
|
|
|
0x07, 0xF8, 0x3F, 0xF8, 0xFC, 0xF3, 0xC0, 0x07, 0x80, 0x0E, 0x00, 0x3C, 0x00, 0x70, 0x00, 0xE0,
|
|
|
|
|
0x01, 0xC0, 0x3F, 0xC0, 0x7F, 0x80, 0xF7, 0x01, 0xEF, 0x03, 0xCF, 0xC7, 0x8F, 0xFF, 0x07, 0xFC,
|
|
|
|
|
0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
|
|
|
0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1C, 0xFF, 0xFF,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07,
|
|
|
|
|
0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x03, 0xF9, 0xF7, 0xFE, 0x7F, 0x00, 0xE0, 0x7B, 0x83,
|
|
|
|
|
0xCE, 0x1E, 0x38, 0xF0, 0xE7, 0x83, 0xBC, 0x0F, 0xE0, 0x3F, 0x00, 0xFE, 0x03, 0xFC, 0x0E, 0xF8,
|
|
|
|
|
0x39, 0xF0, 0xE3, 0xE3, 0x87, 0xCE, 0x0F, 0x38, 0x1E, 0xE0, 0x3C, 0xE0, 0x1C, 0x03, 0x80, 0x70,
|
|
|
|
|
0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07,
|
|
|
|
|
0xFF, 0xFF, 0xE0, 0x38, 0x01, 0xC7, 0x80, 0x1C, 0x7C, 0x03, 0xC7, 0xC0, 0x3E, 0x7E, 0x06, 0xE7,
|
|
|
|
|
0x60, 0x6E, 0x77, 0x0C, 0xE7, 0x30, 0xCE, 0x73, 0x1C, 0xE7, 0x39, 0x8E, 0x71, 0x98, 0xE7, 0x1F,
|
|
|
|
|
0x8E, 0x70, 0xF0, 0xEF, 0x0F, 0x0E, 0xF0, 0xE0, 0xEE, 0x00, 0x0E, 0xE0, 0x00, 0xE0, 0xE0, 0x1D,
|
|
|
|
|
0xE0, 0x3B, 0xE0, 0x77, 0xE0, 0xEF, 0xC1, 0xDD, 0xC3, 0xBB, 0xC7, 0x73, 0xCE, 0xE3, 0x9D, 0xC3,
|
|
|
|
|
0xBB, 0x87, 0xF7, 0x07, 0xEE, 0x07, 0xDC, 0x0F, 0xB8, 0x0F, 0x70, 0x0E, 0xE0, 0x1C, 0x07, 0xF8,
|
|
|
|
|
0x0F, 0xFE, 0x0F, 0xCF, 0x8F, 0x01, 0xE7, 0x00, 0x7B, 0x80, 0x3F, 0xC0, 0x0F, 0xC0, 0x07, 0xE0,
|
|
|
|
|
0x03, 0xF0, 0x01, 0xFC, 0x00, 0xEE, 0x00, 0xF7, 0x00, 0x7B, 0xC0, 0x78, 0xFC, 0xF8, 0x3F, 0xF8,
|
|
|
|
|
0x07, 0xF8, 0x00, 0xFF, 0x87, 0xFF, 0x38, 0xFD, 0xC0, 0xEE, 0x07, 0xF0, 0x3F, 0x81, 0xFC, 0x0E,
|
|
|
|
|
0xE3, 0xF7, 0xFF, 0x3F, 0xF1, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x00, 0x07,
|
|
|
|
|
0xF8, 0x0F, 0xFE, 0x0F, 0xCF, 0x8F, 0x01, 0xE7, 0x00, 0x7B, 0x80, 0x3F, 0xC0, 0x0F, 0xC0, 0x07,
|
|
|
|
|
0xE0, 0x03, 0xF0, 0x01, 0xFC, 0x00, 0xEE, 0x00, 0xF7, 0x00, 0x7B, 0xC0, 0x78, 0xFC, 0xF8, 0x3F,
|
|
|
|
|
0xF8, 0x07, 0xF8, 0x00, 0x70, 0x00, 0x3C, 0x00, 0x0F, 0xC0, 0x03, 0xF0, 0x00, 0x78, 0xFF, 0xC3,
|
|
|
|
|
0xFF, 0x8E, 0x3F, 0x38, 0x1C, 0xE0, 0x7B, 0x81, 0xEE, 0x07, 0x38, 0x7C, 0xFF, 0xE3, 0xFF, 0x0E,
|
|
|
|
|
0x3C, 0x38, 0x70, 0xE0, 0xE3, 0x83, 0xCE, 0x07, 0x38, 0x1E, 0xE0, 0x38, 0x1F, 0xE1, 0xFF, 0x9F,
|
|
|
|
|
0x1C, 0xE0, 0x07, 0x00, 0x3C, 0x00, 0xF8, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x3C, 0x00, 0xF0,
|
|
|
|
|
0x03, 0x80, 0x3D, 0xE7, 0xCF, 0xFE, 0x3F, 0xC0, 0xFF, 0xFF, 0xFF, 0xF0, 0x78, 0x01, 0xE0, 0x07,
|
|
|
|
|
0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E,
|
|
|
|
|
0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80,
|
|
|
|
|
0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0E, 0xE0, 0x3B, 0xC1, 0xE7, 0xCF,
|
|
|
|
|
0x0F, 0xFC, 0x1F, 0xC0, 0xE0, 0x07, 0x78, 0x03, 0x9C, 0x03, 0xCF, 0x01, 0xC7, 0x80, 0xE1, 0xC0,
|
|
|
|
|
0xF0, 0xF0, 0x70, 0x38, 0x78, 0x1C, 0x38, 0x0F, 0x1C, 0x03, 0x9E, 0x01, 0xEE, 0x00, 0x7F, 0x00,
|
|
|
|
|
0x3F, 0x00, 0x0F, 0x80, 0x07, 0x80, 0x03, 0xC0, 0x00, 0xF0, 0x00, 0x0E, 0xE0, 0x00, 0x3D, 0xC0,
|
|
|
|
|
0xE0, 0x73, 0x81, 0xE0, 0xE7, 0x07, 0xC1, 0xCF, 0x0F, 0x83, 0x8E, 0x1F, 0x8F, 0x1C, 0x77, 0x1C,
|
|
|
|
|
0x38, 0xEE, 0x38, 0x79, 0xCE, 0x70, 0x77, 0x1C, 0xE0, 0xEE, 0x3B, 0x81, 0xDC, 0x3F, 0x03, 0xF0,
|
|
|
|
|
0x7E, 0x03, 0xE0, 0x78, 0x07, 0x80, 0xF0, 0x0F, 0x01, 0xE0, 0x70, 0x1E, 0x78, 0x1C, 0x3C, 0x3C,
|
|
|
|
|
0x1C, 0x78, 0x1E, 0x70, 0x0F, 0xF0, 0x07, 0xE0, 0x03, 0xC0, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0,
|
|
|
|
|
0x0E, 0xF0, 0x1E, 0x78, 0x3C, 0x3C, 0x38, 0x1C, 0x78, 0x1E, 0xF0, 0x0E, 0xF0, 0x1E, 0xE0, 0x39,
|
|
|
|
|
0xE0, 0xF1, 0xC1, 0xC3, 0xC7, 0x83, 0xDE, 0x03, 0xB8, 0x07, 0xF0, 0x07, 0xC0, 0x07, 0x00, 0x0E,
|
|
|
|
|
0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x7F, 0xFD, 0xFF, 0xF0,
|
|
|
|
|
0x07, 0x80, 0x3C, 0x00, 0xE0, 0x07, 0x80, 0x3C, 0x01, 0xE0, 0x07, 0x00, 0x3C, 0x01, 0xE0, 0x07,
|
|
|
|
|
0x00, 0x3C, 0x01, 0xE0, 0x07, 0x00, 0x3F, 0xFF, 0xFF, 0xFC, 0xFD, 0xFB, 0x87, 0x0E, 0x1C, 0x38,
|
|
|
|
|
0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x7E, 0xFC,
|
|
|
|
|
0xE0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x38, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x0E,
|
|
|
|
|
0x00, 0xE0, 0x0E, 0x00, 0x70, 0x07, 0x00, 0x70, 0x03, 0x80, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0,
|
|
|
|
|
0x1C, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0xFF, 0xF1, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C,
|
|
|
|
|
0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xFF, 0xFC, 0x0F, 0x00, 0xF0, 0x1F, 0x81, 0xDC, 0x39,
|
|
|
|
|
0xC7, 0x0E, 0x70, 0xEE, 0x07, 0xE0, 0x70, 0x7F, 0xFD, 0xFF, 0xF0, 0x63, 0xC7, 0x0E, 0x18, 0x7F,
|
|
|
|
|
0x8F, 0xF9, 0x8F, 0x00, 0xE0, 0x0E, 0x7F, 0xDF, 0xFF, 0xCF, 0xE0, 0xFC, 0x1F, 0xC7, 0xFF, 0xF3,
|
|
|
|
|
0xFC, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0x8F, 0xFC, 0xF3, 0xEE, 0x0E,
|
|
|
|
|
0xE0, 0xFE, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0xFE, 0x0E, 0xE3, 0xEF, 0xFC, 0xFF, 0x80, 0x1F, 0xC7,
|
|
|
|
|
0xF9, 0xF3, 0x38, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1E, 0x01, 0xF3, 0x1F, 0xE1, 0xFC,
|
|
|
|
|
0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x1F, 0xF3, 0xFF, 0x79, 0xFF, 0x07, 0xE0,
|
|
|
|
|
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x7C, 0x73, 0xFF, 0x1F, 0xF0, 0x1F, 0x83, 0xFE,
|
|
|
|
|
0x79, 0xEF, 0x0F, 0xE0, 0x7F, 0xFF, 0xFF, 0xFE, 0x00, 0xE0, 0x0F, 0x00, 0x7C, 0x63, 0xFE, 0x1F,
|
|
|
|
|
0xE0, 0x3F, 0x7F, 0xF3, 0xE0, 0xE0, 0xE0, 0xFF, 0xFF, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0,
|
|
|
|
|
0xE0, 0xE0, 0xE0, 0xE0, 0x1F, 0xF3, 0xFF, 0x7C, 0xF7, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07,
|
|
|
|
|
0xE0, 0x7F, 0x07, 0x7D, 0xF3, 0xFF, 0x1F, 0xF0, 0x07, 0x00, 0xF7, 0x1E, 0x7F, 0xE7, 0xF8, 0xE0,
|
|
|
|
|
0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xE7, 0xFE, 0xF3, 0xDC, 0x3F, 0x83, 0xF0, 0x7E,
|
|
|
|
|
0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0x80, 0xFF, 0x00, 0x07, 0x77, 0x77, 0x77,
|
|
|
|
|
0x77, 0x77, 0x77, 0x1E, 0x3C, 0x00, 0x00, 0x01, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3,
|
|
|
|
|
0x87, 0x0E, 0x1C, 0x38, 0xF3, 0xDF, 0xBE, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0,
|
|
|
|
|
0x38, 0x77, 0x1C, 0xEF, 0x1F, 0xC3, 0xF0, 0x7C, 0x0F, 0xC1, 0xFC, 0x3B, 0xC7, 0x3C, 0xE3, 0xDC,
|
|
|
|
|
0x3B, 0x87, 0x80, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x79,
|
|
|
|
|
0xF3, 0x80, 0xFF, 0x7F, 0x3F, 0xFF, 0xEE, 0x3E, 0x7F, 0x87, 0x07, 0xE1, 0xC1, 0xF8, 0x70, 0x7E,
|
|
|
|
|
0x1C, 0x1F, 0x87, 0x07, 0xE1, 0xC1, 0xF8, 0x70, 0x7E, 0x1C, 0x1F, 0x87, 0x07, 0xE1, 0xC1, 0xC0,
|
|
|
|
|
0xFF, 0x9F, 0xFB, 0x8F, 0x70, 0xFE, 0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0xF0,
|
|
|
|
|
0x7E, 0x0E, 0x1F, 0xC1, 0xFF, 0x1E, 0x7C, 0xE0, 0xEE, 0x07, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0,
|
|
|
|
|
0x7B, 0x83, 0x9E, 0x7C, 0x7F, 0xC1, 0xFC, 0x00, 0xFF, 0x8F, 0xFC, 0xE3, 0xEE, 0x0E, 0xE0, 0xFE,
|
|
|
|
|
0x07, 0xE0, 0x7E, 0x07, 0xE0, 0xFE, 0x0E, 0xF3, 0xEF, 0xFC, 0xFF, 0x8E, 0x00, 0xE0, 0x0E, 0x00,
|
|
|
|
|
0xE0, 0x0E, 0x00, 0x1F, 0xF3, 0xFF, 0x7C, 0x7F, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0,
|
|
|
|
|
0x7F, 0x07, 0x79, 0xF3, 0xFF, 0x1F, 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0xFF, 0xFF,
|
|
|
|
|
0xE2, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x3F, 0xBF, 0xEF, 0x33, 0x80,
|
|
|
|
|
0xF0, 0x3F, 0x03, 0xF0, 0x3E, 0x03, 0x80, 0xFE, 0x7B, 0xFE, 0xFF, 0x00, 0x70, 0x78, 0x3C, 0x1E,
|
|
|
|
|
0x0F, 0xF7, 0xFB, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F, 0x03, 0x81, 0xE4, 0xFF, 0x3F, 0x80,
|
|
|
|
|
0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0x70, 0x77, 0x07, 0x78,
|
|
|
|
|
0xF3, 0xFF, 0x1F, 0xF0, 0xE0, 0x77, 0x83, 0x9C, 0x1C, 0xE1, 0xC7, 0x8E, 0x1C, 0x70, 0xE7, 0x03,
|
|
|
|
|
0xB8, 0x1F, 0xC0, 0xFC, 0x03, 0xE0, 0x1E, 0x00, 0x70, 0x00, 0xE0, 0xE0, 0xFE, 0x1C, 0x1D, 0xC3,
|
|
|
|
|
0xC3, 0xB8, 0x78, 0xE7, 0x1F, 0x1C, 0x73, 0xF3, 0x8E, 0x6E, 0xE1, 0xDD, 0xDC, 0x1F, 0x9B, 0x83,
|
|
|
|
|
0xE3, 0xE0, 0x7C, 0x7C, 0x07, 0x87, 0x80, 0xE0, 0xE0, 0x70, 0x73, 0xC7, 0x0E, 0x78, 0x3B, 0x81,
|
|
|
|
|
0xF8, 0x07, 0x80, 0x3C, 0x01, 0xF0, 0x1F, 0xC1, 0xEF, 0x0E, 0x38, 0xE1, 0xEF, 0x07, 0x00, 0xE0,
|
|
|
|
|
0x7F, 0x07, 0x70, 0x77, 0x0E, 0x78, 0xE3, 0x8E, 0x38, 0xC1, 0xDC, 0x1D, 0xC1, 0xF8, 0x0F, 0x80,
|
|
|
|
|
0xF8, 0x07, 0x00, 0x70, 0x07, 0x09, 0xE0, 0xFC, 0x0F, 0x80, 0x7F, 0xEF, 0xFC, 0x07, 0x81, 0xE0,
|
|
|
|
|
0x78, 0x1E, 0x03, 0x80, 0xF0, 0x3C, 0x0F, 0x01, 0xC0, 0x3F, 0xF7, 0xFE, 0x0E, 0x1F, 0x3C, 0x38,
|
|
|
|
|
0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x70, 0xE0, 0xF0, 0x78, 0x38, 0x38, 0x38, 0x38, 0x38,
|
|
|
|
|
0x38, 0x38, 0x3C, 0x1F, 0x1F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0xF0,
|
|
|
|
|
0xF8, 0x78, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x18, 0x1C, 0x1E, 0x0F, 0x1F, 0x1E, 0x1C, 0x38,
|
|
|
|
|
0x38, 0x38, 0x38, 0x38, 0x38, 0x78, 0xF8, 0xF0, 0x7C, 0x3F, 0xF7, 0xEF, 0xFC, 0x3E, 0x77, 0x9C,
|
|
|
|
|
0x00, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x73, 0x80, 0x06, 0x01, 0x80, 0x60, 0x7F, 0x3F,
|
|
|
|
|
0xDF, 0x3F, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xF0, 0x1F, 0x33, 0xFC, 0x7F, 0x06, 0x01,
|
|
|
|
|
0x80, 0x60, 0x07, 0xF0, 0xFF, 0x1E, 0x61, 0xC0, 0x1C, 0x03, 0xC0, 0x3C, 0x03, 0xC0, 0xFF, 0xCF,
|
|
|
|
|
0xFC, 0x3C, 0x03, 0x80, 0x38, 0x03, 0x80, 0x38, 0x03, 0xFF, 0x3F, 0xF0, 0x60, 0x7F, 0xFF, 0x7F,
|
|
|
|
|
0xE7, 0x9E, 0x70, 0xE7, 0x06, 0x70, 0x67, 0x0E, 0x79, 0xE7, 0xFE, 0xFF, 0xF6, 0x07, 0xF0, 0x3D,
|
|
|
|
|
0xC0, 0xE7, 0x87, 0x8E, 0x1C, 0x1C, 0xE0, 0x7B, 0x80, 0xFC, 0x01, 0xF0, 0x7F, 0xF9, 0xFF, 0xE0,
|
|
|
|
|
0x38, 0x00, 0xE0, 0x7F, 0xF9, 0xFF, 0xE0, 0x38, 0x00, 0xE0, 0x03, 0x80, 0xFF, 0xFF, 0xFF, 0xFC,
|
|
|
|
|
0x00, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x3F, 0xCF, 0xF9, 0xE7, 0x38, 0x07, 0x00, 0xF8, 0x1F, 0xC7,
|
|
|
|
|
0xFE, 0xE3, 0xDC, 0x1F, 0x83, 0xF8, 0xE7, 0xFC, 0x7F, 0x01, 0xF0, 0x0E, 0x01, 0xDC, 0x7B, 0xFE,
|
|
|
|
|
0x7F, 0x80, 0x73, 0xBB, 0xDC, 0xE0, 0x07, 0xF8, 0x03, 0xFF, 0x81, 0xF3, 0xF0, 0xE0, 0x1E, 0x73,
|
|
|
|
|
0xFB, 0x9D, 0xFE, 0x7E, 0x7B, 0x1F, 0xB8, 0x07, 0xEE, 0x00, 0xFB, 0x80, 0x7E, 0x79, 0x9D, 0xDF,
|
|
|
|
|
0xE7, 0x73, 0xFB, 0x8E, 0x01, 0xE1, 0xF1, 0xF0, 0x3F, 0xF8, 0x07, 0xF8, 0x00, 0x3F, 0x1F, 0x88,
|
|
|
|
|
0xE7, 0xF7, 0xFB, 0x9D, 0xCE, 0xFF, 0x3F, 0x80, 0x0C, 0x61, 0xCF, 0x3C, 0xE3, 0x9C, 0x7B, 0xC7,
|
|
|
|
|
0x38, 0x7B, 0xC3, 0x9C, 0x3C, 0xE1, 0xCF, 0x0C, 0x60, 0xFF, 0xFF, 0xFF, 0x00, 0x70, 0x07, 0x00,
|
|
|
|
|
0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0xFF, 0xFC, 0x07, 0xF8, 0x03, 0xFF, 0x81, 0xF3, 0xF0,
|
|
|
|
|
0xE0, 0x1E, 0x77, 0xF3, 0x9D, 0xFE, 0x7E, 0x73, 0x9F, 0x9C, 0xE7, 0xE7, 0xF8, 0xF9, 0xFC, 0x7E,
|
|
|
|
|
0x77, 0x1D, 0xDC, 0xE7, 0x77, 0x1F, 0x8E, 0x01, 0xE1, 0xF1, 0xF0, 0x3F, 0xF8, 0x07, 0xF8, 0x00,
|
|
|
|
|
0xFF, 0xFF, 0x3E, 0x7E, 0xF7, 0xE3, 0xF7, 0x7E, 0x3E, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x06,
|
|
|
|
|
0x0F, 0xFF, 0xFF, 0xF0, 0x60, 0x06, 0x00, 0x60, 0x06, 0x00, 0x60, 0x00, 0x0F, 0xFF, 0xFF, 0xF0,
|
|
|
|
|
0x7E, 0x7F, 0x67, 0x07, 0x0E, 0x1C, 0x38, 0x70, 0x7F, 0x7F, 0x7E, 0x7E, 0x6F, 0x3E, 0x3E, 0x0F,
|
|
|
|
|
0x07, 0x67, 0x7F, 0x7E, 0x19, 0xDD, 0xCC, 0x00, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0F, 0xC1,
|
|
|
|
|
0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0xE7, 0xFF, 0xFF, 0xFF, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C,
|
|
|
|
|
0x00, 0x1F, 0xF9, 0xFF, 0xDF, 0xCF, 0xFE, 0x7F, 0xF3, 0xFF, 0x9F, 0xFC, 0xFF, 0xE7, 0xFF, 0x3B,
|
|
|
|
|
0xF9, 0xC7, 0xCE, 0x06, 0x70, 0x33, 0x81, 0x9C, 0x0C, 0xE0, 0x67, 0x03, 0x38, 0x19, 0xC0, 0xCE,
|
|
|
|
|
0x06, 0x70, 0x33, 0x81, 0x9C, 0xFF, 0xF0, 0x38, 0xE3, 0x87, 0x7F, 0xE0, 0x3B, 0xFF, 0xF1, 0x8C,
|
|
|
|
|
0x63, 0x18, 0xC0, 0x3E, 0x1F, 0xCF, 0x7B, 0x86, 0xC1, 0xB8, 0x6F, 0x79, 0xFC, 0x3E, 0x00, 0xC7,
|
|
|
|
|
0x1C, 0xE1, 0xCE, 0x3D, 0xE3, 0x9C, 0x7B, 0xCE, 0x73, 0xDE, 0x73, 0x9C, 0xE3, 0x1C, 0x00, 0x38,
|
|
|
|
|
0x00, 0x03, 0xC0, 0x38, 0x3E, 0x03, 0x81, 0xF0, 0x1C, 0x01, 0x81, 0xC0, 0x0C, 0x0E, 0x00, 0x60,
|
|
|
|
|
0xE0, 0x03, 0x07, 0x00, 0x18, 0x70, 0xE0, 0xC7, 0x0F, 0x00, 0x38, 0x78, 0x03, 0x87, 0xC0, 0x1C,
|
|
|
|
|
0x76, 0x01, 0xC3, 0x30, 0x0E, 0x3F, 0xE0, 0xE1, 0xFF, 0x0E, 0x00, 0x60, 0x70, 0x03, 0x00, 0x38,
|
|
|
|
|
0x00, 0x07, 0x80, 0xE0, 0xF8, 0x0E, 0x0F, 0x81, 0xC0, 0x18, 0x1C, 0x01, 0x83, 0x80, 0x18, 0x38,
|
|
|
|
|
0x01, 0x87, 0x00, 0x18, 0xE7, 0xE1, 0x8E, 0xFE, 0x01, 0xC6, 0x70, 0x1C, 0x06, 0x03, 0x80, 0xE0,
|
|
|
|
|
0x38, 0x1C, 0x07, 0x03, 0x80, 0xE0, 0x70, 0x0E, 0x07, 0xF1, 0xC0, 0x7F, 0x7E, 0x00, 0x01, 0xF8,
|
|
|
|
|
0x0E, 0x06, 0xF0, 0x38, 0x0F, 0x81, 0xC0, 0x3E, 0x07, 0x00, 0x3C, 0x38, 0x00, 0x71, 0xC0, 0x19,
|
|
|
|
|
0xC7, 0x00, 0x7F, 0x38, 0x71, 0xF8, 0xE3, 0xC0, 0x07, 0x0F, 0x00, 0x1C, 0x7C, 0x00, 0xE3, 0xB0,
|
|
|
|
|
0x07, 0x0C, 0xC0, 0x1C, 0x7F, 0xC0, 0xE1, 0xFF, 0x03, 0x80, 0x30, 0x1C, 0x00, 0xC0, 0x0E, 0x03,
|
|
|
|
|
0xC0, 0xE0, 0x00, 0x00, 0x01, 0x80, 0xE0, 0x38, 0x1E, 0x0F, 0x03, 0x81, 0xC0, 0x70, 0x3C, 0x0F,
|
|
|
|
|
0x01, 0xE6, 0x7F, 0xCF, 0xE0, 0x07, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x00, 0x38, 0x00, 0x0C, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x03, 0xE0, 0x01, 0xF0, 0x01, 0xF8, 0x00, 0xEE, 0x00, 0xE7,
|
|
|
|
|
0x00, 0x73, 0xC0, 0x78, 0xE0, 0x38, 0x78, 0x1C, 0x1C, 0x1E, 0x0E, 0x0F, 0xFF, 0x87, 0xFF, 0xC7,
|
|
|
|
|
0x00, 0xE3, 0x80, 0x7B, 0xC0, 0x1D, 0xC0, 0x0F, 0x00, 0x60, 0x00, 0x70, 0x00, 0x78, 0x00, 0x38,
|
|
|
|
|
0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x03, 0xE0, 0x01, 0xF0, 0x01, 0xF8, 0x00,
|
|
|
|
|
0xEE, 0x00, 0xE7, 0x00, 0x73, 0xC0, 0x78, 0xE0, 0x38, 0x78, 0x1C, 0x1C, 0x1E, 0x0E, 0x0F, 0xFF,
|
|
|
|
|
0x87, 0xFF, 0xC7, 0x00, 0xE3, 0x80, 0x7B, 0xC0, 0x1D, 0xC0, 0x0F, 0x01, 0x80, 0x01, 0xE0, 0x00,
|
|
|
|
|
0xF8, 0x00, 0xEE, 0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x03, 0xE0, 0x01, 0xF0,
|
|
|
|
|
0x01, 0xF8, 0x00, 0xEE, 0x00, 0xE7, 0x00, 0x73, 0xC0, 0x78, 0xE0, 0x38, 0x78, 0x1C, 0x1C, 0x1E,
|
|
|
|
|
0x0E, 0x0F, 0xFF, 0x87, 0xFF, 0xC7, 0x00, 0xE3, 0x80, 0x7B, 0xC0, 0x1D, 0xC0, 0x0F, 0x07, 0xB8,
|
|
|
|
|
0x07, 0xF8, 0x03, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0xC0, 0x03,
|
|
|
|
|
0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xC0, 0x70, 0xF0, 0x38, 0x38,
|
|
|
|
|
0x3C, 0x1C, 0x1F, 0xFF, 0x0F, 0xFF, 0x8E, 0x01, 0xC7, 0x00, 0xF7, 0x80, 0x3B, 0x80, 0x1E, 0x0E,
|
|
|
|
|
0x70, 0x07, 0xB8, 0x03, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0F, 0x80, 0x07, 0xC0,
|
|
|
|
|
0x07, 0xE0, 0x03, 0xB8, 0x03, 0x9C, 0x01, 0xCF, 0x01, 0xE3, 0x80, 0xE1, 0xE0, 0x70, 0x70, 0x78,
|
|
|
|
|
0x38, 0x3F, 0xFE, 0x1F, 0xFF, 0x1C, 0x03, 0x8E, 0x01, 0xEF, 0x00, 0x77, 0x00, 0x3C, 0x03, 0xC0,
|
|
|
|
|
0x01, 0xB0, 0x01, 0x98, 0x00, 0xEC, 0x00, 0x3E, 0x00, 0x1E, 0x00, 0x0F, 0x80, 0x0F, 0xC0, 0x07,
|
|
|
|
|
0x70, 0x07, 0x38, 0x03, 0x9E, 0x03, 0xC7, 0x01, 0xC3, 0xC0, 0xE0, 0xE0, 0xF0, 0x70, 0x7F, 0xFC,
|
|
|
|
|
0x7F, 0xFE, 0x38, 0x07, 0x1C, 0x03, 0xDE, 0x00, 0xEE, 0x00, 0x78, 0x00, 0x3F, 0xFC, 0x00, 0xFF,
|
|
|
|
|
0xF8, 0x03, 0xF8, 0x00, 0x07, 0x70, 0x00, 0x1E, 0xE0, 0x00, 0x39, 0xC0, 0x00, 0xE3, 0x80, 0x03,
|
|
|
|
|
0xC7, 0xFC, 0x07, 0x0F, 0xF8, 0x1C, 0x1C, 0x00, 0x78, 0x38, 0x00, 0xFF, 0xF0, 0x03, 0xFF, 0xE0,
|
|
|
|
|
0x07, 0x01, 0xC0, 0x1E, 0x03, 0x80, 0x38, 0x07, 0xFF, 0xE0, 0x0F, 0xFE, 0x07, 0xF8, 0x7F, 0xF3,
|
|
|
|
|
0xF3, 0xDE, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x0F, 0x00, 0x3C,
|
|
|
|
|
0x00, 0x70, 0x01, 0xE0, 0x03, 0xF1, 0xC7, 0xFF, 0x07, 0xF8, 0x06, 0x00, 0x3C, 0x00, 0x70, 0x01,
|
|
|
|
|
0xC0, 0x1F, 0x00, 0x7C, 0x00, 0x18, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x00, 0x00, 0x00, 0x0F,
|
|
|
|
|
0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xCF, 0xFC, 0xE0, 0x0E, 0x00,
|
|
|
|
|
0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x01, 0x80, 0x38, 0x07, 0x00, 0xE0, 0x0C,
|
|
|
|
|
0x00, 0x00, 0x00, 0x0F, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xCF,
|
|
|
|
|
0xFC, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x0C, 0x00, 0xE0,
|
|
|
|
|
0x1F, 0x03, 0xB8, 0x31, 0x80, 0x00, 0x00, 0x0F, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0,
|
|
|
|
|
0x0E, 0x00, 0xFF, 0xCF, 0xFC, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF,
|
|
|
|
|
0xFF, 0x3B, 0x87, 0xBC, 0x3B, 0x80, 0x00, 0x00, 0x0F, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00,
|
|
|
|
|
0xE0, 0x0E, 0x00, 0xFF, 0xCF, 0xFC, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF,
|
|
|
|
|
0xFF, 0xFF, 0xE3, 0xC7, 0x0E, 0x18, 0x00, 0x0E, 0x38, 0xE3, 0x8E, 0x38, 0xE3, 0x8E, 0x38, 0xE3,
|
|
|
|
|
0x8E, 0x38, 0xE3, 0x8E, 0x18, 0xE7, 0x9C, 0x60, 0x00, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C,
|
|
|
|
|
0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x18, 0x1C, 0x3E, 0x77, 0x63, 0x00, 0x00, 0x1C, 0x1C, 0x1C,
|
|
|
|
|
0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x67, 0x7B,
|
|
|
|
|
0xD9, 0xC0, 0x00, 0x00, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38,
|
|
|
|
|
0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x3F, 0xF0, 0x1F, 0xFE, 0x0E, 0x1F, 0x87, 0x01, 0xE3,
|
|
|
|
|
0x80, 0x71, 0xC0, 0x3C, 0xE0, 0x0F, 0xFF, 0x07, 0xFF, 0x83, 0x9C, 0x01, 0xCE, 0x00, 0xE7, 0x00,
|
|
|
|
|
0xF3, 0x80, 0x71, 0xC0, 0x78, 0xE1, 0xF8, 0x7F, 0xF8, 0x3F, 0xF0, 0x00, 0x0F, 0x60, 0x3F, 0xC0,
|
|
|
|
|
0x6F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x07, 0x78, 0x0E, 0xF8, 0x1D, 0xF8, 0x3B, 0xF0,
|
|
|
|
|
0x77, 0x70, 0xEE, 0xF1, 0xDC, 0xF3, 0xB8, 0xE7, 0x70, 0xEE, 0xE1, 0xFD, 0xC1, 0xFB, 0x81, 0xF7,
|
|
|
|
|
0x03, 0xEE, 0x03, 0xDC, 0x03, 0xB8, 0x07, 0x00, 0x03, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x00, 0x38,
|
|
|
|
|
0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x1F, 0xFC, 0x1F, 0x9F, 0x1E, 0x03, 0xCE,
|
|
|
|
|
0x00, 0xF7, 0x00, 0x7F, 0x80, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF8, 0x01, 0xDC, 0x01,
|
|
|
|
|
0xEE, 0x00, 0xF7, 0x80, 0xF1, 0xF9, 0xF0, 0x7F, 0xF0, 0x0F, 0xF0, 0x00, 0x70, 0x00, 0x38, 0x00,
|
|
|
|
|
0x38, 0x00, 0x38, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x1F, 0xFC, 0x1F, 0x9F,
|
|
|
|
|
0x1E, 0x03, 0xCE, 0x00, 0xF7, 0x00, 0x7F, 0x80, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF8,
|
|
|
|
|
0x01, 0xDC, 0x01, 0xEE, 0x00, 0xF7, 0x80, 0xF1, 0xF9, 0xF0, 0x7F, 0xF0, 0x0F, 0xF0, 0x00, 0xC0,
|
|
|
|
|
0x00, 0xF0, 0x00, 0xF8, 0x00, 0xEE, 0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x1F,
|
|
|
|
|
0xFC, 0x1F, 0x9F, 0x1E, 0x03, 0xCE, 0x00, 0xF7, 0x00, 0x7F, 0x80, 0x1F, 0x80, 0x0F, 0xC0, 0x07,
|
|
|
|
|
0xE0, 0x03, 0xF8, 0x01, 0xDC, 0x01, 0xEE, 0x00, 0xF7, 0x80, 0xF1, 0xF9, 0xF0, 0x7F, 0xF0, 0x0F,
|
|
|
|
|
0xF0, 0x07, 0xD8, 0x03, 0xFC, 0x03, 0xBC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0,
|
|
|
|
|
0x3F, 0xF8, 0x3F, 0x3E, 0x3C, 0x07, 0x9C, 0x01, 0xEE, 0x00, 0xFF, 0x00, 0x3F, 0x00, 0x1F, 0x80,
|
|
|
|
|
0x0F, 0xC0, 0x07, 0xF0, 0x03, 0xB8, 0x03, 0xDC, 0x01, 0xEF, 0x01, 0xE3, 0xF3, 0xE0, 0xFF, 0xE0,
|
|
|
|
|
0x1F, 0xE0, 0x07, 0x38, 0x03, 0xBC, 0x01, 0xCE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xC0, 0x7F,
|
|
|
|
|
0xF0, 0x7E, 0x7C, 0x78, 0x0F, 0x38, 0x03, 0xDC, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1F,
|
|
|
|
|
0x80, 0x0F, 0xE0, 0x07, 0x70, 0x07, 0xB8, 0x03, 0xDE, 0x03, 0xC7, 0xE7, 0xC1, 0xFF, 0xC0, 0x3F,
|
|
|
|
|
0xC0, 0x40, 0x9C, 0x3B, 0xCF, 0x3F, 0xC3, 0xF0, 0x3C, 0x0F, 0xC3, 0xFC, 0xF3, 0xDC, 0x39, 0x02,
|
|
|
|
|
0x00, 0x00, 0x03, 0x03, 0xFD, 0xC7, 0xFF, 0xC7, 0xE7, 0xC7, 0x81, 0xF3, 0x81, 0xF9, 0xC0, 0xFF,
|
|
|
|
|
0xE0, 0xE7, 0xE0, 0xE3, 0xF0, 0xE1, 0xF8, 0xF0, 0xFE, 0xF0, 0x77, 0x70, 0x3B, 0xF0, 0x3D, 0xF0,
|
|
|
|
|
0x3C, 0x7E, 0x7C, 0x7F, 0xFC, 0x3B, 0xFC, 0x18, 0x00, 0x00, 0x0C, 0x00, 0x38, 0x00, 0xF0, 0x01,
|
|
|
|
|
0xC0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0,
|
|
|
|
|
0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xB8, 0x0E, 0xF0, 0x79, 0xF3,
|
|
|
|
|
0xC3, 0xFF, 0x07, 0xF0, 0x01, 0xC0, 0x0F, 0x00, 0x38, 0x01, 0xC0, 0x06, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x38, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F,
|
|
|
|
|
0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xB8, 0x0E, 0xF0, 0x79, 0xF3, 0xC3, 0xFF, 0x07, 0xF0, 0x03, 0x00,
|
|
|
|
|
0x1C, 0x00, 0xF8, 0x07, 0x70, 0x18, 0xC0, 0x00, 0x00, 0x00, 0x38, 0x0F, 0xE0, 0x3F, 0x80, 0xFE,
|
|
|
|
|
0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xB8,
|
|
|
|
|
0x0E, 0xF0, 0x79, 0xF3, 0xC3, 0xFF, 0x07, 0xF0, 0x1D, 0xC0, 0xF7, 0x81, 0xDC, 0x00, 0x00, 0x00,
|
|
|
|
|
0x03, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80,
|
|
|
|
|
0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3B, 0x80, 0xEF, 0x07, 0x9F, 0x3C, 0x3F, 0xF0, 0x7F, 0x00, 0x00,
|
|
|
|
|
0xC0, 0x03, 0xC0, 0x07, 0x00, 0x1C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x78, 0x0F, 0x70, 0x1C,
|
|
|
|
|
0xF0, 0x78, 0xE0, 0xE1, 0xE3, 0xC1, 0xEF, 0x01, 0xDC, 0x03, 0xF8, 0x03, 0xE0, 0x03, 0x80, 0x07,
|
|
|
|
|
0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0xE0, 0x07, 0x00, 0x38,
|
|
|
|
|
0x01, 0xFF, 0x0F, 0xFE, 0x71, 0xFB, 0x81, 0xDC, 0x0F, 0xE0, 0x7F, 0x03, 0xF8, 0x1D, 0xC7, 0xEF,
|
|
|
|
|
0xFE, 0x7F, 0xE3, 0x80, 0x1C, 0x00, 0xE0, 0x00, 0x3F, 0x83, 0xFE, 0x3E, 0xF9, 0xC1, 0xCE, 0x0E,
|
|
|
|
|
0x70, 0xE3, 0x87, 0x1C, 0x70, 0xE7, 0x07, 0x38, 0x39, 0xE1, 0xCF, 0xCE, 0x3F, 0x70, 0x7F, 0x80,
|
|
|
|
|
0xFC, 0x07, 0xEE, 0x7F, 0x7F, 0xBB, 0xF8, 0x18, 0x03, 0x80, 0x78, 0x07, 0x00, 0x60, 0x00, 0x00,
|
|
|
|
|
0x03, 0xFC, 0x7F, 0xCC, 0x78, 0x07, 0x00, 0x73, 0xFE, 0xFF, 0xFE, 0x7F, 0x07, 0xE0, 0xFE, 0x3F,
|
|
|
|
|
0xFF, 0x9F, 0xE0, 0x03, 0x00, 0xF0, 0x1C, 0x07, 0x00, 0xC0, 0x00, 0x00, 0x03, 0xFC, 0x7F, 0xCC,
|
|
|
|
|
0x78, 0x07, 0x00, 0x73, 0xFE, 0xFF, 0xFE, 0x7F, 0x07, 0xE0, 0xFE, 0x3F, 0xFF, 0x9F, 0xE0, 0x04,
|
|
|
|
|
0x01, 0xC0, 0x7C, 0x1D, 0xC3, 0x18, 0x00, 0x00, 0x03, 0xFC, 0x7F, 0xCC, 0x78, 0x07, 0x00, 0x73,
|
|
|
|
|
0xFE, 0xFF, 0xFE, 0x7F, 0x07, 0xE0, 0xFE, 0x3F, 0xFF, 0x9F, 0xE0, 0x3E, 0xC7, 0xF9, 0xDE, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x1F, 0xE3, 0xFE, 0x63, 0xC0, 0x38, 0x03, 0x9F, 0xF7, 0xFF, 0xF3, 0xF8, 0x3F,
|
|
|
|
|
0x07, 0xF1, 0xFF, 0xFC, 0xFF, 0x00, 0x3B, 0x8F, 0x78, 0xEE, 0x00, 0x00, 0x00, 0xFF, 0x1F, 0xF3,
|
|
|
|
|
0x1E, 0x01, 0xC0, 0x1C, 0xFF, 0xBF, 0xFF, 0x9F, 0xC1, 0xF8, 0x3F, 0x8F, 0xFF, 0xE7, 0xF8, 0x1F,
|
|
|
|
|
0x03, 0x60, 0x6C, 0x0D, 0x81, 0xF0, 0x00, 0x00, 0x03, 0xFC, 0x7F, 0xCC, 0x78, 0x07, 0x00, 0x73,
|
|
|
|
|
0xFE, 0xFF, 0xFE, 0x7F, 0x07, 0xE0, 0xFE, 0x3F, 0xFF, 0x9F, 0xE0, 0x7F, 0x1F, 0x87, 0xFB, 0xFC,
|
|
|
|
|
0x63, 0xFB, 0xE0, 0x1E, 0x0E, 0x00, 0xE0, 0xE3, 0xFF, 0xFE, 0x7F, 0xFF, 0xEF, 0x9E, 0x00, 0xE0,
|
|
|
|
|
0xE0, 0x0E, 0x0F, 0x00, 0xF1, 0xF8, 0xCF, 0xFF, 0xFC, 0x3F, 0x9F, 0xC0, 0x1F, 0xC7, 0xF9, 0xF3,
|
|
|
|
|
0x38, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1E, 0x01, 0xF3, 0x1F, 0xE1, 0xFC, 0x0C, 0x01,
|
|
|
|
|
0xC0, 0x38, 0x07, 0x03, 0xE0, 0x7C, 0x00, 0x1C, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x00, 0x00,
|
|
|
|
|
0x00, 0x01, 0xF8, 0x3F, 0xE7, 0x9E, 0xF0, 0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xF0,
|
|
|
|
|
0x07, 0xC6, 0x3F, 0xE1, 0xFE, 0x01, 0x80, 0x38, 0x07, 0x00, 0xE0, 0x06, 0x00, 0x00, 0x00, 0x01,
|
|
|
|
|
0xF8, 0x3F, 0xE7, 0x9E, 0xF0, 0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xF0, 0x07, 0xC6,
|
|
|
|
|
0x3F, 0xE1, 0xFE, 0x06, 0x00, 0xF0, 0x1F, 0x81, 0x9C, 0x18, 0x80, 0x00, 0x00, 0x01, 0xF8, 0x3F,
|
|
|
|
|
0xE7, 0x9E, 0xF0, 0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xF0, 0x07, 0xC6, 0x3F, 0xE1,
|
|
|
|
|
0xFE, 0x39, 0xC3, 0xDC, 0x39, 0xC0, 0x00, 0x00, 0x01, 0xF8, 0x3F, 0xE7, 0x9E, 0xF0, 0xFE, 0x07,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xF0, 0x07, 0xC6, 0x3F, 0xE1, 0xFE, 0xE7, 0x1C, 0x71, 0x80,
|
|
|
|
|
0x07, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39, 0xCE, 0x70, 0x18, 0xE7, 0x38, 0x60, 0x00, 0x1C, 0x71,
|
|
|
|
|
0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x18, 0x3C, 0x7E, 0x67, 0x62, 0x00, 0x00, 0x1C,
|
|
|
|
|
0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0xE7, 0xF7, 0xE7, 0x00,
|
|
|
|
|
0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x03, 0x00,
|
|
|
|
|
0x1E, 0xE0, 0x3F, 0x81, 0xF8, 0x07, 0xE0, 0x01, 0xC0, 0x07, 0x07, 0xFC, 0x7F, 0xF9, 0xE7, 0xEF,
|
|
|
|
|
0x03, 0xB8, 0x0E, 0xE0, 0x7B, 0x81, 0xCE, 0x07, 0x3C, 0x1C, 0x79, 0xE0, 0xFF, 0x81, 0xF8, 0x00,
|
|
|
|
|
0x3D, 0xCF, 0xF1, 0xBE, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xE7, 0xFE, 0xE3, 0xDC, 0x3F, 0x83, 0xF0,
|
|
|
|
|
0x7E, 0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0x80, 0x1C, 0x00, 0xF0, 0x03, 0x80,
|
|
|
|
|
0x0E, 0x00, 0x30, 0x00, 0x00, 0x00, 0x03, 0xF8, 0x3F, 0xE3, 0xCF, 0x9C, 0x1D, 0xC0, 0xFE, 0x03,
|
|
|
|
|
0xF0, 0x1F, 0x80, 0xFC, 0x0F, 0x70, 0x73, 0xCF, 0x8F, 0xF8, 0x3F, 0x80, 0x01, 0x80, 0x1C, 0x01,
|
|
|
|
|
0xE0, 0x0E, 0x00, 0x60, 0x00, 0x00, 0x00, 0x03, 0xF8, 0x3F, 0xE3, 0xCF, 0x9C, 0x1D, 0xC0, 0xFE,
|
|
|
|
|
0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x0F, 0x70, 0x73, 0xCF, 0x8F, 0xF8, 0x3F, 0x80, 0x06, 0x00, 0x38,
|
|
|
|
|
0x03, 0xE0, 0x3B, 0x81, 0x8C, 0x00, 0x00, 0x00, 0x03, 0xF8, 0x3F, 0xE3, 0xCF, 0x9C, 0x1D, 0xC0,
|
|
|
|
|
0xFE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x0F, 0x70, 0x73, 0xCF, 0x8F, 0xF8, 0x3F, 0x80, 0x1E, 0xE1,
|
|
|
|
|
0xFE, 0x0D, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7F, 0x07, 0xFC, 0x79, 0xF3, 0x83, 0xB8, 0x1F,
|
|
|
|
|
0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x81, 0xEE, 0x0E, 0x79, 0xF1, 0xFF, 0x07, 0xF0, 0x19, 0xC1, 0xEF,
|
|
|
|
|
0x06, 0x70, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0xFF, 0x8F, 0x3E, 0x70, 0x77, 0x03, 0xF8, 0x0F, 0xC0,
|
|
|
|
|
0x7E, 0x03, 0xF0, 0x3D, 0xC1, 0xCF, 0x3E, 0x3F, 0xE0, 0xFE, 0x00, 0x0F, 0x00, 0xF0, 0x0F, 0x00,
|
|
|
|
|
0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0xF0, 0x0F, 0x00, 0xF0, 0x00, 0x30, 0xFF,
|
|
|
|
|
0x8F, 0xFC, 0xF3, 0xEF, 0x1F, 0x70, 0xFF, 0x8E, 0xFC, 0xE7, 0xEE, 0x3F, 0xE3, 0xDE, 0x1C, 0xFB,
|
|
|
|
|
0xE7, 0xFE, 0x7F, 0xE3, 0x00, 0x00, 0x18, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x00, 0x00, 0x00,
|
|
|
|
|
0x0F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x77, 0x07, 0x70, 0x77,
|
|
|
|
|
0x8F, 0x3F, 0xF1, 0xFF, 0x01, 0x80, 0x38, 0x07, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0x00, 0x0F, 0x07,
|
|
|
|
|
0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x77, 0x07, 0x70, 0x77, 0x8F, 0x3F,
|
|
|
|
|
0xF1, 0xFF, 0x06, 0x00, 0xF0, 0x1F, 0x81, 0x9C, 0x18, 0x80, 0x00, 0x00, 0x0F, 0x07, 0xF0, 0x7F,
|
|
|
|
|
0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x77, 0x07, 0x70, 0x77, 0x8F, 0x3F, 0xF1, 0xFF,
|
|
|
|
|
0x39, 0xC3, 0xDC, 0x39, 0xC0, 0x00, 0x00, 0x0F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0,
|
|
|
|
|
0x7F, 0x07, 0xF0, 0x77, 0x07, 0x70, 0x77, 0x8F, 0x3F, 0xF1, 0xFF, 0x01, 0x80, 0x3C, 0x03, 0x80,
|
|
|
|
|
0x70, 0x06, 0x00, 0x00, 0x00, 0x0E, 0x07, 0xF0, 0x77, 0x07, 0x70, 0xE7, 0x8E, 0x38, 0xE3, 0x8C,
|
|
|
|
|
0x1D, 0xC1, 0xDC, 0x1F, 0x80, 0xF8, 0x0F, 0x80, 0x70, 0x07, 0x00, 0x70, 0x9E, 0x0F, 0xC0, 0xF8,
|
|
|
|
|
0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0x8F, 0xFC, 0xF3, 0xEE, 0x0E,
|
|
|
|
|
0xE0, 0xFE, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0xFE, 0x0E, 0xF3, 0xEF, 0xFC, 0xFF, 0x8E, 0x00, 0xE0,
|
|
|
|
|
0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x1D, 0xC3, 0xDE, 0x1D, 0xC0, 0x00, 0x00, 0x0E, 0x07, 0xF0, 0x77,
|
|
|
|
|
0x07, 0x70, 0xE7, 0x8E, 0x38, 0xE3, 0x8C, 0x1D, 0xC1, 0xDC, 0x1F, 0x80, 0xF8, 0x0F, 0x80, 0x70,
|
|
|
|
|
0x07, 0x00, 0x70, 0x9E, 0x0F, 0xC0, 0xF8, 0x00, 0x0F, 0xF0, 0x07, 0xF8, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x0E, 0x00, 0x0F, 0x80, 0x07, 0xC0, 0x07, 0xE0, 0x03, 0xB8, 0x03, 0x9C, 0x01,
|
|
|
|
|
0xCF, 0x01, 0xE3, 0x80, 0xE1, 0xE0, 0x70, 0x70, 0x78, 0x38, 0x3F, 0xFE, 0x1F, 0xFF, 0x1C, 0x03,
|
|
|
|
|
0x8E, 0x01, 0xEF, 0x00, 0x77, 0x00, 0x3C, 0x3F, 0xC7, 0xF8, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x1F,
|
|
|
|
|
0xF3, 0x1E, 0x01, 0xC0, 0x1C, 0xFF, 0xBF, 0xFF, 0x9F, 0xC1, 0xF8, 0x3F, 0x8F, 0xFF, 0xE7, 0xF8,
|
|
|
|
|
0x06, 0x30, 0x03, 0xB8, 0x01, 0xF8, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07,
|
|
|
|
|
0xC0, 0x03, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xC0, 0x70, 0xF0,
|
|
|
|
|
0x38, 0x38, 0x3C, 0x1C, 0x1F, 0xFF, 0x0F, 0xFF, 0x8E, 0x01, 0xC7, 0x00, 0xF7, 0x80, 0x3B, 0x80,
|
|
|
|
|
0x1E, 0x31, 0x87, 0x70, 0x7E, 0x0F, 0x80, 0x00, 0x00, 0x1F, 0xE3, 0xFE, 0x63, 0xC0, 0x38, 0x03,
|
|
|
|
|
0x9F, 0xF7, 0xFF, 0xF3, 0xF8, 0x3F, 0x07, 0xF1, 0xFF, 0xFC, 0xFF, 0x00, 0x01, 0xC0, 0x01, 0xF0,
|
|
|
|
|
0x00, 0xF8, 0x00, 0xFC, 0x00, 0x77, 0x00, 0x73, 0x80, 0x39, 0xE0, 0x3C, 0x70, 0x1C, 0x3C, 0x0E,
|
|
|
|
|
0x0E, 0x0F, 0x07, 0x07, 0xFF, 0xC3, 0xFF, 0xE3, 0x80, 0x71, 0xC0, 0x3D, 0xE0, 0x0E, 0xE0, 0x07,
|
|
|
|
|
0x80, 0x03, 0x80, 0x03, 0x80, 0x01, 0xC0, 0x00, 0xF8, 0x00, 0x7C, 0x7F, 0x8F, 0xF9, 0x8F, 0x00,
|
|
|
|
|
0xE0, 0x0E, 0x7F, 0xDF, 0xFF, 0xCF, 0xE0, 0xFC, 0x1F, 0xC7, 0xFF, 0xF3, 0xFC, 0x07, 0x80, 0xE0,
|
|
|
|
|
0x3C, 0x03, 0xC0, 0x7C, 0x00, 0x60, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x01, 0x80, 0x00, 0x00, 0x00,
|
|
|
|
|
0x01, 0xFE, 0x1F, 0xFC, 0xFC, 0xF7, 0x80, 0x1E, 0x00, 0x70, 0x03, 0xC0, 0x0E, 0x00, 0x38, 0x00,
|
|
|
|
|
0xE0, 0x03, 0xC0, 0x0F, 0x00, 0x1C, 0x00, 0x78, 0x00, 0xFC, 0x71, 0xFF, 0xC1, 0xFE, 0x01, 0x80,
|
|
|
|
|
0x70, 0x1C, 0x07, 0x00, 0x60, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0xCF, 0x99, 0xC0, 0x70, 0x0E, 0x01,
|
|
|
|
|
0xC0, 0x38, 0x07, 0x00, 0xF0, 0x0F, 0x98, 0xFF, 0x0F, 0xE0, 0x01, 0x80, 0x0F, 0x00, 0x7E, 0x01,
|
|
|
|
|
0x9C, 0x06, 0x20, 0x00, 0x00, 0x00, 0x01, 0xFE, 0x1F, 0xFC, 0xFC, 0xF7, 0x80, 0x1E, 0x00, 0x70,
|
|
|
|
|
0x03, 0xC0, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0xC0, 0x0F, 0x00, 0x1C, 0x00, 0x78, 0x00, 0xFC,
|
|
|
|
|
0x71, 0xFF, 0xC1, 0xFE, 0x06, 0x01, 0xE0, 0x7E, 0x0C, 0xE1, 0x88, 0x00, 0x00, 0x00, 0xFE, 0x3F,
|
|
|
|
|
0xCF, 0x99, 0xC0, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xF0, 0x0F, 0x98, 0xFF, 0x0F, 0xE0,
|
|
|
|
|
0x01, 0xC0, 0x0F, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE1, 0xFF, 0xCF, 0xCF, 0x78, 0x01,
|
|
|
|
|
0xE0, 0x07, 0x00, 0x3C, 0x00, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x3C, 0x00, 0xF0, 0x01, 0xC0, 0x07,
|
|
|
|
|
0x80, 0x0F, 0xC7, 0x1F, 0xFC, 0x1F, 0xE0, 0x07, 0x01, 0xE0, 0x1C, 0x00, 0x00, 0x00, 0x3F, 0x8F,
|
|
|
|
|
0xF3, 0xE6, 0x70, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3C, 0x03, 0xE6, 0x3F, 0xC3, 0xF8,
|
|
|
|
|
0x06, 0x20, 0x19, 0xC0, 0x7E, 0x00, 0xF0, 0x01, 0x80, 0x00, 0x00, 0x00, 0x01, 0xFE, 0x1F, 0xFC,
|
|
|
|
|
0xFC, 0xF7, 0x80, 0x1E, 0x00, 0x70, 0x03, 0xC0, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0xC0, 0x0F,
|
|
|
|
|
0x00, 0x1C, 0x00, 0x78, 0x00, 0xFC, 0x71, 0xFF, 0xC1, 0xFE, 0x18, 0x83, 0x38, 0x7E, 0x07, 0x80,
|
|
|
|
|
0x60, 0x00, 0x00, 0x00, 0xFE, 0x3F, 0xCF, 0x99, 0xC0, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00,
|
|
|
|
|
0xF0, 0x0F, 0x98, 0xFF, 0x0F, 0xE0, 0x18, 0xC0, 0x3B, 0x80, 0x3E, 0x00, 0x78, 0x00, 0x60, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x7F, 0xE0, 0xFF, 0xE1, 0xC3, 0xF3, 0x81, 0xE7, 0x01, 0xEE, 0x01, 0xDC, 0x03,
|
|
|
|
|
0xF8, 0x07, 0xF0, 0x0F, 0xE0, 0x1F, 0xC0, 0x3F, 0x80, 0x77, 0x01, 0xEE, 0x07, 0x9C, 0x3F, 0x3F,
|
|
|
|
|
0xF8, 0x7F, 0xE0, 0x00, 0x77, 0x00, 0x77, 0x00, 0x77, 0x00, 0x77, 0x00, 0x77, 0x00, 0x76, 0x1F,
|
|
|
|
|
0xF0, 0x3F, 0xF0, 0x79, 0xF0, 0xF0, 0x70, 0xE0, 0x70, 0xE0, 0x70, 0xE0, 0x70, 0xE0, 0x70, 0xE0,
|
|
|
|
|
0x70, 0xF0, 0x70, 0x7C, 0x70, 0x3F, 0xF0, 0x1F, 0xF0, 0x3F, 0xF0, 0x1F, 0xFE, 0x0E, 0x1F, 0x87,
|
|
|
|
|
0x01, 0xE3, 0x80, 0x71, 0xC0, 0x3C, 0xE0, 0x0F, 0xFF, 0x07, 0xFF, 0x83, 0x9C, 0x01, 0xCE, 0x00,
|
|
|
|
|
0xE7, 0x00, 0xF3, 0x80, 0x71, 0xC0, 0x78, 0xE1, 0xF8, 0x7F, 0xF8, 0x3F, 0xF0, 0x00, 0x00, 0x70,
|
|
|
|
|
0x01, 0xC0, 0x7F, 0xC1, 0xFF, 0x00, 0x70, 0x01, 0xC1, 0xFF, 0x0F, 0xFC, 0x79, 0xF3, 0xC1, 0xCE,
|
|
|
|
|
0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x3C, 0x1C, 0x7C, 0x70, 0xFF, 0xC1, 0xFF, 0x00,
|
|
|
|
|
0x7F, 0x87, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0,
|
|
|
|
|
0x0E, 0x00, 0xFF, 0xCF, 0xFC, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF,
|
|
|
|
|
0xFF, 0x3F, 0xC3, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x3F, 0xE7, 0x9E, 0xF0, 0xFE, 0x07,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xF0, 0x07, 0xC6, 0x3F, 0xE1, 0xFE, 0x31, 0x83, 0xB8, 0x3F,
|
|
|
|
|
0x01, 0xE0, 0x00, 0x00, 0x00, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F,
|
|
|
|
|
0xFC, 0xFF, 0xCE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFF, 0xFF, 0xF0, 0x39,
|
|
|
|
|
0xC1, 0xDC, 0x1F, 0x80, 0xF0, 0x00, 0x00, 0x00, 0x1F, 0x83, 0xFE, 0x79, 0xEF, 0x0F, 0xE0, 0x7F,
|
|
|
|
|
0xFF, 0xFF, 0xFE, 0x00, 0xE0, 0x0F, 0x00, 0x7C, 0x63, 0xFE, 0x1F, 0xE0, 0x0E, 0x00, 0xE0, 0x0E,
|
|
|
|
|
0x00, 0x00, 0x00, 0x0F, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xCF,
|
|
|
|
|
0xFC, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x07, 0x00, 0xF0,
|
|
|
|
|
0x07, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x3F, 0xE7, 0x9E, 0xF0, 0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xE0,
|
|
|
|
|
0x0E, 0x00, 0xF0, 0x07, 0xC6, 0x3F, 0xE1, 0xFE, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E,
|
|
|
|
|
0x00, 0xE0, 0x0F, 0xFC, 0xFF, 0xCE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFF,
|
|
|
|
|
0xFF, 0xF0, 0x0E, 0x01, 0xC0, 0x1C, 0x01, 0xF0, 0x1F, 0x1F, 0x83, 0xFE, 0x79, 0xEF, 0x0F, 0xE0,
|
|
|
|
|
0x7F, 0xFF, 0xFF, 0xFE, 0x00, 0xE0, 0x0F, 0x00, 0x7C, 0x63, 0xFE, 0x1F, 0xE0, 0x1C, 0x03, 0x80,
|
|
|
|
|
0x38, 0x03, 0xE0, 0x3E, 0x31, 0x03, 0x38, 0x1F, 0x01, 0xE0, 0x0C, 0x00, 0x00, 0x00, 0x0F, 0xFE,
|
|
|
|
|
0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xCF, 0xFC, 0xE0, 0x0E, 0x00, 0xE0,
|
|
|
|
|
0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x18, 0x81, 0x9C, 0x1F, 0x80, 0xF0, 0x06, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x1F, 0x83, 0xFE, 0x79, 0xEF, 0x0F, 0xE0, 0x7F, 0xFF, 0xFF, 0xFE, 0x00,
|
|
|
|
|
0xE0, 0x0F, 0x00, 0x7C, 0x63, 0xFE, 0x1F, 0xE0, 0x01, 0x80, 0x03, 0x80, 0x0F, 0x80, 0x3B, 0x80,
|
|
|
|
|
0x63, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x1F, 0xFC, 0x7E, 0x79, 0xE0, 0x03, 0xC0, 0x07, 0x00,
|
|
|
|
|
0x1E, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x1F, 0xE0, 0x3F, 0xC0, 0x7B, 0x80, 0xF7, 0x81, 0xE7,
|
|
|
|
|
0xE3, 0xC7, 0xFF, 0x83, 0xFE, 0x02, 0x00, 0x70, 0x0F, 0x81, 0xDC, 0x18, 0xC0, 0x00, 0x00, 0x01,
|
|
|
|
|
0xFF, 0x3F, 0xF7, 0xCF, 0x70, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xF0, 0x77, 0xDF,
|
|
|
|
|
0x3F, 0xF1, 0xFF, 0x00, 0x70, 0x0F, 0x71, 0xE7, 0xFE, 0x7F, 0x80, 0x06, 0x30, 0x0E, 0xE0, 0x1F,
|
|
|
|
|
0x80, 0x1F, 0x00, 0x00, 0x00, 0x00, 0x01, 0xFE, 0x0F, 0xFE, 0x3F, 0x3C, 0xF0, 0x01, 0xE0, 0x03,
|
|
|
|
|
0x80, 0x0F, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x0F, 0xF0, 0x1F, 0xE0, 0x3D, 0xC0, 0x7B, 0xC0,
|
|
|
|
|
0xF3, 0xF1, 0xE3, 0xFF, 0xC1, 0xFF, 0x00, 0x18, 0xC1, 0xDC, 0x0F, 0xC0, 0xF8, 0x00, 0x00, 0x00,
|
|
|
|
|
0x1F, 0xF3, 0xFF, 0x7C, 0xF7, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x7D,
|
|
|
|
|
0xF3, 0xFF, 0x1F, 0xF0, 0x07, 0x00, 0xF7, 0x1E, 0x7F, 0xE7, 0xF8, 0x01, 0xC0, 0x03, 0x80, 0x07,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0xFF, 0x1F, 0x9E, 0x78, 0x00, 0xF0, 0x01, 0xC0, 0x07,
|
|
|
|
|
0x80, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x07, 0xF8, 0x0F, 0xF0, 0x1E, 0xE0, 0x3D, 0xE0, 0x79, 0xF8,
|
|
|
|
|
0xF1, 0xFF, 0xE0, 0xFF, 0x80, 0x07, 0x00, 0x70, 0x07, 0x00, 0x00, 0x00, 0x01, 0xFF, 0x3F, 0xF7,
|
|
|
|
|
0xCF, 0x70, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xF0, 0x77, 0xDF, 0x3F, 0xF1, 0xFF,
|
|
|
|
|
0x00, 0x70, 0x0F, 0x71, 0xE7, 0xFE, 0x7F, 0x80, 0x07, 0xF8, 0x3F, 0xF8, 0xFC, 0xF3, 0xC0, 0x07,
|
|
|
|
|
0x80, 0x0E, 0x00, 0x3C, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x3F, 0xC0, 0x7F, 0x80, 0xF7, 0x01,
|
|
|
|
|
0xEF, 0x03, 0xCF, 0xC7, 0x8F, 0xFF, 0x07, 0xFC, 0x00, 0x00, 0x07, 0x00, 0x3E, 0x00, 0x78, 0x00,
|
|
|
|
|
0x03, 0x80, 0x38, 0x07, 0x00, 0x70, 0x00, 0x00, 0x00, 0x1F, 0xF3, 0xFF, 0x7C, 0xF7, 0x07, 0xE0,
|
|
|
|
|
0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7F, 0x07, 0x7D, 0xF3, 0xFF, 0x1F, 0xF0, 0x07, 0x00, 0xF7,
|
|
|
|
|
0x1E, 0x7F, 0xE7, 0xF8, 0x03, 0x00, 0x1E, 0x00, 0xF8, 0x07, 0x30, 0x18, 0xC0, 0x00, 0x00, 0x00,
|
|
|
|
|
0x38, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7F, 0xFF, 0xFF, 0xFF,
|
|
|
|
|
0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0x30, 0x07,
|
|
|
|
|
0xC0, 0xFE, 0x0C, 0xC0, 0x00, 0x00, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00,
|
|
|
|
|
0x7F, 0xC7, 0xFE, 0x79, 0xE7, 0x0F, 0x70, 0x77, 0x07, 0x70, 0x77, 0x07, 0x70, 0x77, 0x07, 0x70,
|
|
|
|
|
0x77, 0x07, 0x70, 0x70, 0x38, 0x07, 0x0E, 0x01, 0xCF, 0xFF, 0xFF, 0xFF, 0xFF, 0x38, 0x07, 0x0E,
|
|
|
|
|
0x01, 0xC3, 0x80, 0x70, 0xFF, 0xFC, 0x3F, 0xFF, 0x0E, 0x01, 0xC3, 0x80, 0x70, 0xE0, 0x1C, 0x38,
|
|
|
|
|
0x07, 0x0E, 0x01, 0xC3, 0x80, 0x70, 0xE0, 0x1C, 0x38, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x3F, 0xE1,
|
|
|
|
|
0xFF, 0x03, 0x80, 0x1C, 0x00, 0xFF, 0x87, 0xFE, 0x3C, 0xF1, 0xC3, 0xCE, 0x0E, 0x70, 0x73, 0x83,
|
|
|
|
|
0x9C, 0x1C, 0xE0, 0xE7, 0x07, 0x38, 0x39, 0xC1, 0xCE, 0x0E, 0x7B, 0xFF, 0xB7, 0xC0, 0x00, 0x00,
|
|
|
|
|
0x00, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03,
|
|
|
|
|
0x81, 0xC0, 0xE0, 0x70, 0x7B, 0x7F, 0xB7, 0xC0, 0x00, 0x00, 0x00, 0x70, 0x38, 0x1C, 0x0E, 0x07,
|
|
|
|
|
0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x00, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x1C,
|
|
|
|
|
0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C,
|
|
|
|
|
0xFF, 0xFF, 0x00, 0x00, 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C,
|
|
|
|
|
0x1C, 0x1C, 0xC7, 0xDF, 0xF3, 0xE0, 0x00, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C,
|
|
|
|
|
0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x00, 0x63, 0x77, 0x7E, 0x3C, 0x00, 0x00, 0x1C, 0x1C, 0x1C,
|
|
|
|
|
0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x39, 0xCE, 0x73, 0x9C, 0xE7, 0x39,
|
|
|
|
|
0xCE, 0x73, 0x9C, 0xE7, 0x3B, 0x99, 0xCF, 0xBC, 0x7B, 0xC0, 0x00, 0x1C, 0xE7, 0x39, 0xCE, 0x73,
|
|
|
|
|
0x9C, 0xE7, 0x39, 0xDD, 0xCE, 0x7F, 0xE0, 0x77, 0x70, 0x07, 0x77, 0x77, 0x77, 0x77, 0x77, 0x77,
|
|
|
|
|
0x77, 0x77, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0,
|
|
|
|
|
0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0, 0x0F, 0xE0,
|
|
|
|
|
0x0F, 0xE0, 0x0E, 0xE7, 0x3E, 0xE7, 0xFC, 0xE3, 0xF8, 0xF1, 0xDE, 0x38, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0xE3, 0x9C, 0x73, 0x8E, 0x71, 0xCE, 0x39, 0xC7, 0x38, 0xE7, 0x1C, 0xE3, 0x9C, 0x73, 0x8E, 0x71,
|
|
|
|
|
0xCE, 0x38, 0x07, 0x00, 0xE0, 0x3C, 0x1F, 0x07, 0xC0, 0x00, 0xC0, 0x0F, 0x00, 0x7C, 0x06, 0x70,
|
|
|
|
|
0x31, 0x80, 0x00, 0x00, 0x00, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70,
|
|
|
|
|
0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x1E, 0x73, 0xE3, 0xFF, 0x0F, 0xE0,
|
|
|
|
|
0x0C, 0x0F, 0x0F, 0xC6, 0x73, 0x10, 0x00, 0x00, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
|
|
|
|
|
0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0xF0, 0xF1, 0xF8, 0xF8, 0x00, 0xE0, 0x7B, 0x83,
|
|
|
|
|
0xCE, 0x1E, 0x38, 0xF0, 0xE7, 0x83, 0xBC, 0x0F, 0xE0, 0x3F, 0x00, 0xFE, 0x03, 0xFC, 0x0E, 0xF8,
|
|
|
|
|
0x39, 0xF0, 0xE3, 0xE3, 0x87, 0xCE, 0x0F, 0x38, 0x1E, 0xE0, 0x3C, 0x00, 0x00, 0x70, 0x07, 0xC0,
|
|
|
|
|
0x1E, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x77, 0x1C, 0xEF, 0x1F, 0xC3,
|
|
|
|
|
0xF0, 0x7C, 0x0F, 0xC1, 0xFC, 0x3B, 0xC7, 0x3C, 0xE3, 0xDC, 0x3B, 0x87, 0x80, 0x01, 0xC0, 0x78,
|
|
|
|
|
0x0F, 0x00, 0xE1, 0xEE, 0x3C, 0xE7, 0x8E, 0xF0, 0xFE, 0x0F, 0xC0, 0xFC, 0x0F, 0xE0, 0xEF, 0x0E,
|
|
|
|
|
0x78, 0xE3, 0xCE, 0x1C, 0xE0, 0xE0, 0x18, 0x07, 0x01, 0xC0, 0x70, 0x0C, 0x00, 0x00, 0x00, 0x07,
|
|
|
|
|
0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80,
|
|
|
|
|
0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xFF, 0xFF, 0x19, 0xEF, 0x80, 0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C,
|
|
|
|
|
0x71, 0xC7, 0x1C, 0x71, 0xC7, 0x1C, 0x79, 0xF3, 0x80, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01,
|
|
|
|
|
0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0xFF, 0xFF,
|
|
|
|
|
0xE0, 0x00, 0x38, 0x1F, 0x03, 0xC0, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1,
|
|
|
|
|
0xC3, 0x87, 0x0E, 0x1C, 0x3C, 0x7C, 0x70, 0x03, 0x8F, 0x1C, 0x00, 0xEE, 0x1D, 0xC3, 0xB8, 0x77,
|
|
|
|
|
0x0E, 0xE1, 0xDC, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07,
|
|
|
|
|
0xFF, 0xFF, 0xE0, 0x77, 0x76, 0x76, 0x76, 0x76, 0x76, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70, 0x70,
|
|
|
|
|
0x70, 0x70, 0x70, 0x78, 0x7C, 0x38, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07,
|
|
|
|
|
0x1C, 0xE3, 0xDC, 0x73, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0xFF, 0xFF, 0xE0, 0x70, 0x38,
|
|
|
|
|
0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x3B, 0x9D, 0xEE, 0xE7, 0x03, 0x81, 0xC0, 0xE0,
|
|
|
|
|
0x78, 0x3E, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x90, 0x07,
|
|
|
|
|
0xF0, 0x0F, 0xC0, 0x3E, 0x00, 0xF8, 0x01, 0xF0, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x07, 0x00,
|
|
|
|
|
0x0F, 0xFE, 0x1F, 0xFC, 0x38, 0x38, 0x38, 0x38, 0x38, 0x38, 0x3A, 0x3E, 0x3E, 0xF8, 0xF8, 0x38,
|
|
|
|
|
0x38, 0x38, 0x38, 0x38, 0x3C, 0x3E, 0x1E, 0x00, 0xC0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0x70,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x70, 0x0E, 0xF0, 0x1D, 0xF0, 0x3B, 0xF0, 0x77, 0xE0, 0xEE, 0xE1, 0xDD,
|
|
|
|
|
0xE3, 0xB9, 0xE7, 0x71, 0xCE, 0xE1, 0xDD, 0xC3, 0xFB, 0x83, 0xF7, 0x03, 0xEE, 0x07, 0xDC, 0x07,
|
|
|
|
|
0xB8, 0x07, 0x70, 0x0E, 0x03, 0x00, 0xE0, 0x38, 0x07, 0x00, 0xC0, 0x00, 0x00, 0x07, 0xFC, 0xFF,
|
|
|
|
|
0xDC, 0x7B, 0x87, 0xF0, 0x7E, 0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x70,
|
|
|
|
|
0xE0, 0x1D, 0xE0, 0x3B, 0xE0, 0x77, 0xE0, 0xEF, 0xC1, 0xDD, 0xC3, 0xBB, 0xC7, 0x73, 0xCE, 0xE3,
|
|
|
|
|
0x9D, 0xC3, 0xBB, 0x87, 0xF7, 0x07, 0xEE, 0x07, 0xDC, 0x0F, 0xB8, 0x0F, 0x70, 0x0E, 0xE0, 0x1C,
|
|
|
|
|
0x00, 0x00, 0x1E, 0x00, 0x78, 0x00, 0xF0, 0x00, 0xFF, 0x9F, 0xFB, 0x8F, 0x70, 0xFE, 0x0F, 0xC1,
|
|
|
|
|
0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0E, 0x00, 0x03, 0x81, 0xF0, 0x3C, 0x00,
|
|
|
|
|
0x0C, 0x40, 0x19, 0xC0, 0x3F, 0x00, 0x3C, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x70, 0x0E, 0xF0,
|
|
|
|
|
0x1D, 0xF0, 0x3B, 0xF0, 0x77, 0xE0, 0xEE, 0xE1, 0xDD, 0xE3, 0xB9, 0xE7, 0x71, 0xCE, 0xE1, 0xDD,
|
|
|
|
|
0xC3, 0xFB, 0x83, 0xF7, 0x03, 0xEE, 0x07, 0xDC, 0x07, 0xB8, 0x07, 0x70, 0x0E, 0x31, 0x86, 0x70,
|
|
|
|
|
0x7C, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x07, 0xFC, 0xFF, 0xDC, 0x7B, 0x87, 0xF0, 0x7E, 0x0F, 0xC1,
|
|
|
|
|
0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x70, 0x70, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x0E,
|
|
|
|
|
0x00, 0x00, 0x00, 0xFF, 0x87, 0xFE, 0x38, 0xF1, 0xC3, 0xCE, 0x0E, 0x70, 0x73, 0x83, 0x9C, 0x1C,
|
|
|
|
|
0xE0, 0xE7, 0x07, 0x38, 0x39, 0xC1, 0xCE, 0x0E, 0xE0, 0x1D, 0xE0, 0x3B, 0xE0, 0x77, 0xE0, 0xEF,
|
|
|
|
|
0xC1, 0xDD, 0xC3, 0xBB, 0xC7, 0x73, 0xCE, 0xE3, 0x9D, 0xC3, 0xBB, 0x83, 0xF7, 0x07, 0xEE, 0x07,
|
|
|
|
|
0xDC, 0x07, 0xB8, 0x07, 0x70, 0x0E, 0xE0, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x07, 0xC0,
|
|
|
|
|
0x0F, 0x00, 0xFF, 0x9F, 0xFB, 0x8F, 0x70, 0xFE, 0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F,
|
|
|
|
|
0x83, 0xF0, 0x7E, 0x0E, 0x01, 0xC0, 0x38, 0x0F, 0x07, 0xC0, 0xF8, 0x07, 0xF8, 0x03, 0xFC, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xC0, 0x7F, 0xF0, 0x7E, 0x7C, 0x78, 0x0F, 0x38, 0x03,
|
|
|
|
|
0xDC, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xE0, 0x07, 0x70, 0x07, 0xB8,
|
|
|
|
|
0x03, 0xDE, 0x03, 0xC7, 0xE7, 0xC1, 0xFF, 0xC0, 0x3F, 0xC0, 0x3F, 0xC1, 0xFE, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x0F, 0xE0, 0xFF, 0x8F, 0x3E, 0x70, 0x77, 0x03, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0,
|
|
|
|
|
0x3D, 0xC1, 0xCF, 0x3E, 0x3F, 0xE0, 0xFE, 0x00, 0x06, 0x30, 0x03, 0xB8, 0x00, 0xFC, 0x00, 0x7C,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x3F, 0xF8, 0x3F, 0x3E, 0x3C, 0x07, 0x9C, 0x01, 0xEE,
|
|
|
|
|
0x00, 0xFF, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xF0, 0x03, 0xB8, 0x03, 0xDC, 0x01,
|
|
|
|
|
0xEF, 0x01, 0xE3, 0xF3, 0xE0, 0xFF, 0xE0, 0x1F, 0xE0, 0x18, 0xC0, 0xEE, 0x07, 0xE0, 0x1F, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x7F, 0x07, 0xFC, 0x79, 0xF3, 0x83, 0xB8, 0x1F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F,
|
|
|
|
|
0x81, 0xEE, 0x0E, 0x79, 0xF1, 0xFF, 0x07, 0xF0, 0x00, 0xCC, 0x00, 0xEE, 0x00, 0xEE, 0x00, 0x66,
|
|
|
|
|
0x00, 0x33, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x1F, 0xFC, 0x1F, 0x9F, 0x1E, 0x03, 0xCE,
|
|
|
|
|
0x00, 0xF7, 0x00, 0x7F, 0x80, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF8, 0x01, 0xDC, 0x01,
|
|
|
|
|
0xEE, 0x00, 0xF7, 0x80, 0xF1, 0xF9, 0xF0, 0x7F, 0xF0, 0x0F, 0xF0, 0x06, 0x70, 0x3B, 0x83, 0xB8,
|
|
|
|
|
0x3B, 0x81, 0x98, 0x00, 0x00, 0x00, 0x03, 0xF8, 0x3F, 0xE3, 0xCF, 0x9C, 0x1D, 0xC0, 0xFE, 0x03,
|
|
|
|
|
0xF0, 0x1F, 0x80, 0xFC, 0x0F, 0x70, 0x73, 0xCF, 0x8F, 0xF8, 0x3F, 0x80, 0x07, 0xFF, 0xFE, 0x3F,
|
|
|
|
|
0xFF, 0xFC, 0xFC, 0x38, 0x01, 0xE0, 0x70, 0x07, 0x80, 0xE0, 0x0E, 0x01, 0xC0, 0x3C, 0x03, 0x80,
|
|
|
|
|
0x70, 0x07, 0xFE, 0xE0, 0x0F, 0xFD, 0xC0, 0x1C, 0x03, 0xC0, 0x38, 0x03, 0x80, 0x70, 0x07, 0x80,
|
|
|
|
|
0xE0, 0x0F, 0x81, 0xC0, 0x0F, 0xC3, 0x80, 0x0F, 0xFF, 0xFF, 0x07, 0xFF, 0xFE, 0x1F, 0x87, 0xE0,
|
|
|
|
|
0xFF, 0xBF, 0xC7, 0x9F, 0xE7, 0x9C, 0x1E, 0x0E, 0xE0, 0x78, 0x3F, 0x80, 0xFF, 0xFE, 0x03, 0xFF,
|
|
|
|
|
0xF8, 0x0E, 0x00, 0xE0, 0x78, 0x01, 0xC1, 0xF0, 0x07, 0x9F, 0xF3, 0x0F, 0xFB, 0xFE, 0x1F, 0x87,
|
|
|
|
|
0xF8, 0x03, 0x00, 0x1C, 0x00, 0xF0, 0x03, 0x80, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF0, 0xFF,
|
|
|
|
|
0xE3, 0x8F, 0xCE, 0x07, 0x38, 0x1E, 0xE0, 0x7B, 0x81, 0xCE, 0x1F, 0x3F, 0xF8, 0xFF, 0xC3, 0x8F,
|
|
|
|
|
0x0E, 0x1C, 0x38, 0x38, 0xE0, 0xF3, 0x81, 0xCE, 0x07, 0xB8, 0x0E, 0x0E, 0x0E, 0x1C, 0x38, 0x30,
|
|
|
|
|
0x00, 0x00, 0xFF, 0xFF, 0xE2, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xFF,
|
|
|
|
|
0xC3, 0xFF, 0x8E, 0x3F, 0x38, 0x1C, 0xE0, 0x7B, 0x81, 0xEE, 0x07, 0x38, 0x7C, 0xFF, 0xE3, 0xFF,
|
|
|
|
|
0x0E, 0x3C, 0x38, 0x70, 0xE0, 0xE3, 0x83, 0xCE, 0x07, 0x38, 0x1E, 0xE0, 0x38, 0x00, 0x00, 0x70,
|
|
|
|
|
0x07, 0xC0, 0x1E, 0x00, 0x3F, 0xCF, 0xF3, 0x88, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E,
|
|
|
|
|
0x03, 0x80, 0xE0, 0x38, 0x00, 0x07, 0x83, 0xC0, 0xF0, 0x00, 0x31, 0x80, 0xEE, 0x01, 0xF0, 0x03,
|
|
|
|
|
0x80, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xF0, 0xFF, 0xE3, 0x8F, 0xCE, 0x07, 0x38, 0x1E, 0xE0,
|
|
|
|
|
0x7B, 0x81, 0xCE, 0x1F, 0x3F, 0xF8, 0xFF, 0xC3, 0x8F, 0x0E, 0x1C, 0x38, 0x38, 0xE0, 0xF3, 0x81,
|
|
|
|
|
0xCE, 0x07, 0xB8, 0x0E, 0xC6, 0xEE, 0x7C, 0x3C, 0x18, 0x00, 0x00, 0xFF, 0xFF, 0xE2, 0xE0, 0xE0,
|
|
|
|
|
0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x00, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x60,
|
|
|
|
|
0x00, 0x00, 0x00, 0x03, 0xFC, 0x3F, 0xF3, 0xE3, 0x9C, 0x00, 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C,
|
|
|
|
|
0x00, 0xF8, 0x01, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x07, 0xBC, 0xF9, 0xFF, 0xC7, 0xF8, 0x07,
|
|
|
|
|
0x01, 0xC0, 0xE0, 0x70, 0x18, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xBC, 0xCE, 0x03, 0xC0, 0xFC, 0x0F,
|
|
|
|
|
0xC0, 0xF8, 0x0E, 0x03, 0xF9, 0xEF, 0xFB, 0xFC, 0x03, 0x00, 0x3C, 0x03, 0xF0, 0x39, 0x80, 0x8C,
|
|
|
|
|
0x00, 0x00, 0x00, 0x03, 0xFC, 0x3F, 0xF3, 0xE3, 0x9C, 0x00, 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C,
|
|
|
|
|
0x00, 0xF8, 0x01, 0xF0, 0x07, 0x80, 0x1E, 0x00, 0x70, 0x07, 0xBC, 0xF9, 0xFF, 0xC7, 0xF8, 0x0C,
|
|
|
|
|
0x07, 0x03, 0xE1, 0xDC, 0x63, 0x00, 0x00, 0x00, 0xFE, 0xFF, 0xBC, 0xCE, 0x03, 0xC0, 0xFC, 0x0F,
|
|
|
|
|
0xC0, 0xF8, 0x0E, 0x03, 0xF9, 0xEF, 0xFB, 0xFC, 0x1F, 0xE1, 0xFF, 0x9F, 0x1C, 0xE0, 0x07, 0x00,
|
|
|
|
|
0x3C, 0x00, 0xF8, 0x03, 0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x3C, 0x00, 0xF0, 0x03, 0x80, 0x3D, 0xE7,
|
|
|
|
|
0xCF, 0xFE, 0x3F, 0xC0, 0x38, 0x01, 0xC0, 0x07, 0x00, 0xF8, 0x07, 0x80, 0x3F, 0xBF, 0xEF, 0x33,
|
|
|
|
|
0x80, 0xF0, 0x3F, 0x03, 0xF0, 0x3E, 0x03, 0x80, 0xFE, 0x7B, 0xFE, 0xFF, 0x07, 0x01, 0xC0, 0x38,
|
|
|
|
|
0x3E, 0x1F, 0x00, 0x08, 0xC0, 0xE6, 0x03, 0xF0, 0x0F, 0x00, 0x30, 0x00, 0x00, 0x00, 0x03, 0xFC,
|
|
|
|
|
0x3F, 0xF3, 0xE3, 0x9C, 0x00, 0xE0, 0x07, 0x80, 0x1F, 0x00, 0x7C, 0x00, 0xF8, 0x01, 0xF0, 0x07,
|
|
|
|
|
0x80, 0x1E, 0x00, 0x70, 0x07, 0xBC, 0xF9, 0xFF, 0xC7, 0xF8, 0x63, 0x1D, 0xC3, 0xE0, 0x70, 0x08,
|
|
|
|
|
0x00, 0x00, 0x00, 0xFE, 0xFF, 0xBC, 0xCE, 0x03, 0xC0, 0xFC, 0x0F, 0xC0, 0xF8, 0x0E, 0x03, 0xF9,
|
|
|
|
|
0xEF, 0xFB, 0xFC, 0xFF, 0xFF, 0xFF, 0xF0, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01,
|
|
|
|
|
0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07,
|
|
|
|
|
0x80, 0x0C, 0x00, 0x70, 0x00, 0xE0, 0x03, 0x80, 0x3E, 0x00, 0xF0, 0x00, 0x70, 0x78, 0x3C, 0x1E,
|
|
|
|
|
0x0F, 0xF7, 0xFB, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x07, 0x03, 0x81, 0xE4, 0xFF, 0x3F, 0x86,
|
|
|
|
|
0x03, 0x81, 0xC3, 0xE1, 0xF0, 0x08, 0x40, 0x33, 0x80, 0xFC, 0x01, 0xE0, 0x03, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x3F, 0xFF, 0xFF, 0xFC, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01,
|
|
|
|
|
0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x06,
|
|
|
|
|
0x03, 0x01, 0x8E, 0xCF, 0x67, 0x83, 0xC1, 0xFE, 0xFF, 0x78, 0x3C, 0x1E, 0x0F, 0x07, 0x83, 0xC1,
|
|
|
|
|
0xE0, 0x70, 0x3C, 0x9F, 0xE7, 0xF0, 0xFF, 0xFF, 0xFF, 0xF0, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E,
|
|
|
|
|
0x00, 0x78, 0x0F, 0xFC, 0x3F, 0xF0, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78,
|
|
|
|
|
0x01, 0xE0, 0x07, 0x80, 0x70, 0x78, 0x3C, 0x1E, 0x0F, 0xF7, 0xFB, 0xC1, 0xE0, 0xF0, 0x7F, 0xBF,
|
|
|
|
|
0xDE, 0x0F, 0x03, 0x81, 0xE4, 0xFF, 0x3F, 0x80, 0x1F, 0x60, 0x7F, 0x83, 0xBC, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x0E, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80,
|
|
|
|
|
0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xEE, 0x03, 0xBC, 0x1E, 0x7C, 0xF0, 0xFF, 0xC1, 0xFC,
|
|
|
|
|
0x00, 0x1E, 0xC3, 0xFC, 0x37, 0x80, 0x00, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07,
|
|
|
|
|
0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0x70, 0x77, 0x07, 0x78, 0xF3, 0xFF, 0x1F, 0xF0, 0x1F, 0xE0,
|
|
|
|
|
0x7F, 0x80, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE,
|
|
|
|
|
0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3B, 0x80, 0xEF, 0x07, 0x9F,
|
|
|
|
|
0x3C, 0x3F, 0xF0, 0x7F, 0x00, 0x3F, 0xC3, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x07, 0xF0, 0x7F,
|
|
|
|
|
0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x77, 0x07, 0x70, 0x77, 0x8F, 0x3F, 0xF1, 0xFF,
|
|
|
|
|
0x18, 0xC0, 0x77, 0x00, 0xFC, 0x03, 0xE0, 0x00, 0x00, 0x00, 0x0E, 0x03, 0xF8, 0x0F, 0xE0, 0x3F,
|
|
|
|
|
0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xEE,
|
|
|
|
|
0x03, 0xBC, 0x1E, 0x7C, 0xF0, 0xFF, 0xC1, 0xFC, 0x00, 0x39, 0xC1, 0xDC, 0x1F, 0x80, 0xF0, 0x00,
|
|
|
|
|
0x00, 0x00, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0x70, 0x77,
|
|
|
|
|
0x07, 0x78, 0xF3, 0xFF, 0x1F, 0xF0, 0x07, 0x80, 0x36, 0x00, 0xCC, 0x03, 0x60, 0x07, 0x80, 0x00,
|
|
|
|
|
0x00, 0x00, 0x38, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03,
|
|
|
|
|
0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xB8, 0x0E, 0xF0, 0x79, 0xF3, 0xC3, 0xFF, 0x07, 0xF0,
|
|
|
|
|
0x0F, 0x01, 0xD8, 0x19, 0x81, 0xD8, 0x0F, 0x00, 0x00, 0x00, 0x0F, 0x07, 0xF0, 0x7F, 0x07, 0xF0,
|
|
|
|
|
0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x77, 0x07, 0x70, 0x77, 0x8F, 0x3F, 0xF1, 0xFF, 0x03, 0x30,
|
|
|
|
|
0x1D, 0xC0, 0xEE, 0x07, 0x30, 0x18, 0xC0, 0x00, 0x00, 0x00, 0x38, 0x0F, 0xE0, 0x3F, 0x80, 0xFE,
|
|
|
|
|
0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xB8,
|
|
|
|
|
0x0E, 0xF0, 0x79, 0xF3, 0xC3, 0xFF, 0x07, 0xF0, 0x06, 0x60, 0xEE, 0x0E, 0xE1, 0xDC, 0x19, 0x80,
|
|
|
|
|
0x00, 0x00, 0x0F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x77, 0x07,
|
|
|
|
|
0x70, 0x77, 0x8F, 0x3F, 0xF1, 0xFF, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80,
|
|
|
|
|
0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0E, 0xE0, 0x3B, 0xC1, 0xE7, 0xCF,
|
|
|
|
|
0x8F, 0xFC, 0x1F, 0xE0, 0x0E, 0x00, 0x70, 0x01, 0xC0, 0x07, 0xC0, 0x0F, 0x00, 0xF0, 0x7F, 0x07,
|
|
|
|
|
0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0x70, 0x77, 0x07, 0x78, 0xF3, 0xFF, 0x1F,
|
|
|
|
|
0xF0, 0x0E, 0x01, 0xC0, 0x1C, 0x01, 0xF0, 0x1F, 0x00, 0x18, 0x00, 0x00, 0x70, 0x00, 0x01, 0xF0,
|
|
|
|
|
0x00, 0x07, 0x70, 0x00, 0x0C, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x78, 0x00, 0x07, 0x70,
|
|
|
|
|
0x00, 0x1E, 0xE0, 0x70, 0x39, 0xC0, 0xF0, 0x73, 0x83, 0xE0, 0xE7, 0x87, 0xC1, 0xC7, 0x0F, 0xC7,
|
|
|
|
|
0x8E, 0x3B, 0x8E, 0x1C, 0x77, 0x1C, 0x3C, 0xE7, 0x38, 0x3B, 0x8E, 0x70, 0x77, 0x1D, 0xC0, 0xEE,
|
|
|
|
|
0x1F, 0x81, 0xF8, 0x3F, 0x01, 0xF0, 0x3C, 0x03, 0xC0, 0x78, 0x07, 0x80, 0xF0, 0x00, 0x60, 0x00,
|
|
|
|
|
0x1C, 0x00, 0x07, 0xC0, 0x01, 0xDC, 0x00, 0x31, 0x80, 0x00, 0x00, 0x00, 0x00, 0x07, 0x07, 0x07,
|
|
|
|
|
0xF0, 0xE0, 0xEE, 0x1E, 0x1D, 0xC3, 0xC7, 0x38, 0xF8, 0xE3, 0x9F, 0x9C, 0x73, 0x77, 0x0E, 0xEE,
|
|
|
|
|
0xE0, 0xFC, 0xDC, 0x1F, 0x1F, 0x03, 0xE3, 0xE0, 0x3C, 0x3C, 0x07, 0x07, 0x00, 0x01, 0x00, 0x07,
|
|
|
|
|
0x00, 0x1F, 0x00, 0x77, 0x00, 0xC6, 0x00, 0x00, 0x00, 0x00, 0x78, 0x0F, 0x70, 0x1C, 0xF0, 0x78,
|
|
|
|
|
0xE0, 0xE1, 0xE3, 0xC1, 0xEF, 0x01, 0xDC, 0x03, 0xF8, 0x03, 0xE0, 0x03, 0x80, 0x07, 0x00, 0x0E,
|
|
|
|
|
0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x02, 0x00, 0x70, 0x0F, 0x81, 0xDC,
|
|
|
|
|
0x18, 0xC0, 0x00, 0x00, 0x0E, 0x07, 0xF0, 0x77, 0x07, 0x70, 0xE7, 0x8E, 0x38, 0xE3, 0x8C, 0x1D,
|
|
|
|
|
0xC1, 0xDC, 0x1F, 0x80, 0xF8, 0x0F, 0x80, 0x70, 0x07, 0x00, 0x70, 0x9E, 0x0F, 0xC0, 0xF8, 0x00,
|
|
|
|
|
0x0E, 0xE0, 0x3D, 0xE0, 0x3B, 0x80, 0x00, 0x00, 0x00, 0x1E, 0x03, 0xDC, 0x07, 0x3C, 0x1E, 0x38,
|
|
|
|
|
0x38, 0x78, 0xF0, 0x7B, 0xC0, 0x77, 0x00, 0xFE, 0x00, 0xF8, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80,
|
|
|
|
|
0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0x00, 0xC0, 0x07, 0x00, 0x38, 0x00,
|
|
|
|
|
0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0x7F, 0xFC, 0x01, 0xE0, 0x0F, 0x00, 0x38, 0x01,
|
|
|
|
|
0xE0, 0x0F, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0x00, 0x78, 0x01, 0xC0,
|
|
|
|
|
0x0F, 0xFF, 0xFF, 0xFF, 0x03, 0x80, 0x70, 0x1C, 0x07, 0x00, 0xC0, 0x00, 0x00, 0x03, 0xFF, 0x7F,
|
|
|
|
|
0xE0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x1C, 0x07, 0x81, 0xE0, 0x78, 0x0E, 0x01, 0xFF, 0xBF, 0xF0,
|
|
|
|
|
0x03, 0x80, 0x1E, 0x00, 0x38, 0x00, 0x00, 0x00, 0x01, 0xFF, 0xF7, 0xFF, 0xC0, 0x1E, 0x00, 0xF0,
|
|
|
|
|
0x03, 0x80, 0x1E, 0x00, 0xF0, 0x07, 0x80, 0x1C, 0x00, 0xF0, 0x07, 0x80, 0x1C, 0x00, 0xF0, 0x07,
|
|
|
|
|
0x80, 0x1C, 0x00, 0xFF, 0xFF, 0xFF, 0xF0, 0x0E, 0x01, 0xE0, 0x38, 0x00, 0x00, 0x00, 0xFF, 0xDF,
|
|
|
|
|
0xF8, 0x0F, 0x03, 0xC0, 0xF0, 0x3C, 0x07, 0x01, 0xE0, 0x78, 0x1E, 0x03, 0x80, 0x7F, 0xEF, 0xFC,
|
|
|
|
|
0x0C, 0x60, 0x3B, 0x80, 0x7C, 0x01, 0xE0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x1F, 0xFF, 0x7F, 0xFC,
|
|
|
|
|
0x01, 0xE0, 0x0F, 0x00, 0x38, 0x01, 0xE0, 0x0F, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0x00, 0x78, 0x01,
|
|
|
|
|
0xC0, 0x0F, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0xFF, 0xFF, 0xFF, 0x31, 0x87, 0x30, 0x7C, 0x07, 0x80,
|
|
|
|
|
0x60, 0x00, 0x00, 0x03, 0xFF, 0x7F, 0xE0, 0x3C, 0x0F, 0x03, 0xC0, 0xF0, 0x1C, 0x07, 0x81, 0xE0,
|
|
|
|
|
0x78, 0x0E, 0x01, 0xFF, 0xBF, 0xF0, 0x3F, 0x7F, 0xF0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0,
|
feat: Vietnamese glyphs support (#1147)
## Summary
* **What is the goal of this PR?**
Add Vietnamese glyphs support for the reader's built-in fonts, enabling
proper rendering of Vietnamese text in EPUB content.
* **What changes are included?**
- Added 3 new Unicode intervals to `fontconvert.py` covering Vietnamese
characters:
- **Latin Extended-B** (Vietnamese subset only): `U+01A0–U+01B0` — Ơ/ơ,
Ư/ư
- **Vietnamese Extended**: `U+1EA0–U+1EF9` — All precomposed Vietnamese
characters with tone marks (Ả, Ấ, Ầ, Ẩ, Ẫ, Ậ, Ắ, …, Ỹ)
- Re-generated all 54 built-in font header files (Bookerly, Noto Sans,
OpenDyslexic, Ubuntu across all sizes and styles) to include the new
Vietnamese glyphs.
## Additional Context
* **Scope**: This PR only covers the **reader** fonts. The outer UI
still uses the Ubuntu font which does not fully support Vietnamese — UI
and i18n will be addressed in a follow-up PR (per discussion in PR
#1124).
* **Memory impact**:
| Metric | Before | After | Delta |
|---|---|---|---|
| Flash Data (`.rodata`) | 2,971,028 B | 3,290,748 B | **+319,720 B
(+10.8%)** |
| Total image size | 4,663,235 B | 4,982,955 B | **+319,720 B (+6.9%)**
|
| Flash usage | 69.1% | 74.0% | **+4.9 pp** |
| RAM usage | 29.0% | 29.0% | **No change** |
* **Risk**: Low — this is a data-only change (font glyph tables in
`.rodata`). No logic changes, no RAM impact. Flash headroom remains
comfortable at 74%.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
AI was used to identify the minimal set of Unicode ranges needed for
Vietnamese support and to assist with the PR description.
---------
Co-authored-by: danoooob <danoooob@example.com>
2026-02-25 00:21:39 +07:00
|
|
|
0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x7F, 0xB8,
|
|
|
|
|
0x7F, 0xFE, 0x3F, 0x3F, 0x1E, 0x03, 0xC7, 0x00, 0x71, 0xC0, 0x1E, 0xF0, 0x03, 0xB8, 0x00, 0xEE,
|
|
|
|
|
0x00, 0x3B, 0x80, 0x0E, 0xF0, 0x03, 0x9C, 0x01, 0xE7, 0x00, 0x79, 0xE0, 0x3C, 0x3F, 0x3E, 0x07,
|
|
|
|
|
0xFF, 0x00, 0x7F, 0x80, 0x00, 0x1C, 0x00, 0x71, 0xFD, 0xCF, 0xFE, 0x79, 0xF1, 0xC1, 0xCE, 0x07,
|
|
|
|
|
0xB8, 0x0E, 0xE0, 0x3B, 0x80, 0xEE, 0x07, 0x9C, 0x1C, 0x79, 0xF0, 0xFF, 0x81, 0xFC, 0x00, 0x00,
|
|
|
|
|
0x03, 0x80, 0x01, 0xC0, 0x01, 0xDC, 0x07, 0xEE, 0x03, 0xE7, 0x01, 0xE3, 0x80, 0xF1, 0xC0, 0x78,
|
|
|
|
|
0xE0, 0x3C, 0x70, 0x1E, 0x38, 0x0F, 0x1C, 0x07, 0x8E, 0x03, 0xC7, 0x01, 0xE3, 0x80, 0xE1, 0xC0,
|
|
|
|
|
0x70, 0xF0, 0x78, 0x3E, 0x7C, 0x0F, 0xFC, 0x03, 0xF8, 0x00, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x7F,
|
|
|
|
|
0x83, 0xEF, 0x07, 0x9E, 0x0E, 0x3C, 0x1C, 0x78, 0x38, 0xF0, 0x71, 0xE0, 0xE3, 0xC1, 0xC3, 0x83,
|
2026-02-25 03:56:11 -06:00
|
|
|
0x87, 0x07, 0x0F, 0x1E, 0x0F, 0xFC, 0x0F, 0xF8, 0x00, 0x00, 0x0C, 0x60, 0x00, 0x00, 0x33, 0x80,
|
|
|
|
|
0x00, 0x00, 0x7C, 0x00, 0x00, 0x01, 0xE0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x3F, 0xF0, 0x1F, 0xFE, 0xFF, 0xE0, 0x7F, 0xFB, 0x87, 0xE0, 0x01, 0xEE, 0x07, 0x80,
|
|
|
|
|
0x0F, 0x38, 0x0F, 0x00, 0x38, 0xE0, 0x1C, 0x01, 0xC3, 0x80, 0x78, 0x0F, 0x0E, 0x01, 0xE0, 0x78,
|
|
|
|
|
0x38, 0x07, 0x81, 0xC0, 0xE0, 0x1E, 0x0F, 0x03, 0x80, 0x78, 0x78, 0x0E, 0x01, 0xC1, 0xC0, 0x38,
|
|
|
|
|
0x0F, 0x0E, 0x00, 0xE0, 0x78, 0x78, 0x03, 0x87, 0xE1, 0xC0, 0x0F, 0xFE, 0x0F, 0xFF, 0xFF, 0xF0,
|
|
|
|
|
0x3F, 0xFF, 0x00, 0x00, 0x31, 0x80, 0x00, 0x07, 0x70, 0x00, 0x00, 0x7C, 0x7F, 0xE0, 0x07, 0x8F,
|
|
|
|
|
0xFE, 0x00, 0x61, 0xC3, 0xF0, 0x00, 0x38, 0x1E, 0x00, 0x07, 0x01, 0xE3, 0xFF, 0xE0, 0x1C, 0x7F,
|
|
|
|
|
0xFC, 0x03, 0xC0, 0x3F, 0x80, 0x78, 0x0F, 0x70, 0x0F, 0x03, 0xCE, 0x01, 0xE0, 0xF1, 0xC0, 0x3C,
|
|
|
|
|
0x3C, 0x38, 0x07, 0x07, 0x07, 0x01, 0xE1, 0xE0, 0xE0, 0x78, 0x78, 0x1C, 0x3F, 0x0E, 0x03, 0xFF,
|
|
|
|
|
0x83, 0xFF, 0xFF, 0xE0, 0x7F, 0xF0, 0x00, 0x00, 0xC6, 0x00, 0x38, 0x77, 0x00, 0x1C, 0x1F, 0x00,
|
|
|
|
|
0x0E, 0x07, 0x00, 0x07, 0x01, 0x80, 0x03, 0x80, 0x00, 0x01, 0xC0, 0x00, 0x3F, 0xE3, 0xFF, 0x3F,
|
|
|
|
|
0xF1, 0xFF, 0xBC, 0xF8, 0x03, 0xBC, 0x1C, 0x03, 0xDC, 0x0E, 0x03, 0xCE, 0x07, 0x03, 0xC7, 0x03,
|
|
|
|
|
0x83, 0xC3, 0x81, 0xC1, 0xC1, 0xC0, 0xE1, 0xE0, 0xF0, 0x71, 0xE0, 0x3E, 0x38, 0xE0, 0x0F, 0xFC,
|
|
|
|
|
0xFF, 0xE3, 0xFE, 0x7F, 0xF0, 0xE0, 0x00, 0x1F, 0x80, 0x00, 0x7E, 0x00, 0x01, 0xF8, 0x00, 0x07,
|
|
|
|
|
0xE0, 0x00, 0x1F, 0x80, 0x00, 0x7E, 0x00, 0x01, 0xF8, 0x00, 0x07, 0xE0, 0x00, 0x1F, 0x80, 0x00,
|
|
|
|
|
0x7E, 0x00, 0x01, 0xF8, 0x00, 0x07, 0xE0, 0x00, 0x1F, 0x80, 0x00, 0xFE, 0x01, 0xE7, 0xBF, 0xFF,
|
|
|
|
|
0xFE, 0xFF, 0xEF, 0xE0, 0x00, 0x07, 0xE0, 0x07, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0x07,
|
|
|
|
|
0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0x07,
|
|
|
|
|
0xE0, 0x07, 0xE0, 0x07, 0xFF, 0xE7, 0xFF, 0xE7, 0x00, 0x07, 0x00, 0x0F, 0x00, 0x0E, 0x00, 0x7E,
|
|
|
|
|
0x00, 0x7C, 0x70, 0x0E, 0x3D, 0xC7, 0xB8, 0x07, 0x00, 0xE0, 0x1C, 0x3B, 0x87, 0x70, 0xEE, 0x1D,
|
|
|
|
|
0xC3, 0xB8, 0x77, 0x0E, 0xE1, 0xDC, 0x3B, 0x87, 0x78, 0xEF, 0x9C, 0xE3, 0x80, 0x70, 0x1C, 0x07,
|
|
|
|
|
0x83, 0xF0, 0x7C, 0xE0, 0x1C, 0x00, 0xFE, 0x03, 0x80, 0x1F, 0xE0, 0x70, 0x03, 0xFE, 0x0E, 0x00,
|
|
|
|
|
0x7F, 0xC1, 0xC0, 0x0F, 0xDC, 0x38, 0x01, 0xFB, 0xC7, 0x00, 0x3F, 0x3C, 0xE0, 0x07, 0xE3, 0x9C,
|
|
|
|
|
0x00, 0xFC, 0x3B, 0x80, 0x1F, 0x87, 0xF0, 0x03, 0xF0, 0x7E, 0x00, 0x7E, 0x07, 0xC0, 0x0F, 0xC0,
|
|
|
|
|
0xF8, 0x01, 0xF8, 0x0F, 0x1C, 0xF7, 0x00, 0xE7, 0xFE, 0xE0, 0x1C, 0x7F, 0x80, 0x00, 0x00, 0x3F,
|
|
|
|
|
0x00, 0xE1, 0xFC, 0x07, 0x01, 0xF0, 0x38, 0x0F, 0xC1, 0xC0, 0x7E, 0x0E, 0x1F, 0xB8, 0x70, 0xFD,
|
|
|
|
|
0xE3, 0x87, 0xE7, 0x9C, 0x3F, 0x1C, 0xE1, 0xF8, 0x77, 0x0F, 0xC3, 0xF8, 0x7E, 0x0F, 0xC3, 0xF0,
|
|
|
|
|
0x3E, 0x1F, 0x81, 0xF0, 0xFC, 0x07, 0x87, 0xE0, 0x1C, 0x3F, 0x00, 0xE1, 0xC0, 0x00, 0x0E, 0x00,
|
|
|
|
|
0x00, 0x70, 0x00, 0x07, 0x80, 0x01, 0xF8, 0x00, 0x0F, 0x80, 0x00, 0x03, 0x80, 0x00, 0xE0, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0xEF, 0xFC, 0x3B, 0x8F, 0x0E, 0xE1, 0xE3, 0xB8, 0x38,
|
|
|
|
|
0xEE, 0x0E, 0x3B, 0x83, 0x8E, 0xE0, 0xE3, 0xB8, 0x38, 0xEE, 0x0E, 0x3B, 0x83, 0x8E, 0xE0, 0xE3,
|
|
|
|
|
0xB8, 0x38, 0xE0, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x07, 0x80, 0x07, 0xC0, 0x03, 0xE0, 0x06, 0x30,
|
|
|
|
|
0x03, 0xB8, 0x00, 0xF8, 0x00, 0x78, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x03,
|
|
|
|
|
0xE0, 0x01, 0xF0, 0x01, 0xF8, 0x00, 0xEE, 0x00, 0xE7, 0x00, 0x73, 0xC0, 0x78, 0xE0, 0x38, 0x78,
|
|
|
|
|
0x1C, 0x1C, 0x1E, 0x0E, 0x0F, 0xFF, 0x87, 0xFF, 0xC7, 0x00, 0xE3, 0x80, 0x7B, 0xC0, 0x1D, 0xC0,
|
|
|
|
|
0x0F, 0x31, 0x87, 0x70, 0x7C, 0x07, 0x00, 0x40, 0x00, 0x00, 0x03, 0xFC, 0x7F, 0xCC, 0x78, 0x07,
|
|
|
|
|
0x00, 0x73, 0xFE, 0xFF, 0xFE, 0x7F, 0x07, 0xE0, 0xFE, 0x3F, 0xFF, 0x9F, 0xE0, 0xC7, 0xDD, 0xF1,
|
|
|
|
|
0xC3, 0x00, 0x00, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87,
|
|
|
|
|
0x0E, 0x1C, 0x63, 0x67, 0x3E, 0x3C, 0x18, 0x00, 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C,
|
|
|
|
|
0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x06, 0x30, 0x03, 0xB8, 0x00, 0xF8, 0x00, 0x3C, 0x00, 0x0C,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF0, 0x1F, 0xFC, 0x1F, 0x9F, 0x1E, 0x03, 0xCE, 0x00, 0xF7,
|
|
|
|
|
0x00, 0x7F, 0x80, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF8, 0x01, 0xDC, 0x01, 0xEE, 0x00,
|
|
|
|
|
0xF7, 0x80, 0xF1, 0xF9, 0xF0, 0x7F, 0xF0, 0x0F, 0xF0, 0x18, 0xC0, 0xEE, 0x03, 0xE0, 0x0E, 0x00,
|
|
|
|
|
0x60, 0x00, 0x00, 0x00, 0x03, 0xF8, 0x3F, 0xE3, 0xCF, 0x9C, 0x1D, 0xC0, 0xFE, 0x03, 0xF0, 0x1F,
|
|
|
|
|
0x80, 0xFC, 0x0F, 0x70, 0x73, 0xCF, 0x8F, 0xF8, 0x3F, 0x80, 0x18, 0xC0, 0x77, 0x00, 0xF8, 0x01,
|
|
|
|
|
0xC0, 0x03, 0x00, 0x00, 0x00, 0x00, 0x38, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0,
|
|
|
|
|
0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xB8, 0x0E, 0xF0, 0x79, 0xF3,
|
|
|
|
|
0xC3, 0xFF, 0x07, 0xF0, 0x18, 0x81, 0x9C, 0x1F, 0x80, 0xF0, 0x06, 0x00, 0x00, 0x00, 0x0F, 0x07,
|
|
|
|
|
0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x77, 0x07, 0x70, 0x77, 0x8F, 0x3F,
|
|
|
|
|
0xF1, 0xFF, 0x1F, 0xC0, 0x7F, 0x00, 0x00, 0x07, 0x38, 0x3D, 0xE0, 0x73, 0x80, 0x00, 0x00, 0x00,
|
|
|
|
|
0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F,
|
|
|
|
|
0x80, 0xFE, 0x03, 0xF8, 0x0E, 0xE0, 0x3B, 0xC1, 0xE7, 0xCF, 0x0F, 0xFC, 0x1F, 0xC0, 0x1F, 0xC1,
|
|
|
|
|
0xFC, 0x00, 0x03, 0x9C, 0x39, 0xC3, 0x9C, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07,
|
|
|
|
|
0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0x70, 0x77, 0x07, 0x78, 0xF3, 0xFF, 0x1F, 0xF0, 0x01, 0x80,
|
|
|
|
|
0x0E, 0x03, 0xF6, 0x0F, 0xBC, 0x38, 0x60, 0x00, 0x00, 0x00, 0x38, 0x0F, 0xE0, 0x3F, 0x80, 0xFE,
|
|
|
|
|
0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xB8,
|
|
|
|
|
0x0E, 0xF0, 0x79, 0xF3, 0xC3, 0xFF, 0x07, 0xF0, 0x01, 0x80, 0x38, 0x07, 0x03, 0xFC, 0x39, 0xC3,
|
|
|
|
|
0x9C, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07,
|
|
|
|
|
0x70, 0x77, 0x07, 0x78, 0xF3, 0xFF, 0x1F, 0xF0, 0x18, 0xC0, 0x3F, 0x00, 0x78, 0x0E, 0xD8, 0x38,
|
|
|
|
|
0xF0, 0xE1, 0x80, 0x00, 0x00, 0x00, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80,
|
|
|
|
|
0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0E, 0xE0, 0x3B, 0xC1, 0xE7, 0xCF,
|
|
|
|
|
0x0F, 0xFC, 0x1F, 0xC0, 0x19, 0x81, 0xF8, 0x0F, 0x00, 0x60, 0x39, 0xC3, 0x9C, 0x39, 0xC0, 0x00,
|
|
|
|
|
0x00, 0x0F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x77, 0x07, 0x70,
|
|
|
|
|
0x77, 0x8F, 0x3F, 0xF1, 0xFF, 0x0C, 0x00, 0x38, 0x03, 0xF6, 0x0E, 0xFC, 0x38, 0x60, 0x00, 0x00,
|
|
|
|
|
0x00, 0x38, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8,
|
|
|
|
|
0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xB8, 0x0E, 0xF0, 0x79, 0xF3, 0xC3, 0xFF, 0x07, 0xF0, 0x0C,
|
|
|
|
|
0x01, 0xC0, 0x0E, 0x03, 0xFC, 0x39, 0xC3, 0x9C, 0x00, 0x00, 0x00, 0xF0, 0x7F, 0x07, 0xF0, 0x7F,
|
|
|
|
|
0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0x70, 0x77, 0x07, 0x78, 0xF3, 0xFF, 0x1F, 0xF0, 0x7F,
|
|
|
|
|
0x87, 0xFC, 0x63, 0xE0, 0x0E, 0x00, 0xF0, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x7E, 0x0E, 0x7B, 0xE7,
|
|
|
|
|
0xFC, 0x3F, 0x80, 0x07, 0xF0, 0x03, 0xF8, 0x00, 0x00, 0x01, 0xCE, 0x00, 0xE7, 0x00, 0x73, 0x80,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0xFC, 0x00, 0x77, 0x00, 0x73,
|
|
|
|
|
0x80, 0x39, 0xE0, 0x3C, 0x70, 0x1C, 0x3C, 0x0E, 0x0E, 0x0F, 0x07, 0x07, 0xFF, 0xC3, 0xFF, 0xE3,
|
|
|
|
|
0x80, 0x71, 0xC0, 0x3D, 0xE0, 0x0E, 0xE0, 0x07, 0x80, 0x3F, 0x87, 0xF0, 0x00, 0x18, 0xE7, 0xBC,
|
|
|
|
|
0x63, 0x80, 0x00, 0x00, 0x7F, 0x8F, 0xF9, 0x8F, 0x00, 0xE0, 0x0E, 0x7F, 0xDF, 0xFF, 0xCF, 0xE0,
|
|
|
|
|
0xFC, 0x1F, 0xC7, 0xFF, 0xF3, 0xFC, 0x07, 0xF0, 0x03, 0xF8, 0x00, 0x00, 0x00, 0x38, 0x00, 0x1C,
|
|
|
|
|
0x00, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0xC0, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0xFC, 0x00,
|
|
|
|
|
0x77, 0x00, 0x73, 0x80, 0x39, 0xE0, 0x3C, 0x70, 0x1C, 0x3C, 0x0E, 0x0E, 0x0F, 0x07, 0x07, 0xFF,
|
|
|
|
|
0xC3, 0xFF, 0xE3, 0x80, 0x71, 0xC0, 0x3D, 0xE0, 0x0E, 0xE0, 0x07, 0x80, 0x3F, 0x87, 0xF0, 0x00,
|
|
|
|
|
0x07, 0x00, 0xE0, 0x1C, 0x00, 0x00, 0x00, 0x7F, 0x8F, 0xF9, 0x8F, 0x00, 0xE0, 0x0E, 0x7F, 0xDF,
|
|
|
|
|
0xFF, 0xCF, 0xE0, 0xFC, 0x1F, 0xC7, 0xFF, 0xF3, 0xFC, 0x00, 0x1F, 0xC0, 0x00, 0x3F, 0x80, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x80, 0x1F, 0xFF, 0x00, 0x7F, 0x00,
|
|
|
|
|
0x00, 0xEE, 0x00, 0x03, 0xDC, 0x00, 0x07, 0x38, 0x00, 0x1C, 0x70, 0x00, 0x78, 0xFF, 0x80, 0xE1,
|
|
|
|
|
0xFF, 0x03, 0x83, 0x80, 0x0F, 0x07, 0x00, 0x1F, 0xFE, 0x00, 0x7F, 0xFC, 0x00, 0xE0, 0x38, 0x03,
|
|
|
|
|
0xC0, 0x70, 0x07, 0x00, 0xFF, 0xFC, 0x01, 0xFF, 0xC0, 0x03, 0xFC, 0x00, 0x3F, 0xC0, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xF1, 0xF8, 0x7F, 0xBF, 0xC6, 0x3F, 0xBE, 0x01, 0xE0, 0xE0,
|
|
|
|
|
0x0E, 0x0E, 0x3F, 0xFF, 0xE7, 0xFF, 0xFE, 0xF9, 0xE0, 0x0E, 0x0E, 0x00, 0xE0, 0xF0, 0x0F, 0x1F,
|
|
|
|
|
0x8C, 0xFF, 0xFF, 0xC3, 0xF9, 0xFC, 0x07, 0xF8, 0x0F, 0xFE, 0x0F, 0xCF, 0x0F, 0x00, 0x07, 0x00,
|
|
|
|
|
0x03, 0x80, 0x03, 0xC0, 0x01, 0xC0, 0x00, 0xE0, 0x1E, 0x70, 0x0F, 0x3C, 0x1F, 0xFE, 0x0F, 0xF7,
|
|
|
|
|
0x01, 0xE3, 0xC0, 0xF0, 0xFC, 0x78, 0x3F, 0xFC, 0x07, 0xFC, 0x00, 0x1F, 0xF0, 0xFF, 0xC7, 0xCF,
|
|
|
|
|
0x1C, 0x1C, 0xE0, 0x73, 0x8F, 0xFE, 0x3F, 0xF8, 0x1C, 0xE0, 0x73, 0xC1, 0xC7, 0xDF, 0x0F, 0xFC,
|
|
|
|
|
0x1F, 0xF0, 0x01, 0xC0, 0x0F, 0x1C, 0x78, 0x7F, 0xC1, 0xFE, 0x00, 0x06, 0x30, 0x0E, 0xE0, 0x0F,
|
|
|
|
|
0x80, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x1F, 0xFC, 0x7E, 0x79, 0xE0, 0x03,
|
|
|
|
|
0xC0, 0x07, 0x00, 0x1E, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x1F, 0xE0, 0x3F, 0xC0, 0x7B, 0x80,
|
|
|
|
|
0xF7, 0x81, 0xE7, 0xE3, 0xC7, 0xFF, 0x83, 0xFE, 0x18, 0xC1, 0x9C, 0x0F, 0x80, 0xF0, 0x06, 0x00,
|
|
|
|
|
0x00, 0x00, 0x01, 0xFF, 0x3F, 0xF7, 0xCF, 0x70, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07,
|
|
|
|
|
0xF0, 0x77, 0xDF, 0x3F, 0xF1, 0xFF, 0x00, 0x70, 0x0F, 0x71, 0xE7, 0xFE, 0x7F, 0x80, 0x10, 0x80,
|
|
|
|
|
0xE7, 0x01, 0xF8, 0x03, 0xC0, 0x06, 0x00, 0x00, 0x00, 0x00, 0x38, 0x1E, 0xE0, 0xF3, 0x87, 0x8E,
|
feat: Vietnamese glyphs support (#1147)
## Summary
* **What is the goal of this PR?**
Add Vietnamese glyphs support for the reader's built-in fonts, enabling
proper rendering of Vietnamese text in EPUB content.
* **What changes are included?**
- Added 3 new Unicode intervals to `fontconvert.py` covering Vietnamese
characters:
- **Latin Extended-B** (Vietnamese subset only): `U+01A0–U+01B0` — Ơ/ơ,
Ư/ư
- **Vietnamese Extended**: `U+1EA0–U+1EF9` — All precomposed Vietnamese
characters with tone marks (Ả, Ấ, Ầ, Ẩ, Ẫ, Ậ, Ắ, …, Ỹ)
- Re-generated all 54 built-in font header files (Bookerly, Noto Sans,
OpenDyslexic, Ubuntu across all sizes and styles) to include the new
Vietnamese glyphs.
## Additional Context
* **Scope**: This PR only covers the **reader** fonts. The outer UI
still uses the Ubuntu font which does not fully support Vietnamese — UI
and i18n will be addressed in a follow-up PR (per discussion in PR
#1124).
* **Memory impact**:
| Metric | Before | After | Delta |
|---|---|---|---|
| Flash Data (`.rodata`) | 2,971,028 B | 3,290,748 B | **+319,720 B
(+10.8%)** |
| Total image size | 4,663,235 B | 4,982,955 B | **+319,720 B (+6.9%)**
|
| Flash usage | 69.1% | 74.0% | **+4.9 pp** |
| RAM usage | 29.0% | 29.0% | **No change** |
* **Risk**: Low — this is a data-only change (font glyph tables in
`.rodata`). No logic changes, no RAM impact. Flash headroom remains
comfortable at 74%.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
AI was used to identify the minimal set of Unicode ranges needed for
Vietnamese support and to assist with the PR description.
---------
Co-authored-by: danoooob <danoooob@example.com>
2026-02-25 00:21:39 +07:00
|
|
|
0x3C, 0x39, 0xE0, 0xEF, 0x03, 0xF8, 0x0F, 0xC0, 0x3F, 0x80, 0xFF, 0x03, 0xBE, 0x0E, 0x7C, 0x38,
|
2026-02-25 03:56:11 -06:00
|
|
|
0xF8, 0xE1, 0xF3, 0x83, 0xCE, 0x07, 0xB8, 0x0F, 0xCC, 0x0F, 0xE0, 0x7C, 0x03, 0x80, 0x00, 0x00,
|
|
|
|
|
0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0xE7, 0x1C, 0x77, 0x87, 0xF0,
|
|
|
|
|
0x7E, 0x07, 0xC0, 0x7E, 0x07, 0xF0, 0x77, 0x87, 0x3C, 0x71, 0xE7, 0x0E, 0x70, 0xF0, 0x07, 0xF8,
|
|
|
|
|
0x0F, 0xFE, 0x0F, 0xCF, 0x8F, 0x01, 0xE7, 0x00, 0x7B, 0x80, 0x3F, 0xC0, 0x0F, 0xC0, 0x07, 0xE0,
|
|
|
|
|
0x03, 0xF0, 0x01, 0xFC, 0x00, 0xEE, 0x00, 0xF7, 0x00, 0x7B, 0xC0, 0x78, 0xFC, 0xF8, 0x3F, 0xF8,
|
|
|
|
|
0x07, 0xF8, 0x00, 0x70, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1F, 0x00, 0x07, 0x80, 0x1F, 0xC1, 0xFF,
|
|
|
|
|
0x1E, 0x7C, 0xE0, 0xEE, 0x07, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x7B, 0x83, 0x9E, 0x7C, 0x7F,
|
|
|
|
|
0xC1, 0xFC, 0x03, 0x80, 0x38, 0x01, 0xC0, 0x0F, 0x80, 0x3C, 0x00, 0x07, 0xF8, 0x03, 0xFC, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xC0, 0x7F, 0xF0, 0x7E, 0x7C, 0x78, 0x0F, 0x38, 0x03,
|
|
|
|
|
0xDC, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xE0, 0x07, 0x70, 0x07, 0xB8,
|
|
|
|
|
0x03, 0xDE, 0x03, 0xC7, 0xE7, 0xC1, 0xFF, 0xC0, 0x3F, 0xC0, 0x03, 0x80, 0x03, 0x80, 0x01, 0xC0,
|
|
|
|
|
0x00, 0xF8, 0x00, 0x3C, 0x00, 0x3F, 0xC1, 0xFE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0xFF,
|
|
|
|
|
0x8F, 0x3E, 0x70, 0x77, 0x03, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x3D, 0xC1, 0xCF, 0x3E, 0x3F,
|
|
|
|
|
0xE0, 0xFE, 0x01, 0xC0, 0x1C, 0x00, 0xE0, 0x07, 0xC0, 0x1E, 0x00, 0x08, 0x40, 0xE7, 0x03, 0xF0,
|
|
|
|
|
0x0F, 0x00, 0x30, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0x7F, 0xF8, 0x03, 0x80, 0x38, 0x03, 0x80, 0x38,
|
|
|
|
|
0x03, 0x80, 0x3F, 0x81, 0xFE, 0x01, 0xF8, 0x03, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0xBC, 0xFF, 0xFF,
|
|
|
|
|
0xCF, 0xF8, 0x10, 0x81, 0x9C, 0x1F, 0x80, 0xF0, 0x06, 0x00, 0x00, 0x00, 0x07, 0xFF, 0x7F, 0xE0,
|
|
|
|
|
0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xF0, 0x0F, 0x80, 0xFE, 0x03, 0xF0, 0x0F, 0x00, 0x70, 0x07,
|
|
|
|
|
0x00, 0x70, 0x0F, 0x73, 0xE7, 0xFE, 0x7F, 0x80, 0x31, 0x99, 0xC7, 0xC3, 0xC0, 0xC0, 0x00, 0x00,
|
|
|
|
|
0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0,
|
|
|
|
|
0xF0, 0xF1, 0xF8, 0xF8, 0x00, 0xFF, 0xC0, 0x7F, 0xFB, 0xFF, 0x81, 0xFF, 0xEE, 0x1F, 0x80, 0x07,
|
|
|
|
|
0xB8, 0x1E, 0x00, 0x3C, 0xE0, 0x3C, 0x00, 0xE3, 0x80, 0x70, 0x07, 0x0E, 0x01, 0xE0, 0x3C, 0x38,
|
|
|
|
|
0x07, 0x81, 0xE0, 0xE0, 0x1E, 0x07, 0x03, 0x80, 0x78, 0x3C, 0x0E, 0x01, 0xE1, 0xE0, 0x38, 0x07,
|
|
|
|
|
0x07, 0x00, 0xE0, 0x3C, 0x38, 0x03, 0x81, 0xE1, 0xE0, 0x0E, 0x1F, 0x87, 0x00, 0x3F, 0xF8, 0x3F,
|
|
|
|
|
0xFF, 0xFF, 0xC0, 0xFF, 0xFC, 0xFF, 0xC0, 0x00, 0x1F, 0xFC, 0x00, 0x03, 0x87, 0xE0, 0x00, 0x70,
|
|
|
|
|
0x3C, 0x00, 0x0E, 0x03, 0xC7, 0xFF, 0xC0, 0x38, 0xFF, 0xF8, 0x07, 0x80, 0x7F, 0x00, 0xF0, 0x1E,
|
|
|
|
|
0xE0, 0x1E, 0x07, 0x9C, 0x03, 0xC1, 0xE3, 0x80, 0x78, 0x78, 0x70, 0x0E, 0x0E, 0x0E, 0x03, 0xC3,
|
|
|
|
|
0xC1, 0xC0, 0xF0, 0xF0, 0x38, 0x7E, 0x1C, 0x07, 0xFF, 0x07, 0xFF, 0xFF, 0xC0, 0xFF, 0xE0, 0x00,
|
|
|
|
|
0x70, 0x00, 0x00, 0x38, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x0E, 0x00, 0x00, 0x07, 0x00, 0x00, 0x03,
|
|
|
|
|
0x80, 0x00, 0x7F, 0xC7, 0xFE, 0x7F, 0xE3, 0xFF, 0x79, 0xF0, 0x07, 0x78, 0x38, 0x07, 0xB8, 0x1C,
|
|
|
|
|
0x07, 0x9C, 0x0E, 0x07, 0x8E, 0x07, 0x07, 0x87, 0x03, 0x83, 0x83, 0x81, 0xC3, 0xC1, 0xE0, 0xE3,
|
|
|
|
|
0xC0, 0x7C, 0x71, 0xC0, 0x1F, 0xF9, 0xFF, 0xC7, 0xFC, 0xFF, 0xE0, 0x00, 0x60, 0x01, 0xE0, 0x03,
|
|
|
|
|
0x80, 0x0E, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x03, 0xFC, 0x1F, 0xFC, 0x7E, 0x79, 0xE0, 0x03,
|
|
|
|
|
0xC0, 0x07, 0x00, 0x1E, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x1F, 0xE0, 0x3F, 0xC0, 0x7B, 0x80,
|
|
|
|
|
0xF7, 0x81, 0xE7, 0xE3, 0xC7, 0xFF, 0x83, 0xFE, 0x01, 0x80, 0x38, 0x07, 0x80, 0x70, 0x06, 0x00,
|
|
|
|
|
0x00, 0x00, 0x01, 0xFF, 0x3F, 0xF7, 0xCF, 0x70, 0x7E, 0x07, 0xE0, 0x7E, 0x07, 0xE0, 0x7E, 0x07,
|
|
|
|
|
0xF0, 0x77, 0xDF, 0x3F, 0xF1, 0xFF, 0x00, 0x70, 0x0F, 0x71, 0xE7, 0xFE, 0x7F, 0x80, 0xE0, 0x1C,
|
|
|
|
|
0x01, 0xC0, 0x38, 0x03, 0x80, 0x70, 0x07, 0x00, 0xE0, 0x0E, 0x01, 0xC0, 0xFC, 0x03, 0x81, 0xF8,
|
|
|
|
|
0x07, 0x03, 0xFF, 0xFE, 0x07, 0xFF, 0xFC, 0x0F, 0xC0, 0x38, 0x1F, 0x80, 0x70, 0x3F, 0x00, 0xE0,
|
|
|
|
|
0x7E, 0x01, 0xC0, 0xFC, 0x03, 0x83, 0xF8, 0x07, 0xCF, 0x70, 0x07, 0xFE, 0xE0, 0x07, 0xF0, 0x7F,
|
|
|
|
|
0xC7, 0xFF, 0x3C, 0x7D, 0xC0, 0xFE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x3F, 0x03, 0xF8,
|
|
|
|
|
0x1D, 0xC1, 0xEE, 0x1E, 0x73, 0xE3, 0xFC, 0x1F, 0xC0, 0xF8, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E,
|
|
|
|
|
0x00, 0x70, 0x00, 0x0C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x70, 0x0E, 0xF0, 0x1D, 0xF0, 0x3B, 0xF0, 0x77, 0xE0, 0xEE, 0xE1, 0xDD, 0xE3, 0xB9, 0xE7, 0x71,
|
|
|
|
|
0xCE, 0xE1, 0xDD, 0xC3, 0xFB, 0x83, 0xF7, 0x03, 0xEE, 0x07, 0xDC, 0x07, 0xB8, 0x07, 0x70, 0x0E,
|
|
|
|
|
0x38, 0x07, 0x00, 0x70, 0x07, 0x00, 0x60, 0x00, 0x00, 0x07, 0xFC, 0xFF, 0xDC, 0x7B, 0x87, 0xF0,
|
|
|
|
|
0x7E, 0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x70, 0x00, 0xC0, 0x01, 0xF0,
|
|
|
|
|
0x00, 0xF0, 0x00, 0x78, 0x00, 0x36, 0x00, 0x1B, 0x00, 0x0F, 0x80, 0x07, 0x80, 0x03, 0xE0, 0x03,
|
|
|
|
|
0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xC0, 0x70, 0xF0, 0x38, 0x38, 0x3C, 0x1C,
|
|
|
|
|
0x1F, 0xFF, 0x1F, 0xFF, 0x8E, 0x01, 0xC7, 0x00, 0xF7, 0x80, 0x3B, 0x80, 0x1E, 0x07, 0x03, 0xE0,
|
|
|
|
|
0x78, 0x07, 0x81, 0xB0, 0x36, 0x03, 0xC0, 0x00, 0x7F, 0x8F, 0xF9, 0x8F, 0x00, 0xE0, 0x0E, 0x7F,
|
|
|
|
|
0xDF, 0xFF, 0xCF, 0xE0, 0xFC, 0x1F, 0xC7, 0xFF, 0xF3, 0xFC, 0x00, 0x01, 0x80, 0x00, 0x07, 0x00,
|
|
|
|
|
0x00, 0x1C, 0x00, 0x00, 0x78, 0x00, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F,
|
|
|
|
|
0xFE, 0x00, 0x7F, 0xFC, 0x01, 0xFC, 0x00, 0x03, 0xB8, 0x00, 0x0F, 0x70, 0x00, 0x1C, 0xE0, 0x00,
|
|
|
|
|
0x71, 0xC0, 0x01, 0xE3, 0xFE, 0x03, 0x87, 0xFC, 0x0E, 0x0E, 0x00, 0x3C, 0x1C, 0x00, 0x7F, 0xF8,
|
|
|
|
|
0x01, 0xFF, 0xF0, 0x03, 0x80, 0xE0, 0x0F, 0x01, 0xC0, 0x1C, 0x03, 0xFF, 0xF0, 0x07, 0xFF, 0x00,
|
|
|
|
|
0x30, 0x00, 0x07, 0x80, 0x00, 0x70, 0x00, 0x0E, 0x00, 0x00, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x07, 0xF1, 0xF8, 0x7F, 0xBF, 0xC6, 0x3F, 0xBE, 0x01, 0xE0, 0xE0, 0x0E, 0x0E, 0x3F, 0xFF, 0xE7,
|
|
|
|
|
0xFF, 0xFE, 0xF9, 0xE0, 0x0E, 0x0E, 0x00, 0xE0, 0xF0, 0x0F, 0x1F, 0x8C, 0xFF, 0xFF, 0xC3, 0xF9,
|
|
|
|
|
0xFC, 0x00, 0x70, 0x00, 0x38, 0x00, 0x38, 0x00, 0x38, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x0C,
|
|
|
|
|
0x0F, 0xF7, 0x1F, 0xFF, 0x1F, 0x9F, 0x1E, 0x07, 0xCE, 0x07, 0xE7, 0x03, 0xFF, 0x83, 0x9F, 0x83,
|
|
|
|
|
0x8F, 0xC3, 0x87, 0xE3, 0xC3, 0xFB, 0xC1, 0xDD, 0xC0, 0xEF, 0xC0, 0xF7, 0xC0, 0xF1, 0xF9, 0xF1,
|
|
|
|
|
0xFF, 0xF0, 0xEF, 0xF0, 0x60, 0x00, 0x00, 0x01, 0x80, 0x1C, 0x01, 0xE0, 0x0E, 0x00, 0x60, 0x00,
|
|
|
|
|
0x00, 0x00, 0xC3, 0xFE, 0x3F, 0xF3, 0xCF, 0xBC, 0x7D, 0xC3, 0xFE, 0x3B, 0xF3, 0x9F, 0xB8, 0xFF,
|
|
|
|
|
0x8F, 0x78, 0x73, 0xEF, 0x9F, 0xF9, 0xFF, 0x8C, 0x00, 0x00, 0x19, 0x80, 0x0E, 0xE0, 0x03, 0xB8,
|
|
|
|
|
0x00, 0xEE, 0x00, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x80, 0x03, 0xE0, 0x01, 0xF0, 0x01,
|
|
|
|
|
0xF8, 0x00, 0xEE, 0x00, 0xE7, 0x00, 0x73, 0xC0, 0x78, 0xE0, 0x38, 0x78, 0x1C, 0x1C, 0x1E, 0x0E,
|
|
|
|
|
0x0F, 0xFF, 0x87, 0xFF, 0xC7, 0x00, 0xE3, 0x80, 0x7B, 0xC0, 0x1D, 0xC0, 0x0F, 0xE6, 0x1D, 0xC1,
|
|
|
|
|
0xDC, 0x1D, 0xC1, 0x98, 0x00, 0x00, 0x03, 0xFC, 0x7F, 0xCC, 0x78, 0x07, 0x00, 0x73, 0xFE, 0xFF,
|
|
|
|
|
0xFE, 0x7F, 0x07, 0xE0, 0xFE, 0x3F, 0xFF, 0x9F, 0xE0, 0x03, 0xC0, 0x03, 0xF0, 0x01, 0xDC, 0x01,
|
|
|
|
|
0xC6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x07, 0xC0, 0x03, 0xE0, 0x03, 0xF0, 0x01, 0xDC,
|
|
|
|
|
0x01, 0xCE, 0x00, 0xE7, 0x80, 0xF1, 0xC0, 0x70, 0xF0, 0x38, 0x38, 0x3C, 0x1C, 0x1F, 0xFF, 0x0F,
|
|
|
|
|
0xFF, 0x8E, 0x01, 0xC7, 0x00, 0xF7, 0x80, 0x3B, 0x80, 0x1E, 0x1F, 0x07, 0xF0, 0xEE, 0x18, 0xC0,
|
|
|
|
|
0x00, 0x00, 0x1F, 0xE3, 0xFE, 0x63, 0xC0, 0x38, 0x03, 0x9F, 0xF7, 0xFF, 0xF3, 0xF8, 0x3F, 0x07,
|
|
|
|
|
0xF1, 0xFF, 0xFC, 0xFF, 0x00, 0x66, 0x07, 0x70, 0x73, 0x83, 0xB8, 0x19, 0x80, 0x00, 0x00, 0x0F,
|
|
|
|
|
0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xCF, 0xFC, 0xE0, 0x0E, 0x00,
|
|
|
|
|
0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x66, 0x07, 0x70, 0x3B, 0x83, 0x98, 0x19,
|
|
|
|
|
0x80, 0x00, 0x00, 0x01, 0xF8, 0x3F, 0xE7, 0x9E, 0xF0, 0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E,
|
|
|
|
|
0x00, 0xF0, 0x07, 0xC6, 0x3F, 0xE1, 0xFE, 0x1F, 0x03, 0xF0, 0x3B, 0x83, 0x18, 0x00, 0x00, 0x00,
|
|
|
|
|
0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFC, 0xFF, 0xCE, 0x00, 0xE0,
|
|
|
|
|
0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFF, 0xFF, 0xF0, 0x0F, 0x01, 0xF8, 0x1D, 0xC3, 0x8C,
|
|
|
|
|
0x00, 0x00, 0x00, 0x1F, 0x83, 0xFE, 0x79, 0xEF, 0x0F, 0xE0, 0x7F, 0xFF, 0xFF, 0xFE, 0x00, 0xE0,
|
|
|
|
|
0x0F, 0x00, 0x7C, 0x63, 0xFE, 0x1F, 0xE0, 0xCC, 0x77, 0x1D, 0xC7, 0x73, 0x18, 0x00, 0x00, 0x1C,
|
|
|
|
|
0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
|
|
|
|
|
0x38, 0x1C, 0x66, 0x1D, 0xC3, 0xB8, 0xE6, 0x19, 0x80, 0x00, 0x00, 0x1C, 0x07, 0x01, 0xC0, 0x70,
|
|
|
|
|
0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x7D, 0xFB, 0xBE, 0x30, 0x00,
|
|
|
|
|
0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x00,
|
|
|
|
|
0x3C, 0x7E, 0x77, 0x63, 0x00, 0x00, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C,
|
|
|
|
|
0x1C, 0x1C, 0x1C, 0x0C, 0xC0, 0x0E, 0x70, 0x03, 0xB8, 0x00, 0xEE, 0x00, 0x33, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x0F, 0xF0, 0x1F, 0xFC, 0x1F, 0x9F, 0x1E, 0x03, 0xCE, 0x00, 0xF7, 0x00, 0x7F, 0x80,
|
|
|
|
|
0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF8, 0x01, 0xDC, 0x01, 0xEE, 0x00, 0xF7, 0x80, 0xF1,
|
|
|
|
|
0xF9, 0xF0, 0x7F, 0xF0, 0x0F, 0xF0, 0x66, 0x03, 0xB8, 0x0E, 0xE0, 0x33, 0x81, 0x98, 0x00, 0x00,
|
|
|
|
|
0x00, 0x03, 0xF8, 0x3F, 0xE3, 0xCF, 0x9C, 0x1D, 0xC0, 0xFE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x0F,
|
|
|
|
|
0x70, 0x73, 0xCF, 0x8F, 0xF8, 0x3F, 0x80, 0x01, 0xE0, 0x01, 0xF8, 0x01, 0xDC, 0x00, 0xC6, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x1F, 0xE0, 0x3F, 0xF8, 0x3F, 0x3E, 0x3C, 0x07, 0x9C, 0x01, 0xEE, 0x00,
|
|
|
|
|
0xFF, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xC0, 0x07, 0xF0, 0x03, 0xB8, 0x03, 0xDC, 0x01, 0xEF,
|
|
|
|
|
0x01, 0xE3, 0xF3, 0xE0, 0xFF, 0xE0, 0x1F, 0xE0, 0x0F, 0x80, 0xFC, 0x07, 0x70, 0x31, 0x80, 0x00,
|
|
|
|
|
0x00, 0x00, 0x7F, 0x07, 0xFC, 0x79, 0xF3, 0x83, 0xB8, 0x1F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x81,
|
|
|
|
|
0xEE, 0x0E, 0x79, 0xF1, 0xFF, 0x07, 0xF0, 0xCC, 0x03, 0xB8, 0x07, 0x70, 0x0C, 0xE0, 0x33, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x3F, 0xF0, 0xFF, 0xE3, 0x8F, 0xCE, 0x07, 0x38, 0x1E, 0xE0, 0x7B, 0x81, 0xCE,
|
|
|
|
|
0x1F, 0x3F, 0xF8, 0xFF, 0xC3, 0x8F, 0x0E, 0x1C, 0x38, 0x38, 0xE0, 0xF3, 0x81, 0xCE, 0x07, 0xB8,
|
|
|
|
|
0x0E, 0x66, 0x39, 0xC7, 0x70, 0xEE, 0x19, 0x80, 0x00, 0x00, 0xFF, 0x3F, 0xCE, 0x23, 0x80, 0xE0,
|
|
|
|
|
0x38, 0x0E, 0x03, 0x80, 0xE0, 0x38, 0x0E, 0x03, 0x80, 0xE0, 0x1F, 0x00, 0xFC, 0x03, 0xB8, 0x0C,
|
|
|
|
|
0x60, 0x00, 0x00, 0x00, 0x0F, 0xFC, 0x3F, 0xF8, 0xE3, 0xF3, 0x81, 0xCE, 0x07, 0xB8, 0x1E, 0xE0,
|
|
|
|
|
0x73, 0x87, 0xCF, 0xFE, 0x3F, 0xF0, 0xE3, 0xC3, 0x87, 0x0E, 0x0E, 0x38, 0x3C, 0xE0, 0x73, 0x81,
|
|
|
|
|
0xEE, 0x03, 0x80, 0x3C, 0x7E, 0xEE, 0xC6, 0x00, 0x00, 0xFF, 0xFF, 0xE2, 0xE0, 0xE0, 0xE0, 0xE0,
|
|
|
|
|
0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0xE0, 0x73, 0x01, 0xDC, 0x03, 0xB8, 0x07, 0x70, 0x0C, 0xC0, 0x00,
|
|
|
|
|
0x00, 0x00, 0x38, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03,
|
|
|
|
|
0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xB8, 0x0E, 0xF0, 0x79, 0xF3, 0xC3, 0xFF, 0x07, 0xF0,
|
|
|
|
|
0x66, 0x07, 0x70, 0x33, 0x83, 0xB8, 0x19, 0x80, 0x00, 0x00, 0x0F, 0x07, 0xF0, 0x7F, 0x07, 0xF0,
|
|
|
|
|
0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x77, 0x07, 0x70, 0x77, 0x8F, 0x3F, 0xF1, 0xFF, 0x0F, 0x80,
|
|
|
|
|
0x3F, 0x01, 0xDC, 0x06, 0x30, 0x00, 0x00, 0x00, 0x0E, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE,
|
|
|
|
|
0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xEE, 0x03, 0xBC,
|
|
|
|
|
0x1E, 0x7C, 0xF0, 0xFF, 0xC1, 0xFC, 0x00, 0x0F, 0x01, 0xF8, 0x1D, 0xC3, 0x9C, 0x00, 0x00, 0x00,
|
|
|
|
|
0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0xF0, 0x7F, 0x07, 0x70, 0x77, 0x07, 0x78,
|
|
|
|
|
0xF3, 0xFF, 0x1F, 0xF0, 0x1F, 0xE1, 0xFF, 0x9F, 0x1C, 0xE0, 0x07, 0x00, 0x3C, 0x00, 0xF8, 0x03,
|
|
|
|
|
0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x3C, 0x00, 0xF0, 0x03, 0x80, 0x3D, 0xE7, 0xCF, 0xFE, 0x3F, 0xC0,
|
|
|
|
|
0x00, 0x01, 0xC0, 0x1E, 0x00, 0xE0, 0x00, 0x3F, 0xBF, 0xEF, 0x33, 0x80, 0xF0, 0x3F, 0x03, 0xF0,
|
|
|
|
|
0x3E, 0x03, 0x80, 0xFE, 0x7B, 0xFE, 0xFF, 0x00, 0x01, 0xC1, 0xF0, 0x78, 0x00, 0xFF, 0xFF, 0xFF,
|
|
|
|
|
0xF0, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78,
|
|
|
|
|
0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x00, 0x00, 0x78, 0x03, 0xC0,
|
|
|
|
|
0x0F, 0x00, 0x70, 0x78, 0x3C, 0x1E, 0x0F, 0xF7, 0xFB, 0xC1, 0xE0, 0xF0, 0x78, 0x3C, 0x1E, 0x0F,
|
|
|
|
|
0x03, 0x81, 0xE4, 0xFF, 0x3F, 0x80, 0x07, 0x07, 0x83, 0x80, 0x7F, 0x87, 0xFE, 0x1C, 0xF8, 0x01,
|
|
|
|
|
0xC0, 0x0F, 0x00, 0x78, 0x03, 0x80, 0x3C, 0x07, 0xC0, 0xFC, 0x07, 0xF0, 0x0F, 0xC0, 0x0F, 0x00,
|
|
|
|
|
0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x07, 0x80, 0x78, 0x1F, 0xCF, 0xFC, 0x7F, 0x80, 0xFF, 0x3F,
|
|
|
|
|
0xEE, 0x78, 0x0F, 0x03, 0xC0, 0xE0, 0xF8, 0xFC, 0x3F, 0x83, 0xE0, 0x1C, 0x07, 0x01, 0xC0, 0x70,
|
|
|
|
|
0x7C, 0xFE, 0xFF, 0x3F, 0x00, 0x18, 0xC0, 0x73, 0x00, 0xF8, 0x01, 0xE0, 0x03, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x38, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7F, 0xFF, 0xFF,
|
|
|
|
|
0xFF, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xCC,
|
|
|
|
|
0x0F, 0xE0, 0x7C, 0x03, 0x80, 0x00, 0x00, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0x07,
|
|
|
|
|
0x00, 0x7F, 0xC7, 0xFE, 0x79, 0xE7, 0x0F, 0x70, 0x77, 0x07, 0x70, 0x77, 0x07, 0x70, 0x77, 0x07,
|
|
|
|
|
0x70, 0x77, 0x07, 0x70, 0x70, 0x7C, 0x7E, 0xEE, 0xC6, 0x18, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03,
|
|
|
|
|
0x00, 0x00, 0x00, 0x0F, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xCF,
|
|
|
|
|
0xFC, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0x3B, 0x87, 0xBC,
|
|
|
|
|
0x3B, 0x80, 0x00, 0x00, 0x0F, 0xFE, 0xFF, 0xEE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF,
|
|
|
|
|
0xCF, 0xFC, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, 0xFC,
|
|
|
|
|
0x3F, 0xFF, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x7F, 0xE0, 0x1F, 0xFE,
|
|
|
|
|
0x07, 0x8F, 0x81, 0xC0, 0xF0, 0x70, 0x1C, 0x1C, 0x07, 0x07, 0x01, 0xC1, 0xC0, 0xF0, 0x70, 0x7C,
|
|
|
|
|
0x1C, 0x7E, 0x07, 0x1F, 0x00, 0x03, 0x00, 0xE0, 0x38, 0x0F, 0x00, 0xC0, 0x00, 0x00, 0x07, 0xFF,
|
|
|
|
|
0xFF, 0xFC, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70,
|
|
|
|
|
0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0x07, 0xF8, 0x7F, 0xF3, 0xF1, 0xDE, 0x00, 0x70, 0x01, 0xC0,
|
|
|
|
|
0x0F, 0x00, 0x3F, 0xF8, 0xFF, 0xE3, 0x80, 0x0F, 0x00, 0x1C, 0x00, 0x70, 0x01, 0xE0, 0x03, 0xF1,
|
|
|
|
|
0xC7, 0xFF, 0x07, 0xF8, 0x1F, 0xE1, 0xFF, 0x9F, 0x1C, 0xE0, 0x07, 0x00, 0x3C, 0x00, 0xF8, 0x03,
|
|
|
|
|
0xE0, 0x07, 0xC0, 0x0F, 0x80, 0x3C, 0x00, 0xF0, 0x03, 0x80, 0x3D, 0xE7, 0xCF, 0xFE, 0x3F, 0xC0,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x67, 0x7B, 0xD9, 0xC0, 0x00, 0x00, 0xE0, 0x70, 0x38,
|
|
|
|
|
0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0,
|
|
|
|
|
0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80,
|
|
|
|
|
0x70, 0x0E, 0x03, 0xF9, 0xF7, 0xFE, 0x7F, 0x00, 0x07, 0xFF, 0x00, 0x01, 0xFF, 0xC0, 0x00, 0x70,
|
|
|
|
|
0x70, 0x00, 0x1C, 0x1C, 0x00, 0x07, 0x07, 0x00, 0x01, 0xC1, 0xC0, 0x00, 0x70, 0x7F, 0xC0, 0x1C,
|
|
|
|
|
0x1F, 0xFC, 0x07, 0x07, 0x1F, 0x81, 0xC1, 0xC0, 0xE0, 0xF0, 0x70, 0x38, 0x38, 0x1C, 0x0F, 0x0E,
|
|
|
|
|
0x07, 0x03, 0x87, 0x81, 0xC0, 0xE7, 0xC0, 0x71, 0xFB, 0xE0, 0x1F, 0xFC, 0xF0, 0x07, 0xFC, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0xE0, 0x38, 0x01, 0xC0, 0x70, 0x03, 0x80, 0xE0, 0x07, 0x01, 0xC0, 0x0E, 0x03,
|
|
|
|
|
0x80, 0x1C, 0x07, 0x00, 0x3F, 0xFF, 0xFC, 0x7F, 0xFF, 0xFE, 0xE0, 0x3C, 0x7D, 0xC0, 0x70, 0x3F,
|
|
|
|
|
0x80, 0xE0, 0x3F, 0x01, 0xC0, 0x7E, 0x03, 0x80, 0xFC, 0x07, 0x03, 0xF8, 0x0F, 0x1F, 0x70, 0x1F,
|
|
|
|
|
0xFC, 0xE0, 0x3F, 0xF0, 0xFF, 0xFC, 0x3F, 0xFF, 0x00, 0x70, 0x00, 0x1C, 0x00, 0x07, 0x00, 0x01,
|
|
|
|
|
0xC0, 0x00, 0x7F, 0xE0, 0x1F, 0xFC, 0x07, 0x8F, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07,
|
|
|
|
|
0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x80, 0x01, 0x80, 0x0E, 0x00, 0x70,
|
|
|
|
|
0x03, 0x80, 0x0E, 0x00, 0x00, 0x00, 0x00, 0x38, 0x1E, 0xE0, 0xF3, 0x87, 0x8E, 0x3C, 0x39, 0xE0,
|
|
|
|
|
0xEF, 0x03, 0xF8, 0x0F, 0xC0, 0x3F, 0x80, 0xFF, 0x03, 0xBE, 0x0E, 0x7C, 0x38, 0xF8, 0xE1, 0xF3,
|
|
|
|
|
0x83, 0xCE, 0x07, 0xB8, 0x0F, 0x0C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x1C, 0x00, 0x18, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x70, 0x0E, 0xE0, 0x3D, 0xC0, 0xFB, 0x81, 0xF7, 0x07, 0xEE, 0x1F, 0xDC, 0x7B, 0xB8,
|
|
|
|
|
0xE7, 0x73, 0x8E, 0xEF, 0x1D, 0xFC, 0x3B, 0xF0, 0x77, 0xE0, 0xEF, 0x81, 0xDE, 0x03, 0xBC, 0x07,
|
|
|
|
|
0x70, 0x0E, 0x0C, 0x70, 0x1D, 0xC0, 0x3F, 0x80, 0x3E, 0x00, 0x00, 0x00, 0x00, 0x3C, 0x07, 0xB8,
|
|
|
|
|
0x0E, 0x78, 0x1C, 0x70, 0x78, 0xF0, 0xE0, 0xE1, 0xC1, 0xE7, 0x01, 0xCE, 0x03, 0xFC, 0x03, 0xF0,
|
|
|
|
|
0x03, 0xE0, 0x07, 0x80, 0x07, 0x00, 0x1C, 0x04, 0xF8, 0x0F, 0xE0, 0x1F, 0x80, 0x00, 0xE0, 0x1F,
|
|
|
|
|
0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E,
|
|
|
|
|
0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xFF, 0xFF, 0xFF, 0xFC, 0x1C, 0x00, 0x70, 0x01,
|
|
|
|
|
0xC0, 0x07, 0x00, 0x1C, 0x00, 0x01, 0xC0, 0x01, 0xF0, 0x00, 0xF8, 0x00, 0xFC, 0x00, 0x77, 0x00,
|
|
|
|
|
0x73, 0x80, 0x39, 0xE0, 0x3C, 0x70, 0x1C, 0x3C, 0x0E, 0x0E, 0x0F, 0x07, 0x07, 0xFF, 0xC3, 0xFF,
|
|
|
|
|
0xE3, 0x80, 0x71, 0xC0, 0x3D, 0xE0, 0x0E, 0xE0, 0x07, 0x80, 0xFF, 0xE7, 0xFF, 0x38, 0x01, 0xC0,
|
|
|
|
|
0x0E, 0x00, 0x70, 0x03, 0xFF, 0x1F, 0xFC, 0xE3, 0xF7, 0x03, 0xF8, 0x1F, 0xC0, 0x7E, 0x07, 0xF0,
|
|
|
|
|
0x3B, 0x87, 0xDF, 0xFC, 0xFF, 0xC0, 0xFF, 0xC7, 0xFF, 0x38, 0xFD, 0xC0, 0xEE, 0x07, 0x70, 0x3B,
|
|
|
|
|
0x87, 0xDF, 0xFC, 0xFF, 0xF7, 0x0F, 0xB8, 0x1F, 0xC0, 0x7E, 0x03, 0xF0, 0x3F, 0x87, 0xDF, 0xFE,
|
|
|
|
|
0xFF, 0xC0, 0xFF, 0xFF, 0xFF, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03,
|
|
|
|
|
0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x00, 0x07, 0xFF, 0x01, 0xFF, 0xC0, 0x70,
|
|
|
|
|
0x70, 0x1C, 0x1C, 0x07, 0x07, 0x01, 0xC1, 0xC0, 0x70, 0x70, 0x1C, 0x1C, 0x07, 0x07, 0x01, 0xC1,
|
|
|
|
|
0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x1E, 0x07, 0x07, 0x01, 0xC3, 0xC0, 0x73, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
|
|
|
0xF8, 0x00, 0x7E, 0x00, 0x1F, 0x80, 0x07, 0xE0, 0x01, 0xF8, 0x00, 0x70, 0xFF, 0xEF, 0xFE, 0xE0,
|
|
|
|
|
0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFC, 0xFF, 0xCE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E,
|
|
|
|
|
0x00, 0xE0, 0x0F, 0xFF, 0xFF, 0xF0, 0x78, 0x38, 0x3C, 0x78, 0x70, 0xF0, 0x70, 0xE3, 0xC0, 0x71,
|
|
|
|
|
0xCF, 0x00, 0xF3, 0xBC, 0x00, 0xF7, 0xF0, 0x00, 0xFF, 0xC0, 0x00, 0xFF, 0x80, 0x03, 0xFF, 0x00,
|
|
|
|
|
0x0F, 0x77, 0x00, 0x1C, 0xE7, 0x00, 0x71, 0xCF, 0x01, 0xC3, 0x8F, 0x07, 0x87, 0x0E, 0x0E, 0x0E,
|
|
|
|
|
0x1E, 0x3C, 0x1C, 0x1E, 0xF0, 0x38, 0x1C, 0x3F, 0xC1, 0xFF, 0xC3, 0x9F, 0x00, 0x1E, 0x00, 0x78,
|
|
|
|
|
0x01, 0xC0, 0x1F, 0x07, 0xF8, 0x1F, 0xF0, 0x07, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xE7,
|
|
|
|
|
0x9F, 0x9F, 0xFC, 0x3F, 0xE0, 0xE0, 0x1D, 0xC0, 0x7B, 0x81, 0xF7, 0x03, 0xEE, 0x0F, 0xDC, 0x3F,
|
|
|
|
|
0xB8, 0xF7, 0x71, 0xCE, 0xE7, 0x1D, 0xDE, 0x3B, 0xF8, 0x77, 0xE0, 0xEF, 0xC1, 0xDF, 0x03, 0xBC,
|
|
|
|
|
0x07, 0x78, 0x0E, 0xE0, 0x1C, 0x1C, 0x60, 0x3D, 0xC0, 0x3F, 0x00, 0x3E, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x38, 0x07, 0x70, 0x1E, 0xE0, 0x7D, 0xC0, 0xFB, 0x83, 0xF7, 0x0F, 0xEE, 0x3D, 0xDC, 0x73, 0xB9,
|
|
|
|
|
0xC7, 0x77, 0x8E, 0xFE, 0x1D, 0xF8, 0x3B, 0xF0, 0x77, 0xC0, 0xEF, 0x01, 0xDE, 0x03, 0xB8, 0x07,
|
|
|
|
|
0x00, 0xE0, 0x7B, 0x83, 0xCE, 0x1E, 0x38, 0xF0, 0xE7, 0x83, 0xBC, 0x0F, 0xE0, 0x3F, 0x00, 0xFE,
|
|
|
|
|
0x03, 0xFC, 0x0E, 0xF8, 0x39, 0xF0, 0xE3, 0xE3, 0x87, 0xCE, 0x0F, 0x38, 0x1E, 0xE0, 0x3C, 0x07,
|
|
|
|
|
0xFF, 0x07, 0xFF, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07, 0x07,
|
|
|
|
|
0x07, 0x07, 0x07, 0x0F, 0x07, 0x0E, 0x07, 0x0E, 0x07, 0x1E, 0x07, 0x7C, 0x07, 0xF8, 0x07, 0xF0,
|
|
|
|
|
0x07, 0x38, 0x01, 0xC7, 0x80, 0x1C, 0x7C, 0x03, 0xC7, 0xC0, 0x3E, 0x7E, 0x06, 0xE7, 0x60, 0x6E,
|
|
|
|
|
0x77, 0x0C, 0xE7, 0x30, 0xCE, 0x73, 0x1C, 0xE7, 0x39, 0x8E, 0x71, 0x98, 0xE7, 0x1F, 0x8E, 0x70,
|
|
|
|
|
0xF0, 0xEF, 0x0F, 0x0E, 0xF0, 0xE0, 0xEE, 0x00, 0x0E, 0xE0, 0x00, 0xE0, 0xE0, 0x1F, 0x80, 0x7E,
|
|
|
|
|
0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x80, 0x7E, 0x01, 0xF8,
|
|
|
|
|
0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1C, 0x07, 0xF8, 0x0F, 0xFE, 0x0F, 0xCF,
|
|
|
|
|
0x8F, 0x01, 0xE7, 0x00, 0x7B, 0x80, 0x3F, 0xC0, 0x0F, 0xC0, 0x07, 0xE0, 0x03, 0xF0, 0x01, 0xFC,
|
|
|
|
|
0x00, 0xEE, 0x00, 0xF7, 0x00, 0x7B, 0xC0, 0x78, 0xFC, 0xF8, 0x3F, 0xF8, 0x07, 0xF8, 0x00, 0xFF,
|
|
|
|
|
0xFF, 0xFF, 0xFE, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80,
|
|
|
|
|
0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1C, 0xFF, 0x87, 0xFF,
|
|
|
|
|
0x38, 0xFD, 0xC0, 0xEE, 0x07, 0xF0, 0x3F, 0x81, 0xFC, 0x0E, 0xE3, 0xF7, 0xFF, 0x3F, 0xF1, 0xC0,
|
|
|
|
|
0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x00, 0x07, 0xF8, 0x7F, 0xF3, 0xF3, 0xDE, 0x00,
|
|
|
|
|
0x78, 0x01, 0xC0, 0x0F, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0x0F, 0x00, 0x3C, 0x00, 0x70, 0x01,
|
|
|
|
|
0xE0, 0x03, 0xF1, 0xC7, 0xFF, 0x07, 0xF8, 0xFF, 0xFF, 0xFF, 0xF0, 0x78, 0x01, 0xE0, 0x07, 0x80,
|
|
|
|
|
0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00,
|
|
|
|
|
0x78, 0x01, 0xE0, 0x07, 0x80, 0xF0, 0x1E, 0xE0, 0x39, 0xE0, 0x71, 0xC1, 0xE3, 0xC3, 0x83, 0x87,
|
|
|
|
|
0x07, 0x9C, 0x07, 0x38, 0x0F, 0xF0, 0x0F, 0xC0, 0x0F, 0x80, 0x1E, 0x00, 0x1C, 0x00, 0x70, 0x13,
|
|
|
|
|
0xE0, 0x3F, 0x80, 0x7E, 0x00, 0x00, 0xE0, 0x00, 0x0E, 0x00, 0x07, 0xFE, 0x01, 0xFF, 0xF8, 0x3E,
|
|
|
|
|
0xEF, 0xC7, 0x8E, 0x1E, 0x70, 0xE0, 0xEF, 0x0E, 0x0E, 0xE0, 0xE0, 0xFE, 0x0E, 0x0F, 0xF0, 0xE0,
|
|
|
|
|
0xE7, 0x0E, 0x0E, 0x78, 0xE1, 0xE3, 0xEE, 0xFC, 0x1F, 0xFF, 0x80, 0xFF, 0xE0, 0x00, 0xE0, 0x00,
|
|
|
|
|
0x0E, 0x00, 0x70, 0x1E, 0x78, 0x1C, 0x3C, 0x3C, 0x1C, 0x78, 0x1E, 0x70, 0x0F, 0xF0, 0x07, 0xE0,
|
|
|
|
|
0x03, 0xC0, 0x03, 0xC0, 0x07, 0xE0, 0x0F, 0xE0, 0x0E, 0xF0, 0x1E, 0x78, 0x3C, 0x3C, 0x38, 0x1C,
|
|
|
|
|
0x78, 0x1E, 0xF0, 0x0E, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C,
|
|
|
|
|
0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C,
|
|
|
|
|
0xE0, 0x1C, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07,
|
|
|
|
|
0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFE, 0x07, 0x78, 0x7B, 0xFF,
|
|
|
|
|
0xC7, 0xFE, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0xE0, 0x70, 0x3F, 0x03,
|
|
|
|
|
0x81, 0xF8, 0x1C, 0x0F, 0xC0, 0xE0, 0x7E, 0x07, 0x03, 0xF0, 0x38, 0x1F, 0x81, 0xC0, 0xFC, 0x0E,
|
|
|
|
|
0x07, 0xE0, 0x70, 0x3F, 0x03, 0x81, 0xF8, 0x1C, 0x0F, 0xC0, 0xE0, 0x7E, 0x07, 0x03, 0xF0, 0x38,
|
|
|
|
|
0x1F, 0x81, 0xC0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xF8, 0xE0, 0x70, 0x39, 0xC0, 0xE0, 0x73, 0x81,
|
|
|
|
|
0xC0, 0xE7, 0x03, 0x81, 0xCE, 0x07, 0x03, 0x9C, 0x0E, 0x07, 0x38, 0x1C, 0x0E, 0x70, 0x38, 0x1C,
|
|
|
|
|
0xE0, 0x70, 0x39, 0xC0, 0xE0, 0x73, 0x81, 0xC0, 0xE7, 0x03, 0x81, 0xCE, 0x07, 0x03, 0x9C, 0x0E,
|
|
|
|
|
0x07, 0x38, 0x1C, 0x0E, 0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x38,
|
|
|
|
|
0x00, 0x00, 0x70, 0x00, 0x00, 0xE0, 0x00, 0x01, 0xC0, 0xFF, 0x00, 0x3F, 0xC0, 0x00, 0x70, 0x00,
|
|
|
|
|
0x1C, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x00, 0x7F, 0xC0, 0x1F, 0xFC, 0x07, 0x1F, 0x81, 0xC0, 0xE0,
|
|
|
|
|
0x70, 0x38, 0x1C, 0x0F, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x71, 0xF8, 0x1F, 0xFC, 0x07, 0xFC, 0x00,
|
|
|
|
|
0xE0, 0x03, 0xF8, 0x00, 0xFE, 0x00, 0x3F, 0x80, 0x0F, 0xE0, 0x03, 0xF8, 0x00, 0xFF, 0xF8, 0x3F,
|
|
|
|
|
0xFF, 0x8F, 0xE3, 0xF3, 0xF8, 0x1C, 0xFE, 0x07, 0x3F, 0x81, 0xEF, 0xE0, 0x73, 0xF8, 0x1C, 0xFE,
|
|
|
|
|
0x3F, 0x3F, 0xFF, 0x8F, 0xFF, 0x83, 0xC0, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70,
|
|
|
|
|
0x03, 0xFF, 0x1F, 0xFC, 0xE3, 0xF7, 0x03, 0xB8, 0x1F, 0xC0, 0x7E, 0x07, 0xF0, 0x3B, 0x87, 0xDF,
|
|
|
|
|
0xFC, 0xFF, 0xC0, 0x7F, 0xC1, 0xFF, 0xC1, 0xC7, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0x80, 0x07,
|
|
|
|
|
0x0F, 0xFE, 0x1F, 0xFC, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x03, 0xC0, 0x0F, 0x1C, 0x7C, 0x7F,
|
|
|
|
|
0xF0, 0x7F, 0xC0, 0xE0, 0x1F, 0xC1, 0xC0, 0xFF, 0xE3, 0x83, 0xE7, 0xE7, 0x0F, 0x01, 0xCE, 0x1C,
|
|
|
|
|
0x03, 0xDC, 0x78, 0x03, 0xB8, 0xF0, 0x07, 0x7F, 0xC0, 0x0F, 0xFF, 0x80, 0x1F, 0xC7, 0x00, 0x3F,
|
|
|
|
|
0x8F, 0x00, 0x77, 0x0E, 0x00, 0xEE, 0x1C, 0x03, 0xDC, 0x3C, 0x07, 0x38, 0x3E, 0x7E, 0x70, 0x3F,
|
|
|
|
|
0xF8, 0xE0, 0x1F, 0xC0, 0x0F, 0xFC, 0x7F, 0xF3, 0xE1, 0xDE, 0x07, 0x70, 0x1D, 0xC0, 0x77, 0x81,
|
|
|
|
|
0xCF, 0x87, 0x3F, 0xFC, 0x3F, 0xF0, 0xE1, 0xC7, 0x87, 0x1C, 0x1C, 0xF0, 0x73, 0x81, 0xDE, 0x07,
|
|
|
|
|
0x70, 0x1C, 0x7F, 0x8F, 0xF9, 0x8F, 0x00, 0xE0, 0x0E, 0x7F, 0xDF, 0xFF, 0xCF, 0xE0, 0xFC, 0x1F,
|
|
|
|
|
0xC7, 0xFF, 0xF3, 0xFC, 0x03, 0xF0, 0x7F, 0x87, 0xF8, 0x78, 0x07, 0x80, 0x38, 0x01, 0xFF, 0x1F,
|
|
|
|
|
0xFC, 0xFD, 0xF7, 0x83, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF8, 0x1D, 0xC1, 0xEE, 0x0E, 0x7C, 0xF1,
|
|
|
|
|
0xFF, 0x07, 0xF0, 0xFF, 0x9F, 0xFB, 0x8F, 0x70, 0xEE, 0x3D, 0xFF, 0x3F, 0xF7, 0x1F, 0xE0, 0xFC,
|
|
|
|
|
0x1F, 0x8F, 0xFF, 0xEF, 0xF8, 0xFF, 0x7F, 0xB8, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70,
|
|
|
|
|
0x38, 0x1C, 0x0E, 0x00, 0x0F, 0xF8, 0x1F, 0xF0, 0x38, 0xE0, 0x71, 0xC0, 0xE3, 0x81, 0x87, 0x07,
|
|
|
|
|
0x0E, 0x0E, 0x1C, 0x1C, 0x38, 0x70, 0x70, 0xE0, 0xE7, 0xFF, 0xFF, 0xFF, 0xFC, 0x01, 0xF8, 0x03,
|
|
|
|
|
0xF0, 0x07, 0xE0, 0x0E, 0x1F, 0x83, 0xFE, 0x79, 0xEF, 0x0F, 0xE0, 0x7F, 0xFF, 0xFF, 0xFE, 0x00,
|
|
|
|
|
0xE0, 0x0F, 0x00, 0x7C, 0x63, 0xFE, 0x1F, 0xE0, 0x78, 0xE1, 0xC7, 0x1C, 0x70, 0x73, 0x9C, 0x07,
|
|
|
|
|
0x77, 0x00, 0x7F, 0xC0, 0x07, 0xF8, 0x01, 0xFF, 0x00, 0x77, 0x70, 0x1C, 0xE7, 0x07, 0x9C, 0xF0,
|
|
|
|
|
0xE3, 0x8F, 0x38, 0x70, 0xEF, 0x0E, 0x1E, 0xFF, 0x3F, 0xEE, 0x7C, 0x07, 0x03, 0xCF, 0xE3, 0xF8,
|
|
|
|
|
0x0F, 0x01, 0xC0, 0x7E, 0x3F, 0xFF, 0xFF, 0x00, 0xE0, 0xFC, 0x3F, 0x8F, 0xF1, 0xFE, 0x7F, 0xDD,
|
|
|
|
|
0xFB, 0xBF, 0xE7, 0xF8, 0xFF, 0x1F, 0xC3, 0xF0, 0x7E, 0x0E, 0x71, 0x87, 0x70, 0xFE, 0x0F, 0x80,
|
|
|
|
|
0x00, 0x00, 0x38, 0x3F, 0x0F, 0xE3, 0xFC, 0x7F, 0x9F, 0xF7, 0x7E, 0xEF, 0xF9, 0xFE, 0x3F, 0xC7,
|
|
|
|
|
0xF0, 0xFC, 0x1F, 0x83, 0x80, 0xE1, 0xEE, 0x3C, 0xE7, 0x8E, 0xF0, 0xFE, 0x0F, 0xC0, 0xFC, 0x0F,
|
|
|
|
|
0xE0, 0xEF, 0x0E, 0x78, 0xE3, 0xCE, 0x1C, 0xE0, 0xE0, 0x0F, 0xF8, 0x7F, 0xC3, 0x8E, 0x1C, 0x70,
|
|
|
|
|
0xE3, 0x87, 0x1C, 0x38, 0xE3, 0x87, 0x1C, 0x39, 0xE1, 0xDE, 0x0F, 0xF0, 0x7E, 0x03, 0x80, 0x70,
|
|
|
|
|
0x0E, 0x78, 0x1E, 0x78, 0x1E, 0x7C, 0x3E, 0x7C, 0x36, 0x76, 0x77, 0x76, 0x67, 0x67, 0xE7, 0x63,
|
|
|
|
|
0xC7, 0xE3, 0xC7, 0xE1, 0xC7, 0xE0, 0x07, 0xE0, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0F,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0E, 0x1F, 0xC1, 0xFF, 0x1E, 0x7C,
|
|
|
|
|
0xE0, 0xEE, 0x07, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x7B, 0x83, 0x9E, 0x7C, 0x7F, 0xC1, 0xFC,
|
|
|
|
|
0x00, 0xFF, 0xFF, 0xFF, 0x83, 0xF0, 0x7E, 0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83,
|
|
|
|
|
0xF0, 0x7E, 0x0E, 0xFF, 0x8F, 0xFC, 0xE3, 0xEE, 0x0E, 0xE0, 0xFE, 0x07, 0xE0, 0x7E, 0x07, 0xE0,
|
|
|
|
|
0xFE, 0x0E, 0xF3, 0xEF, 0xFC, 0xFF, 0x8E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x1F, 0xC7,
|
|
|
|
|
0xF9, 0xF3, 0x38, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1E, 0x01, 0xF3, 0x1F, 0xE1, 0xFC,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E,
|
|
|
|
|
0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x7F, 0x07, 0x70, 0x77, 0x0E, 0x78, 0xE3, 0x8E, 0x38, 0xC1, 0xDC,
|
|
|
|
|
0x1D, 0xC1, 0xF8, 0x0F, 0x80, 0xF8, 0x07, 0x00, 0x70, 0x07, 0x09, 0xE0, 0xFC, 0x0F, 0x80, 0x01,
|
|
|
|
|
0xC0, 0x00, 0xE0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x3F, 0xE0, 0x7F, 0xFC,
|
|
|
|
|
0x7D, 0xDF, 0x38, 0xE3, 0xF8, 0x70, 0xFC, 0x38, 0x7E, 0x1C, 0x3F, 0x0E, 0x1F, 0x87, 0x0E, 0xE3,
|
|
|
|
|
0x8F, 0x7D, 0xDF, 0x1F, 0xFF, 0x03, 0xFE, 0x00, 0x38, 0x00, 0x1C, 0x00, 0x0E, 0x00, 0x07, 0x00,
|
|
|
|
|
0x03, 0x80, 0x70, 0x73, 0xC7, 0x0E, 0x78, 0x3B, 0x81, 0xF8, 0x07, 0x80, 0x3C, 0x01, 0xF0, 0x1F,
|
|
|
|
|
0xC1, 0xEF, 0x0E, 0x38, 0xE1, 0xEF, 0x07, 0x00, 0xE0, 0xE7, 0x07, 0x38, 0x39, 0xC1, 0xCE, 0x0E,
|
|
|
|
|
0x70, 0x73, 0x83, 0x9C, 0x1C, 0xE0, 0xE7, 0x07, 0x38, 0x39, 0xFF, 0xFF, 0xFF, 0x80, 0x1C, 0x00,
|
|
|
|
|
0xE0, 0x07, 0x00, 0x38, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0F, 0xE3, 0xDF, 0xF9, 0xFF, 0x00,
|
|
|
|
|
0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0xE1, 0xC3, 0xF0, 0xE1, 0xF8, 0x70, 0xFC, 0x38, 0x7E, 0x1C,
|
|
|
|
|
0x3F, 0x0E, 0x1F, 0x87, 0x0F, 0xC3, 0x87, 0xE1, 0xC3, 0xF0, 0xE1, 0xF8, 0x70, 0xFF, 0xFF, 0xFF,
|
|
|
|
|
0xFF, 0xF8, 0xE1, 0xC3, 0x9C, 0x38, 0x73, 0x87, 0x0E, 0x70, 0xE1, 0xCE, 0x1C, 0x39, 0xC3, 0x87,
|
|
|
|
|
0x38, 0x70, 0xE7, 0x0E, 0x1C, 0xE1, 0xC3, 0x9C, 0x38, 0x73, 0x87, 0x0E, 0x7F, 0xFF, 0xFF, 0xFF,
|
|
|
|
|
0xFE, 0x00, 0x01, 0xC0, 0x00, 0x38, 0x00, 0x07, 0x00, 0x00, 0xE0, 0xFC, 0x01, 0xF8, 0x00, 0x70,
|
|
|
|
|
0x00, 0xE0, 0x01, 0xFF, 0x03, 0xFF, 0x87, 0x8F, 0x0E, 0x0E, 0x1C, 0x1E, 0x38, 0x38, 0x78, 0xF0,
|
|
|
|
|
0xFF, 0xE0, 0xFF, 0x00, 0xE0, 0x0F, 0xC0, 0x1F, 0x80, 0x3F, 0x00, 0x7E, 0x00, 0xFF, 0xF1, 0xFF,
|
|
|
|
|
0xF3, 0xF1, 0xE7, 0xE1, 0xEF, 0xC3, 0xDF, 0x8F, 0x3F, 0xFE, 0x7F, 0xF8, 0xE0, 0xE0, 0x1C, 0x03,
|
|
|
|
|
0x80, 0x70, 0x0F, 0xF9, 0xFF, 0xB8, 0xF7, 0x0F, 0xE0, 0xFC, 0x3F, 0x8F, 0x7F, 0xEF, 0xF8, 0x7F,
|
|
|
|
|
0x07, 0xFC, 0x63, 0xC0, 0x0E, 0x00, 0xE1, 0xFE, 0x1F, 0xF0, 0x0E, 0x00, 0xE0, 0x1E, 0x63, 0xE7,
|
|
|
|
|
0xFC, 0x7F, 0x00, 0xE0, 0xFC, 0x71, 0xFF, 0x38, 0xF3, 0xDC, 0xF0, 0xFE, 0x70, 0x3F, 0xF8, 0x1F,
|
|
|
|
|
0xFC, 0x0F, 0xCE, 0x07, 0xE7, 0x03, 0xF3, 0xC3, 0xF8, 0xF3, 0xDC, 0x3F, 0xCE, 0x0F, 0xC0, 0x1F,
|
|
|
|
|
0xF3, 0xFF, 0x3C, 0x77, 0x07, 0x70, 0x77, 0xC7, 0x3F, 0xF1, 0xFF, 0x1C, 0x73, 0x87, 0x38, 0x77,
|
|
|
|
|
0x07, 0x70, 0x70, 0x1C, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x00, 0x00, 0x00, 0x01, 0xF8, 0x3F,
|
|
|
|
|
0xE7, 0x9E, 0xF0, 0xFE, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xF0, 0x07, 0xC6, 0x3F, 0xE1,
|
|
|
|
|
0xFE, 0x39, 0xC3, 0xDC, 0x39, 0xC0, 0x00, 0x00, 0x01, 0xF8, 0x3F, 0xE7, 0x9E, 0xF0, 0xFE, 0x07,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x00, 0xF0, 0x07, 0xC6, 0x3F, 0xE1, 0xFE, 0x38, 0x01, 0xC0, 0x3F,
|
|
|
|
|
0xE1, 0xFF, 0x03, 0x80, 0x1C, 0x00, 0xFF, 0x87, 0xFE, 0x3C, 0xF1, 0xC3, 0xCE, 0x0E, 0x70, 0x73,
|
|
|
|
|
0x83, 0x9C, 0x1C, 0xE0, 0xE7, 0x07, 0x38, 0x39, 0xC1, 0xCE, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x3C,
|
|
|
|
|
0x07, 0xC0, 0x3E, 0x07, 0x03, 0x83, 0x83, 0x81, 0x80, 0x00, 0x01, 0xFE, 0xFF, 0x70, 0x38, 0x1C,
|
|
|
|
|
0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x00, 0x1F, 0xC7, 0xF9, 0xF3, 0x38, 0x0E,
|
|
|
|
|
0x01, 0xFE, 0x3F, 0xC7, 0x00, 0xE0, 0x1E, 0x01, 0xF3, 0x9F, 0xF1, 0xFE, 0x3F, 0xBF, 0xEF, 0x33,
|
|
|
|
|
0x80, 0xF0, 0x3F, 0x03, 0xF0, 0x3E, 0x03, 0x80, 0xFE, 0x7B, 0xFE, 0xFF, 0x00, 0xFF, 0x00, 0x07,
|
|
|
|
|
0x77, 0x77, 0x77, 0x77, 0x77, 0x77, 0xE7, 0x7B, 0xB9, 0xC0, 0x00, 0x00, 0xE0, 0x70, 0x38, 0x1C,
|
|
|
|
|
0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x00, 0x1E, 0x3C, 0x00, 0x00, 0x01,
|
|
|
|
|
0xC3, 0x87, 0x0E, 0x1C, 0x38, 0x70, 0xE1, 0xC3, 0x87, 0x0E, 0x1C, 0x38, 0xF3, 0xDF, 0xBE, 0x00,
|
|
|
|
|
0x0F, 0xF8, 0x00, 0x7F, 0xC0, 0x03, 0x8E, 0x00, 0x1C, 0x70, 0x00, 0xE3, 0x80, 0x07, 0x1F, 0xF0,
|
|
|
|
|
0x38, 0xFF, 0xC3, 0x87, 0x9F, 0x1C, 0x38, 0x39, 0xE1, 0xC1, 0xDE, 0x0E, 0x1F, 0xF0, 0x7F, 0xEE,
|
|
|
|
|
0x03, 0xFE, 0x00, 0xE0, 0xE0, 0x1C, 0x1C, 0x03, 0x83, 0x80, 0x70, 0x70, 0x0E, 0x0E, 0x01, 0xC1,
|
|
|
|
|
0xFF, 0x3F, 0xFF, 0xF7, 0xFF, 0x1F, 0xE0, 0xE0, 0xFC, 0x1C, 0x1F, 0x83, 0x8F, 0xF0, 0x7F, 0xEE,
|
|
|
|
|
0x0F, 0xF8, 0x38, 0x01, 0xC0, 0x3F, 0xE1, 0xFF, 0x03, 0x80, 0x1C, 0x00, 0xFF, 0x87, 0xFE, 0x3C,
|
|
|
|
|
0xF1, 0xC3, 0xCE, 0x0E, 0x70, 0x73, 0x83, 0x9C, 0x1C, 0xE0, 0xE7, 0x07, 0x38, 0x39, 0xC1, 0xCE,
|
|
|
|
|
0x0E, 0x03, 0x00, 0x70, 0x0E, 0x01, 0xC0, 0x1C, 0x00, 0x00, 0x00, 0x0E, 0x1E, 0xE3, 0xCE, 0x78,
|
|
|
|
|
0xEF, 0x0F, 0xE0, 0xFC, 0x0F, 0xC0, 0xFE, 0x0E, 0xF0, 0xE7, 0x8E, 0x3C, 0xE1, 0xCE, 0x0E, 0x18,
|
|
|
|
|
0x07, 0x80, 0x70, 0x07, 0x00, 0x60, 0x00, 0x00, 0x07, 0x07, 0xE1, 0xFC, 0x7F, 0x8F, 0xF3, 0xFE,
|
|
|
|
|
0xEF, 0xDD, 0xFF, 0x3F, 0xC7, 0xF8, 0xFE, 0x1F, 0x83, 0xF0, 0x70, 0x18, 0xE1, 0xDC, 0x1F, 0xC0,
|
|
|
|
|
0xF8, 0x00, 0x00, 0x00, 0xE0, 0x7F, 0x07, 0x70, 0x77, 0x0E, 0x78, 0xE3, 0x8E, 0x38, 0xC1, 0xDC,
|
|
|
|
|
0x1D, 0xC1, 0xF8, 0x0F, 0x80, 0xF8, 0x07, 0x00, 0x70, 0x07, 0x09, 0xE0, 0xFC, 0x0F, 0x80, 0xE0,
|
|
|
|
|
0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0F, 0xC1, 0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0xFF, 0xFF,
|
|
|
|
|
0xFE, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x0F, 0xFF, 0x83, 0xFF, 0xE0,
|
|
|
|
|
0x07, 0x00, 0x01, 0xC0, 0x00, 0x70, 0x00, 0x1F, 0xF0, 0x07, 0xFF, 0x01, 0xC7, 0xE0, 0x70, 0x38,
|
|
|
|
|
0x1C, 0x0E, 0x07, 0x03, 0xC1, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x7E, 0x07, 0xFF, 0x01, 0xFF, 0x00,
|
|
|
|
|
0x1C, 0x00, 0x38, 0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0x3F, 0xF8, 0x7F, 0xF0, 0x1C,
|
|
|
|
|
0x00, 0x38, 0x00, 0x7F, 0xC0, 0xFF, 0xE1, 0xE3, 0xC3, 0x83, 0x87, 0x07, 0x8E, 0x0E, 0x1E, 0x3C,
|
|
|
|
|
0x3F, 0xF8, 0x3F, 0xC0, 0x07, 0xF8, 0x0F, 0xFE, 0x0F, 0xCF, 0x8F, 0x01, 0xE7, 0x80, 0x7B, 0x80,
|
|
|
|
|
0x3F, 0xC0, 0x0F, 0xFF, 0x07, 0xFF, 0xE7, 0xFC, 0xFF, 0xFC, 0x1F, 0xEE, 0x00, 0x77, 0x00, 0x7B,
|
|
|
|
|
0xC0, 0x78, 0xF8, 0xF8, 0x3F, 0xF8, 0x07, 0xF8, 0x00, 0x1F, 0xC1, 0xFF, 0x1E, 0x7C, 0xE0, 0xEF,
|
|
|
|
|
0xE7, 0xFF, 0xBF, 0xDF, 0xFC, 0x7F, 0xE0, 0x7B, 0x83, 0x9E, 0x7C, 0x7F, 0xC1, 0xFC, 0x00, 0xE0,
|
|
|
|
|
0x03, 0xEF, 0x00, 0x7E, 0x70, 0x0F, 0x67, 0x80, 0xE0, 0x78, 0x0E, 0x03, 0x81, 0xC0, 0x3C, 0x1C,
|
|
|
|
|
0x01, 0xC3, 0xC0, 0x1C, 0x38, 0x01, 0xE3, 0x80, 0x0E, 0x70, 0x00, 0xF7, 0x00, 0x07, 0xF0, 0x00,
|
|
|
|
|
0x7E, 0x00, 0x03, 0xE0, 0x00, 0x3C, 0x00, 0x03, 0xC0, 0x00, 0xE0, 0x3D, 0xE0, 0xF9, 0xC1, 0xC3,
|
|
|
|
|
0x87, 0x07, 0x8E, 0x07, 0x1C, 0x0E, 0x70, 0x0E, 0xE0, 0x1F, 0x80, 0x3F, 0x00, 0x3E, 0x00, 0x78,
|
|
|
|
|
0x00, 0x70, 0x00, 0x1C, 0x60, 0x0F, 0x70, 0x03, 0xF0, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x03,
|
|
|
|
|
0x80, 0x71, 0xC0, 0x78, 0xE0, 0x7C, 0x70, 0x3E, 0x38, 0x3F, 0x1C, 0x3F, 0x8E, 0x3D, 0xC7, 0x1C,
|
|
|
|
|
0xE3, 0x9C, 0x71, 0xDE, 0x38, 0xFE, 0x1C, 0x7E, 0x0E, 0x3F, 0x07, 0x1F, 0x03, 0x8F, 0x01, 0xC7,
|
|
|
|
|
0x80, 0xFF, 0x80, 0x7E, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x07, 0x00, 0x03, 0x80, 0x01, 0x80, 0x71,
|
|
|
|
|
0x80, 0xEE, 0x03, 0xF8, 0x07, 0xC0, 0x00, 0x00, 0x00, 0x0E, 0x0E, 0x38, 0x78, 0xE3, 0xE3, 0x8F,
|
|
|
|
|
0x8E, 0x7E, 0x3B, 0xB8, 0xEE, 0xE3, 0xF3, 0x8F, 0x8E, 0x3E, 0x38, 0xF0, 0xE3, 0x83, 0xEE, 0x0F,
|
|
|
|
|
0x80, 0x0E, 0x00, 0x38, 0x01, 0xE0, 0x07, 0x00, 0x18, 0x38, 0x00, 0x70, 0x03, 0xFE, 0x07, 0xFC,
|
|
|
|
|
0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1F, 0xF8, 0x3F, 0xF8, 0x71, 0xF8, 0xE0, 0x79, 0xC0, 0x73,
|
|
|
|
|
0x80, 0xE7, 0x01, 0xCE, 0x07, 0x9C, 0x3E, 0x3F, 0xF8, 0x7F, 0xE0, 0x38, 0x07, 0xFC, 0x3F, 0xE0,
|
|
|
|
|
0x70, 0x03, 0x80, 0x1F, 0xF0, 0xFF, 0xC7, 0x1F, 0x38, 0x39, 0xC1, 0xCE, 0x3E, 0x7F, 0xE3, 0xFE,
|
|
|
|
|
0x00, 0xFF, 0x87, 0xFF, 0x38, 0xFD, 0xC0, 0xEE, 0x07, 0xF0, 0x3F, 0x99, 0xFC, 0xEE, 0xE7, 0xF7,
|
|
|
|
|
0xFF, 0x3F, 0xF1, 0xC3, 0x8E, 0x0E, 0x70, 0x63, 0x80, 0x1C, 0x00, 0xE0, 0x00, 0xFF, 0x8F, 0xFC,
|
|
|
|
|
0xE3, 0xEE, 0x0E, 0xE0, 0xFE, 0x07, 0xE0, 0x7E, 0x67, 0xE6, 0xFE, 0x7E, 0xF3, 0xEF, 0xFC, 0xFF,
|
|
|
|
|
0xCE, 0x1C, 0xE0, 0xCE, 0x00, 0xE0, 0x0E, 0x00, 0x00, 0xE0, 0x1C, 0x03, 0xFF, 0xFF, 0xFF, 0xC0,
|
|
|
|
|
0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C,
|
|
|
|
|
0x03, 0x80, 0x70, 0x00, 0x03, 0x81, 0xC0, 0xFF, 0xFF, 0xFF, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38,
|
|
|
|
|
0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0x3F, 0xF9, 0xFF, 0xCE, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00,
|
|
|
|
|
0xE0, 0x1F, 0xF0, 0xFF, 0x81, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00,
|
|
|
|
|
0x38, 0x00, 0x3F, 0xE7, 0xFC, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x3F, 0xE7, 0xFC, 0x38, 0x07, 0x00,
|
|
|
|
|
0xE0, 0x1C, 0x03, 0x80, 0xFF, 0xE7, 0xFF, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0xFE, 0x1F,
|
|
|
|
|
0xFC, 0xF1, 0xF7, 0x07, 0xB8, 0x1F, 0xC0, 0xFE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x78,
|
|
|
|
|
0x03, 0x80, 0x3C, 0x07, 0xC0, 0xFE, 0x07, 0xC0, 0xFF, 0x9F, 0xF3, 0x80, 0x70, 0x0E, 0x01, 0xFF,
|
|
|
|
|
0x3F, 0xF7, 0x1E, 0xE1, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0E, 0x01, 0xC0, 0x78, 0x1E, 0x0F, 0x81,
|
|
|
|
|
0xE0, 0x78, 0x38, 0x3C, 0x78, 0x70, 0xF0, 0x70, 0xE3, 0xC0, 0x71, 0xCF, 0x00, 0x73, 0xBC, 0x00,
|
|
|
|
|
0xF7, 0xF0, 0x00, 0xFF, 0xC0, 0x00, 0xFF, 0x80, 0x03, 0xFF, 0x00, 0x0F, 0x77, 0x00, 0x1C, 0xE7,
|
|
|
|
|
0x00, 0x71, 0xCF, 0x01, 0xC3, 0x8F, 0x07, 0x87, 0x0E, 0x0E, 0x0E, 0x1E, 0x3C, 0x1C, 0x1F, 0xF0,
|
|
|
|
|
0x38, 0x1E, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x38, 0x00, 0x00, 0x70, 0x00, 0x00, 0xE0, 0x00, 0x01,
|
|
|
|
|
0xC0, 0x78, 0xE1, 0xC3, 0x8E, 0x38, 0x1C, 0xE7, 0x00, 0xEE, 0xE0, 0x07, 0xFC, 0x00, 0x3F, 0xC0,
|
|
|
|
|
0x07, 0xFC, 0x00, 0xEF, 0xE0, 0x1C, 0xEF, 0x03, 0xCE, 0x78, 0x38, 0xE3, 0xC7, 0x0E, 0x1F, 0xF0,
|
|
|
|
|
0xE1, 0xF0, 0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x07, 0x00, 0x00, 0x70, 0x3F, 0xC1, 0xFF, 0xC3,
|
|
|
|
|
0x9F, 0x00, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x1F, 0x07, 0xF8, 0x1F, 0xF0, 0x07, 0xE0, 0x03, 0x80,
|
|
|
|
|
0x0E, 0x00, 0x38, 0x00, 0xE7, 0x9F, 0x9F, 0xFC, 0x7F, 0xE0, 0x1C, 0x00, 0x38, 0x00, 0xE0, 0x0F,
|
|
|
|
|
0x80, 0x3C, 0x00, 0xFF, 0x3F, 0xEE, 0x7C, 0x07, 0x03, 0xCF, 0xE3, 0xF8, 0x0F, 0x01, 0xC0, 0x7E,
|
|
|
|
|
0x3F, 0xFF, 0xFF, 0x07, 0x00, 0xE0, 0x38, 0x3E, 0x0F, 0x00, 0xE0, 0x79, 0xC1, 0xE3, 0x87, 0x87,
|
|
|
|
|
0x1E, 0x0E, 0x78, 0x1D, 0xE0, 0x3F, 0x80, 0x7E, 0x00, 0xFE, 0x01, 0xDE, 0x03, 0x9E, 0x07, 0x1E,
|
|
|
|
|
0x0E, 0x1E, 0x1C, 0x3C, 0x38, 0x3C, 0x70, 0x3F, 0xE0, 0x3E, 0x00, 0x1C, 0x00, 0x38, 0x00, 0x70,
|
|
|
|
|
0x00, 0xE0, 0x01, 0xC0, 0xE1, 0xEE, 0x3C, 0xE7, 0x8E, 0xE0, 0xFC, 0x0F, 0x80, 0xFC, 0x0F, 0xE0,
|
|
|
|
|
0xEF, 0x0E, 0x78, 0xE3, 0xCE, 0x1F, 0xE0, 0xF0, 0x07, 0x00, 0x70, 0x07, 0x00, 0x70, 0xE0, 0x1E,
|
|
|
|
|
0xE0, 0x3C, 0xE0, 0x78, 0xEC, 0xF0, 0xED, 0xE0, 0xED, 0xC0, 0xEF, 0x80, 0xFF, 0x80, 0xFF, 0x80,
|
|
|
|
|
0xED, 0xC0, 0xED, 0xE0, 0xEC, 0xF0, 0xEC, 0x70, 0xE0, 0x78, 0xE0, 0x3C, 0xE0, 0x1E, 0xE0, 0x0E,
|
|
|
|
|
0xE0, 0x73, 0x83, 0xCF, 0xDE, 0x3F, 0xF0, 0xFF, 0x83, 0xFC, 0x0F, 0xF8, 0x3F, 0xE0, 0xFD, 0xC3,
|
|
|
|
|
0xF3, 0x8E, 0x0F, 0x38, 0x1E, 0xE0, 0x38, 0x38, 0x1E, 0x38, 0x3C, 0xFF, 0x78, 0xFF, 0xF0, 0x39,
|
|
|
|
|
0xE0, 0x3B, 0xC0, 0x3F, 0x80, 0x3F, 0x00, 0x3F, 0x80, 0x3F, 0xC0, 0x3B, 0xE0, 0x38, 0xF0, 0x38,
|
|
|
|
|
0x78, 0x38, 0x3C, 0x38, 0x1E, 0x38, 0x1E, 0x38, 0x0F, 0x38, 0x78, 0xE3, 0xCF, 0xFE, 0x3F, 0xF0,
|
|
|
|
|
0x3F, 0x80, 0xFC, 0x03, 0xF0, 0x0F, 0xE0, 0x3B, 0xC0, 0xE7, 0x83, 0x8F, 0x0E, 0x1E, 0x38, 0x3C,
|
|
|
|
|
0xFF, 0x03, 0xDF, 0xE0, 0xF0, 0x1C, 0x3C, 0x03, 0x8F, 0x00, 0x73, 0xC0, 0x0E, 0xF0, 0x01, 0xF8,
|
|
|
|
|
0x00, 0x3F, 0x00, 0x07, 0xF0, 0x00, 0xFF, 0x00, 0x1D, 0xF0, 0x03, 0x9F, 0x00, 0x71, 0xE0, 0x0E,
|
|
|
|
|
0x1E, 0x01, 0xC1, 0xE0, 0x38, 0x1E, 0x07, 0x01, 0xC0, 0xFC, 0x1D, 0xF8, 0x70, 0x71, 0xE0, 0xE7,
|
|
|
|
|
0x81, 0xDE, 0x03, 0xF8, 0x07, 0xE0, 0x0F, 0xF0, 0x1C, 0xF0, 0x38, 0xF0, 0x70, 0xE0, 0xE0, 0xE1,
|
|
|
|
|
0xC1, 0xE0, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C,
|
|
|
|
|
0xFF, 0xFC, 0xFF, 0xFC, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C,
|
|
|
|
|
0xE0, 0x1F, 0xE0, 0x1F, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0xE0, 0xE7,
|
|
|
|
|
0x07, 0x38, 0x39, 0xC1, 0xCE, 0x0E, 0x7F, 0xF3, 0xFF, 0x9C, 0x1C, 0xE0, 0xE7, 0x07, 0x38, 0x39,
|
|
|
|
|
0xC1, 0xFE, 0x0F, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0xE0, 0x1F, 0xF8, 0x07, 0xFE, 0x01,
|
|
|
|
|
0xC3, 0x80, 0x70, 0xE0, 0x1C, 0x38, 0x07, 0x0E, 0x01, 0xC3, 0xFF, 0xF0, 0xFF, 0xFC, 0x38, 0x07,
|
|
|
|
|
0x0E, 0x01, 0xC3, 0x80, 0x70, 0xE0, 0x1C, 0x38, 0x07, 0x0E, 0x01, 0xC3, 0x80, 0x70, 0xE0, 0x1C,
|
|
|
|
|
0x00, 0xE0, 0xFF, 0x83, 0xFE, 0x0E, 0x38, 0x38, 0xE0, 0xE3, 0xFF, 0x8F, 0xFE, 0x38, 0x38, 0xE0,
|
|
|
|
|
0xE3, 0x83, 0x8E, 0x0E, 0x38, 0x38, 0xE0, 0xE0, 0xFF, 0xF8, 0x01, 0xFF, 0xF0, 0x03, 0x80, 0xE0,
|
|
|
|
|
0x07, 0x01, 0xC0, 0x0E, 0x03, 0x80, 0x1C, 0x07, 0x00, 0x38, 0x0F, 0xFC, 0x70, 0x1F, 0xFC, 0xE0,
|
|
|
|
|
0x3C, 0x7D, 0xC0, 0x70, 0x3B, 0x80, 0xE0, 0x7F, 0x01, 0xC0, 0x7E, 0x03, 0x80, 0xFC, 0x07, 0x01,
|
|
|
|
|
0xF8, 0x0E, 0x03, 0xF0, 0x1C, 0x07, 0xE0, 0x38, 0x0E, 0x00, 0x00, 0x3C, 0x00, 0x00, 0x70, 0x00,
|
|
|
|
|
0x03, 0xE0, 0x00, 0x3F, 0x80, 0x00, 0x7C, 0x00, 0xFF, 0xE0, 0x1F, 0xFC, 0x03, 0x83, 0x80, 0x70,
|
|
|
|
|
0x70, 0x0E, 0x0E, 0x01, 0xC1, 0xFF, 0x38, 0x3F, 0xF7, 0x07, 0x1E, 0xE0, 0xE1, 0xFC, 0x1C, 0x1F,
|
|
|
|
|
0x83, 0x83, 0xF0, 0x70, 0x7E, 0x0E, 0x0E, 0x00, 0x01, 0xC0, 0x00, 0x78, 0x00, 0x1E, 0x00, 0x0F,
|
|
|
|
|
0xC0, 0x01, 0xE0, 0x07, 0xF8, 0x07, 0xFE, 0x03, 0xF1, 0x80, 0xF0, 0x00, 0x78, 0x00, 0x1C, 0x1F,
|
|
|
|
|
0x8F, 0x0F, 0xF3, 0x87, 0xDE, 0xE1, 0xC3, 0xB8, 0x70, 0xFF, 0x1C, 0x3D, 0xC7, 0x0F, 0x71, 0xC3,
|
|
|
|
|
0x9E, 0x71, 0xE3, 0xFD, 0xF0, 0x7F, 0xF8, 0x07, 0xF8, 0x00, 0x1C, 0x00, 0x07, 0x80, 0x00, 0xFC,
|
|
|
|
|
0x00, 0x1F, 0x80, 0x01, 0xE0, 0x1F, 0x80, 0xFE, 0x07, 0xC0, 0x3C, 0x7C, 0xE3, 0xFB, 0x9E, 0xEE,
|
|
|
|
|
0x73, 0xB9, 0xC6, 0xE7, 0x3B, 0xDC, 0xE7, 0xFF, 0x8F, 0xFC, 0x1F, 0xC0, 0x0F, 0x00, 0x1F, 0x00,
|
|
|
|
|
0x3E, 0x00, 0x78, 0x07, 0xF8, 0x7F, 0xF3, 0xF3, 0xDE, 0x00, 0x78, 0x01, 0xC0, 0x0F, 0x00, 0x38,
|
|
|
|
|
0x00, 0xE0, 0x03, 0x80, 0x0F, 0x00, 0x3C, 0x00, 0x70, 0x01, 0xE0, 0x03, 0xF1, 0xC7, 0xFF, 0x07,
|
|
|
|
|
0xF8, 0x06, 0x00, 0x3C, 0x00, 0x70, 0x01, 0xC0, 0x1F, 0x00, 0x7C, 0x00, 0x1F, 0xC7, 0xF9, 0xF3,
|
|
|
|
|
0x38, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1E, 0x01, 0xF3, 0x1F, 0xE1, 0xFC, 0x0C, 0x01,
|
|
|
|
|
0xC0, 0x38, 0x07, 0x03, 0xE0, 0x7C, 0x00, 0xFF, 0xFF, 0xFF, 0xF0, 0x78, 0x01, 0xE0, 0x07, 0x80,
|
|
|
|
|
0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00, 0x78, 0x01, 0xE0, 0x07, 0x80, 0x1E, 0x00,
|
|
|
|
|
0x78, 0x01, 0xF8, 0x07, 0xE0, 0x03, 0x80, 0x0E, 0x00, 0x38, 0x00, 0xE0, 0x03, 0x80, 0xFF, 0xFF,
|
|
|
|
|
0xFF, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xF8,
|
|
|
|
|
0x0F, 0x80, 0x38, 0x03, 0x80, 0x38, 0x03, 0x80, 0xF0, 0x1E, 0xE0, 0x39, 0xE0, 0xF1, 0xC1, 0xC3,
|
|
|
|
|
0xC7, 0x83, 0xDE, 0x03, 0xB8, 0x07, 0xF0, 0x07, 0xC0, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38,
|
|
|
|
|
0x00, 0x70, 0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0xE0, 0x77, 0x83, 0x9C, 0x1C, 0xE1, 0xE7, 0x8E,
|
|
|
|
|
0x1C, 0x70, 0xE7, 0x07, 0xB8, 0x1D, 0xC0, 0xFC, 0x03, 0xE0, 0x1E, 0x00, 0xF0, 0x03, 0x80, 0x1C,
|
|
|
|
|
0x00, 0xE0, 0x07, 0x00, 0x38, 0x00, 0xF0, 0x1E, 0xE0, 0x39, 0xE0, 0xF1, 0xC1, 0xC3, 0xC7, 0x83,
|
|
|
|
|
0xCE, 0x03, 0xB8, 0x07, 0xF0, 0x07, 0xC0, 0x0F, 0x80, 0xFF, 0xE1, 0xFF, 0xC0, 0x38, 0x00, 0x70,
|
|
|
|
|
0x00, 0xE0, 0x01, 0xC0, 0x03, 0x80, 0xE0, 0x77, 0x83, 0x9C, 0x1C, 0xE1, 0xC7, 0x8E, 0x1C, 0x70,
|
|
|
|
|
0xE7, 0x03, 0xB8, 0x1F, 0x80, 0xFC, 0x03, 0xE0, 0xFF, 0xC7, 0xFE, 0x03, 0x80, 0x1C, 0x00, 0xE0,
|
|
|
|
|
0x07, 0x00, 0x38, 0x00, 0x70, 0x1E, 0x3C, 0x0E, 0x0F, 0x0F, 0x03, 0x8F, 0x01, 0xE7, 0x00, 0x7F,
|
|
|
|
|
0x80, 0x1F, 0x80, 0x07, 0x80, 0x03, 0xC0, 0x03, 0xF0, 0x03, 0xF8, 0x01, 0xDE, 0x01, 0xE7, 0x81,
|
|
|
|
|
0xE1, 0xE0, 0xE0, 0x70, 0xF0, 0x3F, 0xF0, 0x0F, 0x80, 0x03, 0xC0, 0x01, 0xE0, 0x00, 0xF0, 0x00,
|
|
|
|
|
0x78, 0x00, 0x3C, 0x70, 0x73, 0xC7, 0x0E, 0x78, 0x3B, 0x81, 0xF8, 0x07, 0x80, 0x3C, 0x01, 0xF0,
|
|
|
|
|
0x1F, 0xC1, 0xEF, 0x0E, 0x38, 0xE1, 0xFF, 0x07, 0x80, 0x1C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0xFF,
|
|
|
|
|
0xF8, 0xE7, 0xFF, 0xC7, 0x01, 0xC0, 0x38, 0x0E, 0x01, 0xC0, 0x70, 0x0E, 0x03, 0x80, 0x70, 0x1C,
|
|
|
|
|
0x03, 0x80, 0xE0, 0x1C, 0x07, 0x00, 0xE0, 0x38, 0x07, 0x01, 0xC0, 0x38, 0x0E, 0x01, 0xC0, 0x70,
|
|
|
|
|
0x0E, 0x03, 0x80, 0x70, 0x1C, 0x03, 0x80, 0xFF, 0xFF, 0x07, 0xFF, 0xF8, 0x00, 0x01, 0xC0, 0x00,
|
|
|
|
|
0x0E, 0x00, 0x00, 0x70, 0x00, 0x03, 0x80, 0x00, 0x1C, 0xFF, 0xCE, 0xFF, 0xCE, 0x1E, 0x0E, 0x1E,
|
|
|
|
|
0x0E, 0x1E, 0x0E, 0x1E, 0x0E, 0x1E, 0x0E, 0x1E, 0x0E, 0x1E, 0x0E, 0x1E, 0x0E, 0x1E, 0x0E, 0x1F,
|
|
|
|
|
0xFF, 0x1F, 0xFF, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0x00, 0x07, 0xE0, 0x39, 0xC0, 0x73, 0x80,
|
|
|
|
|
0xE7, 0x01, 0xCE, 0x03, 0x9C, 0x07, 0x38, 0x0E, 0x78, 0x1C, 0x78, 0x78, 0xFF, 0xF0, 0x7F, 0xE0,
|
|
|
|
|
0x01, 0xC0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1F, 0x00, 0x3E, 0x00, 0x1C, 0x00, 0x38, 0x00,
|
|
|
|
|
0x70, 0x00, 0xE0, 0x01, 0xC0, 0xE0, 0xE7, 0x07, 0x38, 0x39, 0xC1, 0xCE, 0x0E, 0x78, 0xF1, 0xFF,
|
|
|
|
|
0x87, 0xFC, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xE0, 0x0F, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00,
|
|
|
|
|
0x70, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x9C, 0xFE, 0xE7, 0x7F, 0x7B,
|
|
|
|
|
0xFF, 0xCF, 0xFE, 0x0E, 0x70, 0x73, 0x83, 0x9C, 0x00, 0xE0, 0x07, 0x00, 0x38, 0x70, 0x77, 0x07,
|
|
|
|
|
0x70, 0x77, 0x07, 0x76, 0x77, 0xEF, 0x7F, 0xF3, 0xFF, 0x06, 0x70, 0x67, 0x00, 0x70, 0x07, 0x00,
|
|
|
|
|
0x70, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0xFF, 0x1F, 0xFE, 0xF1, 0xF7,
|
|
|
|
|
0x03, 0xF8, 0x0F, 0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x38, 0xE0, 0x1C, 0x03,
|
|
|
|
|
0x80, 0x70, 0x0E, 0x01, 0xC0, 0x3F, 0xE7, 0xFE, 0xF3, 0xDC, 0x3F, 0x83, 0xF0, 0x7E, 0x0F, 0xC1,
|
|
|
|
|
0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0x80, 0x00, 0x7F, 0x00, 0x1F, 0xFC, 0x03, 0xE7, 0xC0,
|
|
|
|
|
0x38, 0x1E, 0xE7, 0x00, 0xEE, 0x70, 0x0F, 0xFF, 0x00, 0x77, 0xFF, 0xFF, 0x3F, 0xFF, 0xF0, 0xF0,
|
|
|
|
|
0x00, 0x07, 0x00, 0x00, 0x70, 0x00, 0x07, 0x80, 0x00, 0x38, 0x00, 0x03, 0xF1, 0xC0, 0x1F, 0xFE,
|
|
|
|
|
0x00, 0x7F, 0xC0, 0x01, 0xF8, 0x07, 0xFC, 0xE7, 0x9E, 0xEF, 0x0E, 0xFE, 0x07, 0x7F, 0xFF, 0x3F,
|
|
|
|
|
0xFF, 0x0E, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x07, 0xCE, 0x07, 0xFE, 0x01, 0xFE, 0x00, 0x7F, 0x00,
|
|
|
|
|
0x1F, 0xFC, 0x03, 0xE7, 0xC0, 0x38, 0x1E, 0xE7, 0x00, 0xEE, 0x70, 0x0F, 0xFF, 0x00, 0x77, 0xFF,
|
|
|
|
|
0xFF, 0x3F, 0xFF, 0xF0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x70, 0x00, 0x07, 0x80, 0x00, 0x38, 0x00,
|
|
|
|
|
0x03, 0xF1, 0xC0, 0x1F, 0xFE, 0x00, 0x7F, 0xC0, 0x01, 0xC0, 0x00, 0x1C, 0x00, 0x01, 0xC0, 0x00,
|
|
|
|
|
0x1C, 0x00, 0x01, 0xC0, 0x01, 0xF8, 0x07, 0xFC, 0xE7, 0x9E, 0xEF, 0x0E, 0xFE, 0x07, 0x7F, 0xFF,
|
|
|
|
|
0x3F, 0xFF, 0x0E, 0x00, 0x0E, 0x00, 0x0F, 0x00, 0x07, 0xCE, 0x07, 0xFE, 0x01, 0xFE, 0x00, 0xE0,
|
|
|
|
|
0x00, 0xE0, 0x00, 0xE0, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xE0, 0x01, 0xC6, 0x00,
|
|
|
|
|
0x01, 0xDC, 0x00, 0x03, 0xF8, 0x00, 0x03, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x0E,
|
|
|
|
|
0x0F, 0x1E, 0x1C, 0x3C, 0x1C, 0x38, 0xF0, 0x1C, 0x73, 0xC0, 0x3C, 0xEF, 0x00, 0x3D, 0xFC, 0x00,
|
|
|
|
|
0x3F, 0xF0, 0x00, 0x3F, 0xE0, 0x00, 0xFF, 0xC0, 0x03, 0xDD, 0xC0, 0x07, 0x39, 0xC0, 0x1C, 0x73,
|
|
|
|
|
0xC0, 0x70, 0xE3, 0xC1, 0xE1, 0xC3, 0x83, 0x83, 0x87, 0x8F, 0x07, 0x07, 0xBC, 0x0E, 0x07, 0x00,
|
|
|
|
|
0x03, 0x1C, 0x00, 0x77, 0x00, 0x0F, 0xE0, 0x00, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1E, 0x38,
|
|
|
|
|
0x71, 0xC7, 0x1C, 0x1C, 0xE7, 0x01, 0xDD, 0xC0, 0x1F, 0xF0, 0x01, 0xFE, 0x00, 0x7F, 0xC0, 0x1D,
|
|
|
|
|
0xDC, 0x07, 0x39, 0xC1, 0xE7, 0x3C, 0x38, 0xE3, 0xCE, 0x1C, 0x3B, 0xC3, 0x87, 0x80, 0xE0, 0x7F,
|
|
|
|
|
0x07, 0xB8, 0x79, 0xC7, 0x8E, 0x78, 0x77, 0x83, 0xF8, 0x1F, 0xF0, 0xFF, 0xE7, 0x1F, 0xB8, 0x3D,
|
|
|
|
|
0xC0, 0xFE, 0x03, 0xF0, 0x1F, 0x80, 0xFC, 0x07, 0xE0, 0x38, 0x03, 0xC0, 0x1C, 0x03, 0xE0, 0xFE,
|
|
|
|
|
0x07, 0xE0, 0xE1, 0xFC, 0x7B, 0x9E, 0x77, 0x8F, 0xC1, 0xFE, 0x3F, 0xE7, 0x3E, 0xE1, 0xFC, 0x1F,
|
|
|
|
|
0x83, 0xF0, 0x7E, 0x0E, 0x01, 0xC0, 0x78, 0x1E, 0x0F, 0x81, 0xE0, 0x07, 0xFF, 0x01, 0xFF, 0xC0,
|
|
|
|
|
0x70, 0x70, 0x1C, 0x1C, 0x07, 0x07, 0x01, 0xC1, 0xC0, 0x70, 0x70, 0x1C, 0x1C, 0x07, 0x07, 0x01,
|
|
|
|
|
0xC1, 0xC0, 0xF0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x07, 0x81, 0xC7, 0xC0, 0x73, 0xE0, 0x1F, 0xF0,
|
|
|
|
|
0x07, 0xC0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x07, 0x00, 0x01, 0xC0, 0x0F, 0xF8, 0x0F,
|
|
|
|
|
0xF8, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x0E, 0x38, 0x1C, 0x38, 0x1C, 0x38, 0x3C,
|
|
|
|
|
0x38, 0x78, 0x38, 0xF8, 0x3E, 0xE0, 0x3E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x1E, 0x00, 0x1C, 0x00,
|
|
|
|
|
0x18, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xFF, 0xFF, 0xFF,
|
|
|
|
|
0xFF, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1F, 0x80, 0x7E, 0x01, 0xF8, 0x07, 0xE0, 0x1C, 0x00,
|
|
|
|
|
0x70, 0x01, 0xC0, 0x0F, 0x00, 0xF8, 0x03, 0xE0, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0F, 0xFF,
|
|
|
|
|
0xFF, 0xFF, 0x07, 0xE0, 0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0E, 0x01, 0xC0, 0x38, 0x0F, 0x07, 0xC0,
|
|
|
|
|
0xF8, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xFF,
|
|
|
|
|
0xFC, 0xFF, 0xFC, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0, 0x1C, 0xE0,
|
|
|
|
|
0x1F, 0xE0, 0x1F, 0x00, 0x07, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x1C, 0xE0, 0xE7, 0x07,
|
|
|
|
|
0x38, 0x39, 0xC1, 0xCE, 0x0E, 0x7F, 0xF3, 0xFF, 0x9C, 0x1C, 0xE0, 0xE7, 0x07, 0x38, 0x39, 0xC1,
|
|
|
|
|
0xFE, 0x0F, 0x80, 0x1C, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x03, 0x00, 0xE0, 0x3F, 0x01, 0xF8, 0x0F,
|
|
|
|
|
0xC0, 0x7E, 0x03, 0xF0, 0x1F, 0xC0, 0xEE, 0x07, 0x7C, 0x7B, 0xFF, 0xC7, 0xFE, 0x00, 0x70, 0x03,
|
|
|
|
|
0x80, 0x1C, 0x00, 0xE0, 0x1F, 0x00, 0xF8, 0x07, 0x00, 0x38, 0x01, 0xC0, 0x0E, 0x00, 0x70, 0xE0,
|
|
|
|
|
0xFC, 0x1F, 0x83, 0xF0, 0x7E, 0x0F, 0xE3, 0xDF, 0xF9, 0xFF, 0x00, 0xE0, 0x1C, 0x03, 0x81, 0xF0,
|
|
|
|
|
0x3E, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x38, 0x01, 0xC1, 0xE0, 0x07, 0x07, 0xC0, 0x3C, 0x1F,
|
|
|
|
|
0x00, 0xF8, 0x7E, 0x06, 0xE1, 0xD8, 0x1B, 0x87, 0x70, 0xCE, 0x1C, 0xC3, 0x38, 0x73, 0x1C, 0xE1,
|
|
|
|
|
0xCE, 0x63, 0x87, 0x19, 0x8E, 0x1C, 0x7E, 0x38, 0x70, 0xF0, 0xE3, 0xC3, 0xC3, 0x8F, 0x0E, 0x0E,
|
|
|
|
|
0x38, 0x00, 0x3F, 0xE0, 0x00, 0xFC, 0x00, 0x00, 0xE0, 0x00, 0x03, 0x80, 0x00, 0x1C, 0x00, 0x00,
|
|
|
|
|
0x70, 0x00, 0x01, 0x80, 0x70, 0x0E, 0x1E, 0x07, 0x87, 0x81, 0xE1, 0xF0, 0xF8, 0x7C, 0x36, 0x1D,
|
|
|
|
|
0x9D, 0xC7, 0x66, 0x71, 0x9F, 0x9C, 0x63, 0xC7, 0x38, 0xF1, 0xCE, 0x1C, 0x73, 0x80, 0x1F, 0xE0,
|
|
|
|
|
0x07, 0xC0, 0x00, 0x70, 0x00, 0x38, 0x00, 0x0E, 0x00, 0x07, 0x80, 0x00, 0xC0, 0xFF, 0xFF, 0xFF,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0xE0, 0x0E, 0x30, 0x03, 0xB8, 0x01, 0xFC, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0x07, 0x00, 0x07, 0xC0, 0x03, 0xE0, 0x03, 0xF0, 0x01, 0xDC, 0x01, 0xCE, 0x00, 0xE7, 0x80,
|
|
|
|
|
0xF1, 0xC0, 0x70, 0xF0, 0x38, 0x38, 0x3C, 0x1C, 0x1F, 0xFF, 0x0F, 0xFF, 0x8E, 0x01, 0xC7, 0x00,
|
|
|
|
|
0xF7, 0x80, 0x3B, 0x80, 0x1E, 0x31, 0xC7, 0x70, 0xFE, 0x0F, 0x80, 0x00, 0x00, 0x1F, 0xE3, 0xFE,
|
|
|
|
|
0x63, 0xC0, 0x38, 0x03, 0x9F, 0xF7, 0xFF, 0xF3, 0xF8, 0x3F, 0x07, 0xF1, 0xFF, 0xFC, 0xFF, 0x00,
|
|
|
|
|
0x0E, 0x70, 0x07, 0xB8, 0x03, 0x9C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x00, 0x0F, 0x80, 0x07,
|
|
|
|
|
0xC0, 0x07, 0xE0, 0x03, 0xB8, 0x03, 0x9C, 0x01, 0xCF, 0x01, 0xE3, 0x80, 0xE1, 0xE0, 0x70, 0x70,
|
|
|
|
|
0x78, 0x38, 0x3F, 0xFE, 0x1F, 0xFF, 0x1C, 0x03, 0x8E, 0x01, 0xEF, 0x00, 0x77, 0x00, 0x3C, 0x3B,
|
|
|
|
|
0x8F, 0x78, 0xEE, 0x00, 0x00, 0x00, 0xFF, 0x1F, 0xF3, 0x1E, 0x01, 0xC0, 0x1C, 0xFF, 0xBF, 0xFF,
|
|
|
|
|
0x9F, 0xC1, 0xF8, 0x3F, 0x8F, 0xFF, 0xE7, 0xF8, 0x00, 0x3F, 0xFC, 0x00, 0xFF, 0xF8, 0x03, 0xF8,
|
|
|
|
|
0x00, 0x07, 0x70, 0x00, 0x1E, 0xE0, 0x00, 0x39, 0xC0, 0x00, 0xE3, 0x80, 0x03, 0xC7, 0xFC, 0x07,
|
|
|
|
|
0x0F, 0xF8, 0x1C, 0x1C, 0x00, 0x78, 0x38, 0x00, 0xFF, 0xF0, 0x03, 0xFF, 0xE0, 0x07, 0x01, 0xC0,
|
|
|
|
|
0x1E, 0x03, 0x80, 0x38, 0x07, 0xFF, 0xE0, 0x0F, 0xFE, 0x7F, 0x1F, 0x87, 0xFB, 0xFC, 0x63, 0xFB,
|
|
|
|
|
0xE0, 0x1E, 0x0E, 0x00, 0xE0, 0xE3, 0xFF, 0xFE, 0x7F, 0xFF, 0xEF, 0x9E, 0x00, 0xE0, 0xE0, 0x0E,
|
|
|
|
|
0x0F, 0x00, 0xF1, 0xF8, 0xCF, 0xFF, 0xFC, 0x3F, 0x9F, 0xC0, 0x71, 0x87, 0xB8, 0x3F, 0x01, 0xF0,
|
|
|
|
|
0x00, 0x00, 0x00, 0xFF, 0xEF, 0xFE, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFC, 0xFF,
|
|
|
|
|
0xCE, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0xE0, 0x0F, 0xFF, 0xFF, 0xF0, 0x38, 0xC3, 0xDC,
|
|
|
|
|
0x1F, 0x80, 0xF8, 0x00, 0x00, 0x00, 0x1F, 0x83, 0xFE, 0x79, 0xEF, 0x0F, 0xE0, 0x7F, 0xFF, 0xFF,
|
|
|
|
|
0xFE, 0x00, 0xE0, 0x0F, 0x00, 0x7C, 0x63, 0xFE, 0x1F, 0xE0, 0x3F, 0xE0, 0x7F, 0xF8, 0x38, 0xFC,
|
|
|
|
|
0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0xFF, 0xFF, 0xFF, 0xFE, 0xE0, 0x0E,
|
|
|
|
|
0xF0, 0x0E, 0x70, 0x1E, 0x78, 0x1C, 0x7C, 0x7C, 0x3F, 0xF8, 0x0F, 0xE0, 0x7F, 0x87, 0xFC, 0x63,
|
|
|
|
|
0xE0, 0x0F, 0x00, 0x70, 0x07, 0xFF, 0xFF, 0xFF, 0xE0, 0x7F, 0x0F, 0x79, 0xE7, 0xFC, 0x3F, 0x80,
|
|
|
|
|
0x0E, 0xE0, 0x1E, 0xF0, 0x0E, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xE0, 0x7F, 0xF8, 0x38, 0xFC,
|
|
|
|
|
0x00, 0x3C, 0x00, 0x1E, 0x00, 0x0E, 0x00, 0x0E, 0x00, 0x0E, 0xFF, 0xFF, 0xFF, 0xFE, 0xE0, 0x0E,
|
|
|
|
|
0xF0, 0x0E, 0x70, 0x1E, 0x78, 0x1C, 0x7C, 0x7C, 0x3F, 0xF8, 0x0F, 0xE0, 0x39, 0xC3, 0xBC, 0x39,
|
|
|
|
|
0xC0, 0x00, 0x00, 0x07, 0xF8, 0x7F, 0xC6, 0x3E, 0x00, 0xF0, 0x07, 0x00, 0x7F, 0xFF, 0xFF, 0xFE,
|
|
|
|
|
0x07, 0xF0, 0xF7, 0x9E, 0x7F, 0xC3, 0xF8, 0x01, 0xCE, 0x00, 0x03, 0xDC, 0x00, 0x07, 0x38, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x0F, 0x07, 0x07, 0x8F, 0x0E, 0x1E, 0x0E, 0x1C, 0x78, 0x0E, 0x39,
|
|
|
|
|
0xE0, 0x1E, 0x77, 0x80, 0x1E, 0xFE, 0x00, 0x1F, 0xF8, 0x00, 0x1F, 0xF0, 0x00, 0x7F, 0xE0, 0x01,
|
|
|
|
|
0xEE, 0xE0, 0x03, 0x9C, 0xE0, 0x0E, 0x39, 0xE0, 0x38, 0x71, 0xE0, 0xF0, 0xE1, 0xC1, 0xC1, 0xC3,
|
|
|
|
|
0xC7, 0x83, 0x83, 0xDE, 0x07, 0x03, 0x80, 0x03, 0xB8, 0x00, 0xF7, 0x80, 0x0E, 0xE0, 0x00, 0x00,
|
|
|
|
|
0x00, 0x00, 0x00, 0xF1, 0xC3, 0x8E, 0x38, 0xE0, 0xE7, 0x38, 0x0E, 0xEE, 0x00, 0xFF, 0x80, 0x0F,
|
|
|
|
|
0xF0, 0x03, 0xFE, 0x00, 0xEE, 0xE0, 0x39, 0xCE, 0x0F, 0x39, 0xE1, 0xC7, 0x1E, 0x70, 0xE1, 0xDE,
|
|
|
|
|
0x1C, 0x3C, 0x19, 0xC0, 0xF7, 0x81, 0x9C, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x07, 0xFF, 0x0E, 0x7C,
|
|
|
|
|
0x00, 0x78, 0x01, 0xE0, 0x07, 0x00, 0x7C, 0x1F, 0xE0, 0x7F, 0xC0, 0x1F, 0x80, 0x0E, 0x00, 0x38,
|
|
|
|
|
0x00, 0xE0, 0x03, 0x9E, 0x7E, 0x7F, 0xF0, 0xFF, 0x80, 0x73, 0x9D, 0xE7, 0x38, 0x00, 0x00, 0x3F,
|
|
|
|
|
0xCF, 0xFB, 0x9F, 0x01, 0xC0, 0xF3, 0xF8, 0xFE, 0x03, 0xC0, 0x70, 0x1F, 0x8F, 0xFF, 0xFF, 0xC0,
|
|
|
|
|
0x7F, 0xFB, 0xFF, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xC0, 0x1C, 0x01, 0xFC, 0x0F, 0xF0, 0x0F,
|
|
|
|
|
0xC0, 0x1E, 0x00, 0x70, 0x03, 0x80, 0x1D, 0xE7, 0xFF, 0xFE, 0x7F, 0xC0, 0x7F, 0xF7, 0xFE, 0x00,
|
|
|
|
|
0xE0, 0x1C, 0x03, 0x80, 0x70, 0x0F, 0x00, 0xF8, 0x0F, 0xE0, 0x3F, 0x00, 0xF0, 0x07, 0x00, 0x70,
|
|
|
|
|
0x07, 0x00, 0xF7, 0x3E, 0x7F, 0xE7, 0xF8, 0x1F, 0xE0, 0x3F, 0xC0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x1C, 0x03, 0xB8, 0x0F, 0x70, 0x3E, 0xE0, 0x7D, 0xC1, 0xFB, 0x87, 0xF7, 0x1E, 0xEE, 0x39, 0xDC,
|
|
|
|
|
0xE3, 0xBB, 0xC7, 0x7F, 0x0E, 0xFC, 0x1D, 0xF8, 0x3B, 0xE0, 0x77, 0x80, 0xEF, 0x01, 0xDC, 0x03,
|
|
|
|
|
0x80, 0x3F, 0x87, 0xF0, 0x00, 0x00, 0x00, 0x01, 0xC1, 0xF8, 0x7F, 0x1F, 0xE3, 0xFC, 0xFF, 0xBB,
|
|
|
|
|
0xF7, 0x7F, 0xCF, 0xF1, 0xFE, 0x3F, 0x87, 0xE0, 0xFC, 0x1C, 0x1C, 0xE0, 0x3D, 0xC0, 0x73, 0x80,
|
feat: Vietnamese glyphs support (#1147)
## Summary
* **What is the goal of this PR?**
Add Vietnamese glyphs support for the reader's built-in fonts, enabling
proper rendering of Vietnamese text in EPUB content.
* **What changes are included?**
- Added 3 new Unicode intervals to `fontconvert.py` covering Vietnamese
characters:
- **Latin Extended-B** (Vietnamese subset only): `U+01A0–U+01B0` — Ơ/ơ,
Ư/ư
- **Vietnamese Extended**: `U+1EA0–U+1EF9` — All precomposed Vietnamese
characters with tone marks (Ả, Ấ, Ầ, Ẩ, Ẫ, Ậ, Ắ, …, Ỹ)
- Re-generated all 54 built-in font header files (Bookerly, Noto Sans,
OpenDyslexic, Ubuntu across all sizes and styles) to include the new
Vietnamese glyphs.
## Additional Context
* **Scope**: This PR only covers the **reader** fonts. The outer UI
still uses the Ubuntu font which does not fully support Vietnamese — UI
and i18n will be addressed in a follow-up PR (per discussion in PR
#1124).
* **Memory impact**:
| Metric | Before | After | Delta |
|---|---|---|---|
| Flash Data (`.rodata`) | 2,971,028 B | 3,290,748 B | **+319,720 B
(+10.8%)** |
| Total image size | 4,663,235 B | 4,982,955 B | **+319,720 B (+6.9%)**
|
| Flash usage | 69.1% | 74.0% | **+4.9 pp** |
| RAM usage | 29.0% | 29.0% | **No change** |
* **Risk**: Low — this is a data-only change (font glyph tables in
`.rodata`). No logic changes, no RAM impact. Flash headroom remains
comfortable at 74%.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
AI was used to identify the minimal set of Unicode ranges needed for
Vietnamese support and to assist with the PR description.
---------
Co-authored-by: danoooob <danoooob@example.com>
2026-02-25 00:21:39 +07:00
|
|
|
0x00, 0x00, 0x00, 0x1C, 0x03, 0xB8, 0x0F, 0x70, 0x3E, 0xE0, 0x7D, 0xC1, 0xFB, 0x87, 0xF7, 0x1E,
|
|
|
|
|
0xEE, 0x39, 0xDC, 0xE3, 0xBB, 0xC7, 0x7F, 0x0E, 0xFC, 0x1D, 0xF8, 0x3B, 0xE0, 0x77, 0x80, 0xEF,
|
2026-02-25 03:56:11 -06:00
|
|
|
0x01, 0xDC, 0x03, 0x80, 0x3B, 0x8F, 0x78, 0xEE, 0x00, 0x00, 0x01, 0xC1, 0xF8, 0x7F, 0x1F, 0xE3,
|
|
|
|
|
0xFC, 0xFF, 0xBB, 0xF7, 0x7F, 0xCF, 0xF1, 0xFE, 0x3F, 0x87, 0xE0, 0xFC, 0x1C, 0x07, 0x38, 0x03,
|
|
|
|
|
0xBC, 0x01, 0xCE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3F, 0xC0, 0x7F, 0xF0, 0x7E, 0x7C, 0x78, 0x0F,
|
|
|
|
|
0x38, 0x03, 0xDC, 0x01, 0xFE, 0x00, 0x7E, 0x00, 0x3F, 0x00, 0x1F, 0x80, 0x0F, 0xE0, 0x07, 0x70,
|
|
|
|
|
0x07, 0xB8, 0x03, 0xDE, 0x03, 0xC7, 0xE7, 0xC1, 0xFF, 0xC0, 0x3F, 0xC0, 0x19, 0xC1, 0xEF, 0x06,
|
|
|
|
|
0x70, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0xFF, 0x8F, 0x3E, 0x70, 0x77, 0x03, 0xF8, 0x0F, 0xC0, 0x7E,
|
|
|
|
|
0x03, 0xF0, 0x3D, 0xC1, 0xCF, 0x3E, 0x3F, 0xE0, 0xFE, 0x00, 0x07, 0xF8, 0x0F, 0xFE, 0x0F, 0x8F,
|
|
|
|
|
0x8F, 0x01, 0xE7, 0x00, 0x7B, 0x80, 0x1F, 0x80, 0x0F, 0xFF, 0xFF, 0xFF, 0xFF, 0xF0, 0x01, 0xFC,
|
|
|
|
|
0x00, 0xEE, 0x00, 0xF7, 0x80, 0x7B, 0xC0, 0x78, 0xFC, 0xF8, 0x3F, 0xF8, 0x07, 0xF8, 0x00, 0x1F,
|
|
|
|
|
0xC1, 0xFF, 0x1F, 0x7C, 0xE0, 0xEE, 0x07, 0xFF, 0xFF, 0xFF, 0xFC, 0x07, 0xE0, 0x7B, 0x83, 0x9E,
|
|
|
|
|
0x7C, 0x7F, 0xC1, 0xFC, 0x00, 0x07, 0x38, 0x03, 0xBC, 0x01, 0xCE, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x3F, 0xC0, 0x7F, 0xF0, 0x7C, 0x7C, 0x78, 0x0F, 0x38, 0x03, 0xDC, 0x00, 0xFC, 0x00, 0x7F, 0xFF,
|
|
|
|
|
0xFF, 0xFF, 0xFF, 0x80, 0x0F, 0xE0, 0x07, 0x70, 0x07, 0xBC, 0x03, 0xDE, 0x03, 0xC7, 0xE7, 0xC1,
|
|
|
|
|
0xFF, 0xC0, 0x3F, 0xC0, 0x39, 0xC1, 0xEF, 0x0E, 0x70, 0x00, 0x00, 0x00, 0x0F, 0xE0, 0xFF, 0x8F,
|
|
|
|
|
0xBE, 0x70, 0x77, 0x03, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xF0, 0x3D, 0xC1, 0xCF, 0x3E, 0x3F, 0xE0,
|
|
|
|
|
0xFE, 0x00, 0x39, 0xC0, 0x7B, 0x80, 0xE7, 0x00, 0x00, 0x00, 0x00, 0x0F, 0xF8, 0x3F, 0xF8, 0x38,
|
|
|
|
|
0xF8, 0x00, 0x78, 0x00, 0x78, 0x00, 0x70, 0x00, 0xE1, 0xFF, 0xC3, 0xFF, 0x80, 0x07, 0x00, 0x0E,
|
|
|
|
|
0x00, 0x1C, 0x00, 0x78, 0x01, 0xE3, 0x8F, 0x8F, 0xFE, 0x0F, 0xF8, 0x00, 0x73, 0x87, 0xB8, 0x73,
|
|
|
|
|
0x80, 0x00, 0x00, 0x07, 0xF0, 0x7F, 0xC6, 0x3C, 0x00, 0xE0, 0x0E, 0x1F, 0xE1, 0xFF, 0x00, 0xE0,
|
|
|
|
|
0x0E, 0x01, 0xE6, 0x3E, 0x7F, 0xC7, 0xF0, 0x0F, 0xF0, 0x1F, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00,
|
feat: Vietnamese glyphs support (#1147)
## Summary
* **What is the goal of this PR?**
Add Vietnamese glyphs support for the reader's built-in fonts, enabling
proper rendering of Vietnamese text in EPUB content.
* **What changes are included?**
- Added 3 new Unicode intervals to `fontconvert.py` covering Vietnamese
characters:
- **Latin Extended-B** (Vietnamese subset only): `U+01A0–U+01B0` — Ơ/ơ,
Ư/ư
- **Vietnamese Extended**: `U+1EA0–U+1EF9` — All precomposed Vietnamese
characters with tone marks (Ả, Ấ, Ầ, Ẩ, Ẫ, Ậ, Ắ, …, Ỹ)
- Re-generated all 54 built-in font header files (Bookerly, Noto Sans,
OpenDyslexic, Ubuntu across all sizes and styles) to include the new
Vietnamese glyphs.
## Additional Context
* **Scope**: This PR only covers the **reader** fonts. The outer UI
still uses the Ubuntu font which does not fully support Vietnamese — UI
and i18n will be addressed in a follow-up PR (per discussion in PR
#1124).
* **Memory impact**:
| Metric | Before | After | Delta |
|---|---|---|---|
| Flash Data (`.rodata`) | 2,971,028 B | 3,290,748 B | **+319,720 B
(+10.8%)** |
| Total image size | 4,663,235 B | 4,982,955 B | **+319,720 B (+6.9%)**
|
| Flash usage | 69.1% | 74.0% | **+4.9 pp** |
| RAM usage | 29.0% | 29.0% | **No change** |
* **Risk**: Low — this is a data-only change (font glyph tables in
`.rodata`). No logic changes, no RAM impact. Flash headroom remains
comfortable at 74%.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
AI was used to identify the minimal set of Unicode ranges needed for
Vietnamese support and to assist with the PR description.
---------
Co-authored-by: danoooob <danoooob@example.com>
2026-02-25 00:21:39 +07:00
|
|
|
0x1E, 0x03, 0xDC, 0x07, 0x3C, 0x0E, 0x38, 0x3C, 0x78, 0x70, 0x70, 0xE0, 0xF3, 0x80, 0xE7, 0x01,
|
|
|
|
|
0xFE, 0x01, 0xF8, 0x01, 0xF0, 0x03, 0xC0, 0x03, 0x80, 0x0E, 0x02, 0x7C, 0x07, 0xF0, 0x0F, 0xC0,
|
2026-02-25 03:56:11 -06:00
|
|
|
0x00, 0x1F, 0xC1, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x07, 0xF0, 0x77, 0x07, 0x70, 0xE7, 0x8E,
|
|
|
|
|
0x38, 0xE3, 0x8C, 0x1D, 0xC1, 0xDC, 0x1F, 0x80, 0xF8, 0x0F, 0x80, 0x70, 0x07, 0x00, 0x70, 0x9E,
|
|
|
|
|
0x0F, 0xC0, 0xF8, 0x00, 0x0E, 0x70, 0x3D, 0xE0, 0x39, 0xC0, 0x00, 0x00, 0x00, 0x1E, 0x03, 0xDC,
|
|
|
|
|
0x07, 0x3C, 0x0E, 0x38, 0x3C, 0x78, 0x70, 0x70, 0xE0, 0xF3, 0x80, 0xE7, 0x01, 0xFE, 0x01, 0xF8,
|
|
|
|
|
0x01, 0xF0, 0x03, 0xC0, 0x03, 0x80, 0x0E, 0x02, 0x7C, 0x07, 0xF0, 0x0F, 0xC0, 0x00, 0x1D, 0xC3,
|
|
|
|
|
0xDE, 0x1D, 0xC0, 0x00, 0x00, 0x0E, 0x07, 0xF0, 0x77, 0x07, 0x70, 0xE7, 0x8E, 0x38, 0xE3, 0x8C,
|
|
|
|
|
0x1D, 0xC1, 0xDC, 0x1F, 0x80, 0xF8, 0x0F, 0x80, 0x70, 0x07, 0x00, 0x70, 0x9E, 0x0F, 0xC0, 0xF8,
|
|
|
|
|
0x00, 0x01, 0x98, 0x07, 0x70, 0x1D, 0xC0, 0x73, 0x00, 0x66, 0x00, 0x00, 0x00, 0x00, 0x78, 0x0F,
|
|
|
|
|
0x70, 0x1C, 0xF0, 0x38, 0xE0, 0xF1, 0xE1, 0xC1, 0xC3, 0x83, 0xCE, 0x03, 0x9C, 0x07, 0xF8, 0x07,
|
|
|
|
|
0xE0, 0x07, 0xC0, 0x0F, 0x00, 0x0E, 0x00, 0x38, 0x09, 0xF0, 0x1F, 0xC0, 0x3F, 0x00, 0x07, 0x30,
|
|
|
|
|
0x77, 0x0E, 0xE1, 0xDC, 0x18, 0xC0, 0x00, 0x00, 0x0E, 0x07, 0xF0, 0x77, 0x07, 0x70, 0xE7, 0x8E,
|
feat: Vietnamese glyphs support (#1147)
## Summary
* **What is the goal of this PR?**
Add Vietnamese glyphs support for the reader's built-in fonts, enabling
proper rendering of Vietnamese text in EPUB content.
* **What changes are included?**
- Added 3 new Unicode intervals to `fontconvert.py` covering Vietnamese
characters:
- **Latin Extended-B** (Vietnamese subset only): `U+01A0–U+01B0` — Ơ/ơ,
Ư/ư
- **Vietnamese Extended**: `U+1EA0–U+1EF9` — All precomposed Vietnamese
characters with tone marks (Ả, Ấ, Ầ, Ẩ, Ẫ, Ậ, Ắ, …, Ỹ)
- Re-generated all 54 built-in font header files (Bookerly, Noto Sans,
OpenDyslexic, Ubuntu across all sizes and styles) to include the new
Vietnamese glyphs.
## Additional Context
* **Scope**: This PR only covers the **reader** fonts. The outer UI
still uses the Ubuntu font which does not fully support Vietnamese — UI
and i18n will be addressed in a follow-up PR (per discussion in PR
#1124).
* **Memory impact**:
| Metric | Before | After | Delta |
|---|---|---|---|
| Flash Data (`.rodata`) | 2,971,028 B | 3,290,748 B | **+319,720 B
(+10.8%)** |
| Total image size | 4,663,235 B | 4,982,955 B | **+319,720 B (+6.9%)**
|
| Flash usage | 69.1% | 74.0% | **+4.9 pp** |
| RAM usage | 29.0% | 29.0% | **No change** |
* **Risk**: Low — this is a data-only change (font glyph tables in
`.rodata`). No logic changes, no RAM impact. Flash headroom remains
comfortable at 74%.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
AI was used to identify the minimal set of Unicode ranges needed for
Vietnamese support and to assist with the PR description.
---------
Co-authored-by: danoooob <danoooob@example.com>
2026-02-25 00:21:39 +07:00
|
|
|
0x38, 0xE3, 0x8C, 0x1D, 0xC1, 0xDC, 0x1F, 0x80, 0xF8, 0x0F, 0x80, 0x70, 0x07, 0x00, 0x70, 0x9E,
|
2026-02-25 03:56:11 -06:00
|
|
|
0x0F, 0xC0, 0xF8, 0x00, 0x1C, 0xE0, 0xEF, 0x07, 0x38, 0x00, 0x00, 0x00, 0x70, 0x1F, 0x80, 0xFC,
|
|
|
|
|
0x07, 0xE0, 0x3F, 0x01, 0xF8, 0x0F, 0xC0, 0x7F, 0x03, 0xBC, 0x3D, 0xFF, 0xE3, 0xFF, 0x00, 0x38,
|
|
|
|
|
0x01, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0x80, 0x1C, 0x73, 0x8F, 0x71, 0xCE, 0x00, 0x00, 0x01, 0xC1,
|
|
|
|
|
0xF8, 0x3F, 0x07, 0xE0, 0xFC, 0x1F, 0xC7, 0xBF, 0xF3, 0xFE, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0,
|
|
|
|
|
0x1C, 0xFF, 0xFF, 0xFF, 0x80, 0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80,
|
|
|
|
|
0x70, 0x0E, 0x01, 0xC0, 0x38, 0x07, 0xC0, 0xF8, 0x07, 0x00, 0xE0, 0x1C, 0x03, 0x80, 0x70, 0x00,
|
|
|
|
|
0xFF, 0x7F, 0xB8, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1E, 0x0F, 0x03, 0x81,
|
|
|
|
|
0xC0, 0xE0, 0x70, 0x00, 0x07, 0x38, 0x01, 0xDE, 0x00, 0x73, 0x80, 0x00, 0x00, 0x00, 0x00, 0x38,
|
|
|
|
|
0x00, 0xFE, 0x00, 0x3F, 0x80, 0x0F, 0xE0, 0x03, 0xF8, 0x00, 0xFE, 0x00, 0x3F, 0xFE, 0x0F, 0xFF,
|
|
|
|
|
0xE3, 0xF8, 0xFC, 0xFE, 0x07, 0x3F, 0x81, 0xCF, 0xE0, 0x7B, 0xF8, 0x1C, 0xFE, 0x07, 0x3F, 0x8F,
|
|
|
|
|
0xCF, 0xFF, 0xE3, 0xFF, 0xE0, 0xF0, 0x0E, 0xE0, 0x3D, 0xE0, 0x3B, 0x80, 0x00, 0x00, 0x00, 0x1C,
|
|
|
|
|
0x01, 0xF8, 0x03, 0xF0, 0x07, 0xE0, 0x0F, 0xC0, 0x1F, 0xFE, 0x3F, 0xFE, 0x7E, 0x3C, 0xFC, 0x3D,
|
|
|
|
|
0xF8, 0x7B, 0xF1, 0xE7, 0xFF, 0xCF, 0xFF, 0x1C, 0x06, 0x00, 0x1E, 0x00, 0x1C, 0x00, 0x1C, 0x00,
|
|
|
|
|
0x18, 0x00, 0x00, 0x00, 0x00, 0x78, 0x0F, 0x70, 0x1C, 0xF0, 0x78, 0xE0, 0xE1, 0xE3, 0xC1, 0xEF,
|
|
|
|
|
0x01, 0xDC, 0x03, 0xF8, 0x03, 0xE0, 0x03, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x38, 0x00,
|
|
|
|
|
0x70, 0x00, 0xE0, 0x01, 0xC0, 0x0C, 0x01, 0xE0, 0x0E, 0x00, 0x70, 0x03, 0x00, 0x00, 0x00, 0x00,
|
|
|
|
|
0x00, 0xE0, 0x7F, 0x07, 0x70, 0x77, 0x0E, 0x78, 0xE3, 0x8E, 0x38, 0xC1, 0xDC, 0x1D, 0xC1, 0xF8,
|
|
|
|
|
0x0F, 0x80, 0xF8, 0x07, 0x00, 0x70, 0x07, 0x09, 0xE0, 0xFC, 0x0F, 0x80, 0x7F, 0xFD, 0xFF, 0xF0,
|
|
|
|
|
0x7F, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xF0, 0x7F, 0xFF, 0xFF, 0xDF, 0xFF, 0xFF, 0xF0, 0x33, 0x9D,
|
|
|
|
|
0xEE, 0x70, 0x7B, 0xDC, 0xE7, 0x30, 0x7B, 0xDC, 0xE7, 0x30, 0x33, 0xB9, 0xDD, 0xDE, 0xEE, 0x77,
|
|
|
|
|
0x38, 0x7B, 0xBD, 0xDC, 0xEE, 0x77, 0x73, 0x38, 0x7B, 0xBD, 0xDC, 0xEE, 0x77, 0x73, 0x38, 0x06,
|
|
|
|
|
0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0xFF, 0xDF, 0xF8, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00,
|
|
|
|
|
0x60, 0x0C, 0x01, 0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x06, 0x00, 0xC0, 0x18,
|
|
|
|
|
0x03, 0x00, 0x60, 0xFF, 0xDF, 0xF8, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x07, 0xFE, 0xFF, 0xC1,
|
|
|
|
|
0x80, 0x30, 0x06, 0x00, 0xC0, 0x18, 0x03, 0x00, 0x60, 0x7D, 0xFF, 0xFF, 0xFF, 0xEF, 0x80, 0xF0,
|
|
|
|
|
0x70, 0x77, 0x87, 0x83, 0xFC, 0x1C, 0x1C, 0x3E, 0x07, 0x00, 0x03, 0xF8, 0x70, 0x00, 0x3D, 0xC3,
|
|
|
|
|
0x80, 0x01, 0xC6, 0x38, 0x00, 0x0E, 0x3B, 0xC0, 0x00, 0x71, 0xDC, 0x00, 0x03, 0x8D, 0xC0, 0x00,
|
|
|
|
|
0x1E, 0xEE, 0xF8, 0x7C, 0x7F, 0xEF, 0xE7, 0xF1, 0xF7, 0xF7, 0x3B, 0x80, 0x77, 0x1B, 0x8E, 0x07,
|
|
|
|
|
0xB8, 0xFC, 0x70, 0x39, 0xC7, 0xE3, 0x83, 0x8E, 0x37, 0x1C, 0x1C, 0x7B, 0x9D, 0xC1, 0xC1, 0xFC,
|
|
|
|
|
0xFE, 0x0E, 0x07, 0xC3, 0xE0, 0x0C, 0x38, 0xF1, 0xC7, 0x8E, 0x1E, 0x1C, 0x3C, 0x38, 0x30, 0xC3,
|
|
|
|
|
0x87, 0x1E, 0x38, 0xF3, 0x9E, 0x73, 0x8C, 0x00, 0x00, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x0E,
|
|
|
|
|
0x00, 0xE0, 0x0E, 0x00, 0x70, 0x07, 0x00, 0x38, 0x03, 0x80, 0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xE0,
|
|
|
|
|
0x0E, 0x00, 0x70, 0x00, 0x3E, 0x3F, 0x9D, 0xDC, 0x6E, 0x3F, 0x1F, 0x8C, 0xEE, 0x7F, 0x1F, 0x00,
|
|
|
|
|
0x07, 0x07, 0x87, 0xC7, 0xE7, 0x73, 0x3B, 0xFF, 0xFF, 0x07, 0x03, 0x80, 0x7E, 0xFD, 0x87, 0xEF,
|
|
|
|
|
0xE3, 0xC1, 0xE7, 0xFF, 0xF8, 0x0F, 0x1F, 0x9F, 0x0F, 0xC7, 0xF3, 0xB9, 0x8E, 0xEE, 0x7F, 0x1F,
|
|
|
|
|
0x00, 0x7F, 0x3F, 0x81, 0xC1, 0xC0, 0xE0, 0xE0, 0x70, 0x30, 0x38, 0x1C, 0x00, 0x3E, 0x3F, 0x9D,
|
|
|
|
|
0xCE, 0xE7, 0xE3, 0xF9, 0x8D, 0xEE, 0x7F, 0x1F, 0x00, 0x3E, 0x3F, 0x9D, 0xDC, 0x67, 0xF1, 0xF8,
|
|
|
|
|
0x1C, 0x3E, 0x7E, 0x3C, 0x00, 0x3E, 0x3F, 0x9D, 0xDC, 0x6E, 0x3F, 0x1F, 0x8C, 0xEE, 0x7F, 0x1F,
|
|
|
|
|
0x00, 0x3B, 0xFF, 0xF1, 0x8C, 0x63, 0x18, 0xC0, 0x7E, 0x7F, 0x67, 0x07, 0x0E, 0x1C, 0x38, 0x70,
|
|
|
|
|
0x7F, 0x7F, 0x7E, 0x7E, 0x6F, 0x3E, 0x3E, 0x0F, 0x07, 0x67, 0x7F, 0x7E, 0x07, 0x07, 0x87, 0xC7,
|
|
|
|
|
0xE7, 0x73, 0x3B, 0xFF, 0xFF, 0x07, 0x03, 0x80, 0x7E, 0xFD, 0x87, 0xEF, 0xE3, 0xC1, 0xE7, 0xFF,
|
|
|
|
|
0xF8, 0x0F, 0x1F, 0x9F, 0x0F, 0xC7, 0xF3, 0xB9, 0x8E, 0xEE, 0x7F, 0x1F, 0x00, 0x7F, 0x3F, 0x81,
|
|
|
|
|
0xC1, 0xC0, 0xE0, 0xE0, 0x70, 0x30, 0x38, 0x1C, 0x00, 0x3E, 0x3F, 0x9D, 0xCE, 0xE7, 0xE3, 0xF9,
|
|
|
|
|
0x8D, 0xEE, 0x7F, 0x1F, 0x00, 0x3E, 0x3F, 0x9D, 0xDC, 0x67, 0xF1, 0xF8, 0x1C, 0x3E, 0x7E, 0x3C,
|
|
|
|
|
0x00, 0x07, 0xF8, 0x7F, 0x87, 0xCC, 0x38, 0x03, 0x80, 0x7F, 0xF3, 0xFF, 0x87, 0x00, 0x38, 0x07,
|
|
|
|
|
0xFE, 0x3F, 0xF0, 0x70, 0x03, 0xC0, 0x0E, 0x00, 0x7C, 0x61, 0xFF, 0x07, 0xF8, 0xFF, 0xFF, 0xFF,
|
|
|
|
|
0xC1, 0xC0, 0x0E, 0x00, 0x70, 0x03, 0xF0, 0x1F, 0x83, 0xF0, 0x7F, 0x03, 0xF8, 0x01, 0xF8, 0x0F,
|
|
|
|
|
0xC1, 0xF8, 0x3F, 0x81, 0xFC, 0x00, 0xE0, 0x07, 0x00, 0x1F, 0xC0, 0x7F, 0xC1, 0x8F, 0x00, 0x1C,
|
|
|
|
|
0x00, 0x71, 0xFF, 0xF7, 0xFF, 0xC0, 0xF8, 0x07, 0xC0, 0x7C, 0x07, 0xFF, 0xDF, 0xFF, 0x38, 0x00,
|
|
|
|
|
0xE0, 0x03, 0xC7, 0x0F, 0xFE, 0x0F, 0xF0, 0xFF, 0xFF, 0xFF, 0xC0, 0xF0, 0x03, 0x8F, 0xFF, 0x7F,
|
|
|
|
|
0xF8, 0x07, 0x00, 0xF8, 0xFF, 0x87, 0xF0, 0x1E, 0x00, 0x78, 0x01, 0xC0, 0x07, 0x00, 0x3C, 0x00,
|
|
|
|
|
0xF0, 0x03, 0xC0, 0x7F, 0x83, 0xFE, 0x18, 0xF8, 0x01, 0xE0, 0x07, 0x00, 0x38, 0x01, 0xC3, 0xFF,
|
|
|
|
|
0x3F, 0xFB, 0xE7, 0xFC, 0x1F, 0xC0, 0xFE, 0x07, 0x70, 0x3B, 0x81, 0xDC, 0x1C, 0xFB, 0xE3, 0xFE,
|
|
|
|
|
0x0F, 0xE0, 0x03, 0xC0, 0x01, 0xE0, 0x01, 0xF8, 0x00, 0xFC, 0x00, 0x67, 0x00, 0x73, 0x80, 0x38,
|
|
|
|
|
0xE0, 0x38, 0x70, 0x1C, 0x38, 0x1C, 0x0E, 0x0E, 0x07, 0x07, 0x03, 0xC7, 0x00, 0xE3, 0x80, 0x71,
|
|
|
|
|
0xC0, 0x3D, 0xFF, 0xFE, 0xFF, 0xFF, 0x00, 0xFF, 0xFF, 0xFF, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F,
|
|
|
|
|
0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE,
|
|
|
|
|
0x03, 0xF8, 0x0F, 0xE0, 0x3F, 0x80, 0xFE, 0x03, 0xF8, 0x0F, 0xFF, 0xFF, 0xFF, 0xDC, 0x00, 0x70,
|
|
|
|
|
0x03, 0xC0, 0x0E, 0x00, 0x38, 0x01, 0xE0, 0x07, 0x80, 0x1C, 0x01, 0xE0, 0x1E, 0x00, 0xE0, 0x0F,
|
|
|
|
|
0x00, 0xF0, 0x07, 0x00, 0x78, 0x03, 0x80, 0x3F, 0xFF, 0xFF, 0xF0, 0xFF, 0xFF, 0xFF, 0x00, 0x38,
|
|
|
|
|
0x03, 0x80, 0x1C, 0x01, 0xC0, 0x0E, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x07, 0x00, 0x38, 0x03, 0x80,
|
|
|
|
|
0x1C, 0x01, 0xC0, 0x1C, 0x00, 0xE0, 0x0E, 0x00, 0x70, 0x00, 0xFF, 0xF0, 0x00, 0x0E, 0x00, 0x18,
|
|
|
|
|
0x00, 0x70, 0x00, 0xE0, 0x01, 0x80, 0x07, 0x00, 0x0E, 0x00, 0x1C, 0x00, 0x30, 0x00, 0xE1, 0xE1,
|
|
|
|
|
0xC7, 0xC3, 0x0F, 0xCE, 0x03, 0x9C, 0x03, 0xB8, 0x07, 0x60, 0x07, 0xC0, 0x0F, 0x80, 0x1E, 0x00,
|
|
|
|
|
0x1C, 0x00, 0x3C, 0xF9, 0xFF, 0xF7, 0x7D, 0xF8, 0xE3, 0x77, 0xDD, 0xFF, 0xF3, 0xEF, 0x80, 0x07,
|
|
|
|
|
0x83, 0xE0, 0xF0, 0x78, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x07,
|
|
|
|
|
0x01, 0xC0, 0x70, 0x1C, 0x07, 0x01, 0xC0, 0x70, 0x1C, 0x1F, 0x0F, 0x83, 0xC0, 0x7C, 0x3F, 0xF7,
|
|
|
|
|
0xEF, 0xFC, 0x3E, 0x00, 0x00, 0x00, 0x7C, 0x3F, 0xF7, 0xEF, 0xFC, 0x3E, 0x00, 0xC0, 0x1C, 0x01,
|
|
|
|
|
0xCF, 0xFF, 0xFF, 0xF0, 0x70, 0x07, 0x00, 0xE0, 0xFF, 0xFF, 0xFF, 0x38, 0x03, 0x80, 0x30, 0x00,
|
|
|
|
|
0x00, 0x70, 0x1F, 0x0F, 0xE3, 0xF8, 0xFC, 0x0F, 0x00, 0xFC, 0x03, 0xF8, 0x0F, 0xE0, 0x1F, 0x00,
|
|
|
|
|
0x70, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0xE0, 0x0F, 0x80, 0x7F, 0x00, 0xFC, 0x03, 0xF0, 0x0F,
|
|
|
|
|
0x03, 0xF0, 0xFC, 0x7F, 0x0F, 0x80, 0xE0, 0x00, 0x00, 0x00, 0x0F, 0xFF, 0xFF, 0xF0, 0x3F, 0x1F,
|
|
|
|
|
0xDF, 0xCF, 0xFF, 0x33, 0xCB, 0x80, 0xE0, 0xE0, 0x78, 0x38, 0x1C, 0x0F, 0xF7, 0xFB, 0xFD, 0xFE,
|
|
|
|
|
0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03, 0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x03,
|
|
|
|
|
0x81, 0xC0, 0xE0, 0x70, 0x38, 0x1C, 0x0E, 0x07, 0x00, 0x3F, 0x03, 0xFB, 0xFC, 0x9F, 0xC0, 0x0E,
|
|
|
|
|
0x00, 0x70, 0x03, 0xFD, 0xDF, 0xEE, 0xE0, 0x77, 0x03, 0xB8, 0x1D, 0xC0, 0xEE, 0x07, 0x70, 0x3B,
|
|
|
|
|
0x81, 0xDC, 0x0E, 0xE0, 0x77, 0x03, 0xB8, 0x1C, 0x3F, 0x71, 0xFD, 0xCF, 0x27, 0x38, 0x1C, 0xE0,
|
|
|
|
|
0x73, 0x81, 0xCF, 0xF7, 0x3F, 0xDC, 0xE0, 0x73, 0x81, 0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x73, 0x81,
|
|
|
|
|
0xCE, 0x07, 0x38, 0x1C, 0xE0, 0x7B, 0x80, 0xFE, 0x01, 0xC0, 0x3F, 0x1F, 0x80, 0xFE, 0x7F, 0x3B,
|
|
|
|
|
0xCC, 0xF2, 0x77, 0x01, 0xC0, 0x0E, 0x07, 0x80, 0x1C, 0x0E, 0x00, 0x3F, 0xDF, 0xE7, 0x7F, 0xBF,
|
|
|
|
|
0xCE, 0xE0, 0x70, 0x1D, 0xC0, 0xE0, 0x3B, 0x81, 0xC0, 0x77, 0x03, 0x80, 0xEE, 0x07, 0x01, 0xDC,
|
|
|
|
|
0x0E, 0x03, 0xB8, 0x1C, 0x07, 0x70, 0x38, 0x0E, 0xE0, 0x70, 0x1D, 0xC0, 0xE0, 0x3B, 0x81, 0xC0,
|
|
|
|
|
0x70, 0x3F, 0x1F, 0x9C, 0x7F, 0x3F, 0x9C, 0xF3, 0x3C, 0x9C, 0xE0, 0x38, 0x1C, 0xE0, 0x78, 0x1C,
|
|
|
|
|
0xE0, 0x70, 0x1C, 0xFF, 0x7F, 0x9C, 0xFF, 0x7F, 0x9C, 0xE0, 0x70, 0x1C, 0xE0, 0x70, 0x1C, 0xE0,
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
0x70, 0x1C, 0xE0, 0x70, 0x1C, 0xE0, 0x70, 0x1C, 0xE0, 0x70, 0x1C, 0xE0, 0x70, 0x1C, 0xE0, 0x70,
|
2026-02-25 03:56:11 -06:00
|
|
|
0x1C, 0xE0, 0x70, 0x1E, 0xE0, 0x70, 0x0F, 0xE0, 0x70, 0x0F,
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const EpdGlyph ubuntu_12_regularGlyphs[] = {
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0, 0, 0, 0, 0, 0, 0 }, // U+0000
|
|
|
|
|
{ 0, 0, 0, 0, 0, 0, 0 }, // U+0008
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0, 0, 92, 0, 0, 0, 0 }, // U+0009
|
|
|
|
|
{ 0, 0, 92, 0, 0, 0, 0 }, // U+000D
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0, 0, 0, 0, 0, 0, 0 }, // U+001D
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0, 0, 92, 0, 0, 0, 0 }, // U+0020
|
|
|
|
|
{ 5, 17, 110, 1, 17, 11, 0 }, // !
|
|
|
|
|
{ 8, 6, 167, 1, 20, 6, 11 }, // "
|
|
|
|
|
{ 15, 17, 267, 1, 17, 32, 17 }, // #
|
|
|
|
|
{ 12, 23, 226, 1, 20, 35, 49 }, // $
|
|
|
|
|
{ 20, 17, 343, 1, 17, 43, 84 }, // %
|
|
|
|
|
{ 16, 17, 266, 1, 17, 34, 127 }, // &
|
|
|
|
|
{ 4, 6, 96, 1, 20, 3, 161 }, // '
|
|
|
|
|
{ 7, 25, 130, 1, 20, 22, 164 }, // (
|
|
|
|
|
{ 7, 25, 130, 0, 20, 22, 186 }, // )
|
|
|
|
|
{ 12, 10, 192, 0, 17, 15, 208 }, // *
|
|
|
|
|
{ 12, 12, 226, 1, 14, 18, 223 }, // +
|
|
|
|
|
{ 5, 7, 98, 0, 3, 5, 241 }, // ,
|
|
|
|
|
{ 7, 2, 120, 0, 8, 2, 246 }, // -
|
|
|
|
|
{ 4, 3, 98, 1, 3, 2, 248 }, // .
|
|
|
|
|
{ 12, 25, 154, -1, 20, 38, 250 }, // /
|
|
|
|
|
{ 12, 17, 226, 1, 17, 26, 288 }, // 0
|
|
|
|
|
{ 8, 17, 226, 2, 17, 17, 314 }, // 1
|
|
|
|
|
{ 12, 17, 226, 1, 17, 26, 331 }, // 2
|
|
|
|
|
{ 12, 17, 226, 1, 17, 26, 357 }, // 3
|
|
|
|
|
{ 13, 17, 226, 1, 17, 28, 383 }, // 4
|
|
|
|
|
{ 12, 17, 226, 1, 17, 26, 411 }, // 5
|
|
|
|
|
{ 12, 17, 226, 1, 17, 26, 437 }, // 6
|
|
|
|
|
{ 12, 17, 226, 1, 17, 26, 463 }, // 7
|
|
|
|
|
{ 12, 17, 226, 1, 17, 26, 489 }, // 8
|
|
|
|
|
{ 12, 17, 226, 1, 17, 26, 515 }, // 9
|
|
|
|
|
{ 4, 13, 98, 1, 13, 7, 541 }, // :
|
|
|
|
|
{ 5, 17, 98, 0, 13, 11, 548 }, // ;
|
|
|
|
|
{ 12, 11, 226, 1, 13, 17, 559 }, // <
|
|
|
|
|
{ 12, 7, 226, 1, 11, 11, 576 }, // =
|
|
|
|
|
{ 12, 11, 226, 1, 13, 17, 587 }, // >
|
|
|
|
|
{ 10, 17, 162, 0, 17, 22, 604 }, // ?
|
|
|
|
|
{ 22, 22, 380, 1, 18, 61, 626 }, // @
|
|
|
|
|
{ 17, 17, 265, 0, 17, 37, 687 }, // A
|
|
|
|
|
{ 13, 17, 257, 2, 17, 28, 724 }, // B
|
|
|
|
|
{ 14, 17, 248, 1, 17, 30, 752 }, // C
|
|
|
|
|
{ 15, 17, 285, 2, 17, 32, 782 }, // D
|
|
|
|
|
{ 12, 17, 228, 2, 17, 26, 814 }, // E
|
|
|
|
|
{ 11, 17, 215, 2, 17, 24, 840 }, // F
|
|
|
|
|
{ 15, 17, 269, 1, 17, 32, 864 }, // G
|
|
|
|
|
{ 14, 17, 282, 2, 17, 30, 896 }, // H
|
|
|
|
|
{ 3, 17, 108, 2, 17, 7, 926 }, // I
|
|
|
|
|
{ 11, 17, 200, 0, 17, 24, 933 }, // J
|
|
|
|
|
{ 14, 17, 252, 2, 17, 30, 957 }, // K
|
|
|
|
|
{ 11, 17, 208, 2, 17, 24, 987 }, // L
|
|
|
|
|
{ 20, 17, 348, 1, 17, 43, 1011 }, // M
|
|
|
|
|
{ 15, 17, 291, 2, 17, 32, 1054 }, // N
|
|
|
|
|
{ 17, 17, 311, 1, 17, 37, 1086 }, // O
|
|
|
|
|
{ 13, 17, 243, 2, 17, 28, 1123 }, // P
|
|
|
|
|
{ 17, 22, 311, 1, 17, 47, 1151 }, // Q
|
|
|
|
|
{ 14, 17, 252, 2, 17, 30, 1198 }, // R
|
|
|
|
|
{ 13, 17, 213, 0, 17, 28, 1228 }, // S
|
|
|
|
|
{ 14, 17, 226, 0, 17, 30, 1256 }, // T
|
|
|
|
|
{ 14, 17, 275, 2, 17, 30, 1286 }, // U
|
|
|
|
|
{ 17, 17, 262, 0, 17, 37, 1316 }, // V
|
|
|
|
|
{ 23, 17, 372, 0, 17, 49, 1353 }, // W
|
|
|
|
|
{ 16, 17, 252, 0, 17, 34, 1402 }, // X
|
|
|
|
|
{ 15, 17, 239, 0, 17, 32, 1436 }, // Y
|
|
|
|
|
{ 14, 17, 229, 0, 17, 30, 1468 }, // Z
|
|
|
|
|
{ 7, 25, 132, 2, 20, 22, 1498 }, // [
|
|
|
|
|
{ 12, 25, 154, -1, 20, 38, 1520 }, // <backslash>
|
|
|
|
|
{ 6, 25, 132, 0, 20, 19, 1558 }, // ]
|
|
|
|
|
{ 12, 9, 226, 1, 17, 14, 1577 }, // ^
|
|
|
|
|
{ 14, 2, 197, -1, -3, 4, 1591 }, // _
|
|
|
|
|
{ 6, 5, 150, 1, 20, 4, 1595 }, // `
|
|
|
|
|
{ 11, 13, 209, 1, 13, 18, 1599 }, // a
|
|
|
|
|
{ 12, 19, 236, 2, 19, 29, 1617 }, // b
|
|
|
|
|
{ 11, 13, 186, 1, 13, 18, 1646 }, // c
|
|
|
|
|
{ 12, 19, 236, 1, 19, 29, 1664 }, // d
|
|
|
|
|
{ 12, 13, 224, 1, 13, 20, 1693 }, // e
|
|
|
|
|
{ 8, 19, 154, 2, 19, 19, 1713 }, // f
|
|
|
|
|
{ 12, 18, 231, 1, 13, 27, 1732 }, // g
|
|
|
|
|
{ 11, 19, 228, 2, 19, 27, 1759 }, // h
|
|
|
|
|
{ 4, 18, 101, 1, 18, 9, 1786 }, // i
|
|
|
|
|
{ 7, 23, 101, -2, 18, 21, 1795 }, // j
|
|
|
|
|
{ 11, 19, 209, 2, 19, 27, 1816 }, // k
|
|
|
|
|
{ 6, 19, 109, 1, 19, 15, 1843 }, // l
|
|
|
|
|
{ 18, 13, 344, 2, 13, 30, 1858 }, // m
|
|
|
|
|
{ 11, 13, 230, 2, 13, 18, 1888 }, // n
|
|
|
|
|
{ 13, 13, 236, 1, 13, 22, 1906 }, // o
|
|
|
|
|
{ 12, 18, 236, 2, 13, 27, 1928 }, // p
|
|
|
|
|
{ 12, 18, 236, 1, 13, 27, 1955 }, // q
|
|
|
|
|
{ 8, 13, 154, 2, 13, 13, 1982 }, // r
|
|
|
|
|
{ 10, 13, 178, 1, 13, 17, 1995 }, // s
|
|
|
|
|
{ 9, 17, 161, 1, 17, 20, 2012 }, // t
|
|
|
|
|
{ 12, 13, 230, 1, 13, 20, 2032 }, // u
|
|
|
|
|
{ 13, 13, 201, 0, 13, 22, 2052 }, // v
|
|
|
|
|
{ 19, 13, 311, 0, 13, 31, 2074 }, // w
|
|
|
|
|
{ 13, 13, 204, 0, 13, 22, 2105 }, // x
|
|
|
|
|
{ 12, 18, 199, 0, 13, 27, 2127 }, // y
|
|
|
|
|
{ 11, 13, 188, 0, 13, 18, 2154 }, // z
|
|
|
|
|
{ 8, 25, 133, 1, 20, 25, 2172 }, // {
|
|
|
|
|
{ 3, 25, 112, 2, 20, 10, 2197 }, // |
|
|
|
|
|
{ 8, 25, 133, 0, 20, 25, 2207 }, // }
|
|
|
|
|
{ 12, 4, 226, 1, 10, 6, 2232 }, // ~
|
|
|
|
|
{ 0, 0, 92, 0, 0, 0, 2238 }, // U+00A0
|
|
|
|
|
{ 5, 18, 110, 1, 13, 12, 2238 }, // U+00A1
|
|
|
|
|
{ 10, 19, 226, 2, 16, 24, 2250 }, // U+00A2
|
|
|
|
|
{ 12, 17, 226, 1, 17, 26, 2274 }, // U+00A3
|
|
|
|
|
{ 12, 12, 226, 1, 15, 18, 2300 }, // U+00A4
|
|
|
|
|
{ 14, 17, 226, 0, 17, 30, 2318 }, // U+00A5
|
|
|
|
|
{ 3, 25, 112, 2, 20, 10, 2348 }, // U+00A6
|
|
|
|
|
{ 11, 20, 197, 1, 18, 28, 2358 }, // U+00A7
|
|
|
|
|
{ 9, 3, 150, 0, 18, 4, 2386 }, // U+00A8
|
|
|
|
|
{ 18, 17, 326, 1, 17, 39, 2390 }, // U+00A9
|
|
|
|
|
{ 9, 9, 156, 0, 17, 11, 2429 }, // U+00AA
|
|
|
|
|
{ 12, 11, 200, 0, 13, 17, 2440 }, // U+00AB
|
|
|
|
|
{ 12, 9, 226, 1, 10, 14, 2457 }, // U+00AC
|
|
|
|
|
{ 7, 2, 120, 0, 8, 2, 2471 }, // U+00AD
|
|
|
|
|
{ 18, 17, 326, 1, 17, 39, 2473 }, // U+00AE
|
|
|
|
|
{ 8, 2, 150, 1, 18, 2, 2512 }, // U+00AF
|
|
|
|
|
{ 8, 7, 133, 0, 19, 7, 2514 }, // U+00B0
|
|
|
|
|
{ 12, 15, 226, 1, 15, 23, 2521 }, // U+00B1
|
|
|
|
|
{ 8, 10, 144, 0, 18, 10, 2544 }, // U+00B2
|
|
|
|
|
{ 8, 10, 144, 0, 18, 10, 2554 }, // U+00B3
|
|
|
|
|
{ 5, 5, 150, 3, 20, 4, 2564 }, // U+00B4
|
|
|
|
|
{ 11, 18, 232, 2, 13, 25, 2568 }, // U+00B5
|
|
|
|
|
{ 13, 22, 258, 1, 17, 36, 2593 }, // U+00B6
|
|
|
|
|
{ 4, 3, 98, 1, 9, 2, 2629 }, // U+00B7
|
|
|
|
|
{ 6, 6, 150, 1, 1, 5, 2631 }, // U+00B8
|
|
|
|
|
{ 5, 10, 144, 1, 18, 7, 2636 }, // U+00B9
|
|
|
|
|
{ 10, 9, 178, 1, 17, 12, 2643 }, // U+00BA
|
|
|
|
|
{ 11, 11, 200, 1, 13, 16, 2655 }, // U+00BB
|
|
|
|
|
{ 21, 18, 350, 1, 18, 48, 2671 }, // U+00BC
|
|
|
|
|
{ 20, 18, 350, 1, 18, 45, 2719 }, // U+00BD
|
|
|
|
|
{ 22, 18, 350, 0, 18, 50, 2764 }, // U+00BE
|
|
|
|
|
{ 10, 18, 162, 0, 13, 23, 2814 }, // U+00BF
|
|
|
|
|
{ 17, 24, 265, 0, 24, 51, 2837 }, // U+00C0
|
|
|
|
|
{ 17, 24, 265, 0, 24, 51, 2888 }, // U+00C1
|
|
|
|
|
{ 17, 24, 265, 0, 24, 51, 2939 }, // U+00C2
|
|
|
|
|
{ 17, 23, 265, 0, 23, 49, 2990 }, // U+00C3
|
|
|
|
|
{ 17, 22, 265, 0, 22, 47, 3039 }, // U+00C4
|
|
|
|
|
{ 17, 21, 265, 0, 21, 45, 3086 }, // U+00C5
|
|
|
|
|
{ 23, 17, 374, 0, 17, 49, 3131 }, // U+00C6
|
|
|
|
|
{ 14, 23, 248, 1, 17, 41, 3180 }, // U+00C7
|
|
|
|
|
{ 12, 24, 228, 2, 24, 36, 3221 }, // U+00C8
|
|
|
|
|
{ 12, 24, 228, 2, 24, 36, 3257 }, // U+00C9
|
|
|
|
|
{ 12, 24, 228, 2, 24, 36, 3293 }, // U+00CA
|
|
|
|
|
{ 12, 22, 228, 2, 22, 33, 3329 }, // U+00CB
|
|
|
|
|
{ 6, 24, 108, 0, 24, 18, 3362 }, // U+00CC
|
|
|
|
|
{ 6, 24, 108, 1, 24, 18, 3380 }, // U+00CD
|
|
|
|
|
{ 8, 24, 108, -1, 24, 24, 3398 }, // U+00CE
|
|
|
|
|
{ 9, 22, 108, -1, 22, 25, 3422 }, // U+00CF
|
|
|
|
|
{ 17, 17, 290, 0, 17, 37, 3447 }, // U+00D0
|
|
|
|
|
{ 15, 23, 291, 2, 23, 44, 3484 }, // U+00D1
|
|
|
|
|
{ 17, 24, 311, 1, 24, 51, 3528 }, // U+00D2
|
|
|
|
|
{ 17, 24, 311, 1, 24, 51, 3579 }, // U+00D3
|
|
|
|
|
{ 17, 24, 311, 1, 24, 51, 3630 }, // U+00D4
|
|
|
|
|
{ 17, 23, 311, 1, 23, 49, 3681 }, // U+00D5
|
|
|
|
|
{ 17, 22, 311, 1, 22, 47, 3730 }, // U+00D6
|
|
|
|
|
{ 11, 11, 226, 2, 13, 16, 3777 }, // U+00D7
|
|
|
|
|
{ 17, 19, 311, 1, 18, 41, 3793 }, // U+00D8
|
|
|
|
|
{ 14, 24, 275, 2, 24, 42, 3834 }, // U+00D9
|
|
|
|
|
{ 14, 24, 275, 2, 24, 42, 3876 }, // U+00DA
|
|
|
|
|
{ 14, 24, 275, 2, 24, 42, 3918 }, // U+00DB
|
|
|
|
|
{ 14, 22, 275, 2, 22, 39, 3960 }, // U+00DC
|
|
|
|
|
{ 15, 24, 239, 0, 24, 45, 3999 }, // U+00DD
|
|
|
|
|
{ 13, 17, 244, 2, 17, 28, 4044 }, // U+00DE
|
|
|
|
|
{ 13, 19, 249, 2, 19, 31, 4072 }, // U+00DF
|
|
|
|
|
{ 11, 20, 209, 1, 20, 28, 4103 }, // U+00E0
|
|
|
|
|
{ 11, 20, 209, 1, 20, 28, 4131 }, // U+00E1
|
|
|
|
|
{ 11, 20, 209, 1, 20, 28, 4159 }, // U+00E2
|
|
|
|
|
{ 11, 19, 209, 1, 19, 27, 4187 }, // U+00E3
|
|
|
|
|
{ 11, 18, 209, 1, 18, 25, 4214 }, // U+00E4
|
|
|
|
|
{ 11, 20, 209, 1, 20, 28, 4239 }, // U+00E5
|
|
|
|
|
{ 20, 13, 344, 1, 13, 33, 4267 }, // U+00E6
|
|
|
|
|
{ 11, 19, 186, 1, 13, 27, 4300 }, // U+00E7
|
|
|
|
|
{ 12, 20, 224, 1, 20, 30, 4327 }, // U+00E8
|
|
|
|
|
{ 12, 20, 224, 1, 20, 30, 4357 }, // U+00E9
|
|
|
|
|
{ 12, 20, 224, 1, 20, 30, 4387 }, // U+00EA
|
|
|
|
|
{ 12, 18, 224, 1, 18, 27, 4417 }, // U+00EB
|
|
|
|
|
{ 5, 20, 101, 0, 20, 13, 4444 }, // U+00EC
|
|
|
|
|
{ 6, 20, 101, 1, 20, 15, 4457 }, // U+00ED
|
|
|
|
|
{ 8, 20, 101, -1, 20, 20, 4472 }, // U+00EE
|
|
|
|
|
{ 8, 18, 101, -1, 18, 18, 4492 }, // U+00EF
|
|
|
|
|
{ 14, 19, 236, 1, 19, 34, 4510 }, // U+00F0
|
|
|
|
|
{ 11, 19, 230, 2, 19, 27, 4544 }, // U+00F1
|
|
|
|
|
{ 13, 20, 236, 1, 20, 33, 4571 }, // U+00F2
|
|
|
|
|
{ 13, 20, 236, 1, 20, 33, 4604 }, // U+00F3
|
|
|
|
|
{ 13, 20, 236, 1, 20, 33, 4637 }, // U+00F4
|
|
|
|
|
{ 13, 19, 236, 1, 19, 31, 4670 }, // U+00F5
|
|
|
|
|
{ 13, 18, 236, 1, 18, 30, 4701 }, // U+00F6
|
|
|
|
|
{ 12, 12, 226, 1, 14, 18, 4731 }, // U+00F7
|
|
|
|
|
{ 13, 15, 236, 1, 14, 25, 4749 }, // U+00F8
|
|
|
|
|
{ 12, 20, 230, 1, 20, 30, 4774 }, // U+00F9
|
|
|
|
|
{ 12, 20, 230, 1, 20, 30, 4804 }, // U+00FA
|
|
|
|
|
{ 12, 20, 230, 1, 20, 30, 4834 }, // U+00FB
|
|
|
|
|
{ 12, 18, 230, 1, 18, 27, 4864 }, // U+00FC
|
|
|
|
|
{ 12, 25, 199, 0, 20, 38, 4891 }, // U+00FD
|
|
|
|
|
{ 12, 24, 236, 2, 19, 36, 4929 }, // U+00FE
|
|
|
|
|
{ 12, 23, 199, 0, 18, 35, 4965 }, // U+00FF
|
|
|
|
|
{ 17, 22, 265, 0, 22, 47, 5000 }, // U+0100
|
|
|
|
|
{ 11, 18, 209, 1, 18, 25, 5047 }, // U+0101
|
|
|
|
|
{ 17, 23, 265, 0, 23, 49, 5072 }, // U+0102
|
|
|
|
|
{ 11, 19, 209, 1, 19, 27, 5121 }, // U+0103
|
|
|
|
|
{ 17, 22, 265, 0, 17, 47, 5148 }, // U+0104
|
|
|
|
|
{ 11, 18, 209, 1, 13, 25, 5195 }, // U+0105
|
|
|
|
|
{ 14, 24, 248, 1, 24, 42, 5220 }, // U+0106
|
|
|
|
|
{ 11, 20, 186, 1, 20, 28, 5262 }, // U+0107
|
|
|
|
|
{ 14, 24, 248, 1, 24, 42, 5290 }, // U+0108
|
|
|
|
|
{ 11, 20, 186, 1, 20, 28, 5332 }, // U+0109
|
|
|
|
|
{ 14, 22, 248, 1, 22, 39, 5360 }, // U+010A
|
|
|
|
|
{ 11, 18, 186, 1, 18, 25, 5399 }, // U+010B
|
|
|
|
|
{ 14, 24, 248, 1, 24, 42, 5424 }, // U+010C
|
|
|
|
|
{ 11, 20, 186, 1, 20, 28, 5466 }, // U+010D
|
|
|
|
|
{ 15, 24, 285, 2, 24, 45, 5494 }, // U+010E
|
|
|
|
|
{ 16, 19, 249, 1, 19, 38, 5539 }, // U+010F
|
|
|
|
|
{ 17, 17, 290, 0, 17, 37, 5577 }, // U+0110
|
|
|
|
|
{ 14, 19, 236, 1, 19, 34, 5614 }, // U+0111
|
|
|
|
|
{ 12, 22, 228, 2, 22, 33, 5648 }, // U+0112
|
|
|
|
|
{ 12, 18, 224, 1, 18, 27, 5681 }, // U+0113
|
|
|
|
|
{ 12, 23, 228, 2, 23, 35, 5708 }, // U+0114
|
|
|
|
|
{ 12, 19, 224, 1, 19, 29, 5743 }, // U+0115
|
|
|
|
|
{ 12, 22, 228, 2, 22, 33, 5772 }, // U+0116
|
|
|
|
|
{ 12, 18, 224, 1, 18, 27, 5805 }, // U+0117
|
|
|
|
|
{ 12, 22, 228, 2, 17, 33, 5832 }, // U+0118
|
|
|
|
|
{ 12, 18, 224, 1, 13, 27, 5865 }, // U+0119
|
|
|
|
|
{ 12, 24, 228, 2, 24, 36, 5892 }, // U+011A
|
|
|
|
|
{ 12, 21, 224, 1, 21, 32, 5928 }, // U+011B
|
|
|
|
|
{ 15, 24, 269, 1, 24, 45, 5960 }, // U+011C
|
|
|
|
|
{ 12, 25, 231, 1, 20, 38, 6005 }, // U+011D
|
|
|
|
|
{ 15, 23, 269, 1, 23, 44, 6043 }, // U+011E
|
|
|
|
|
{ 12, 24, 231, 1, 19, 36, 6087 }, // U+011F
|
|
|
|
|
{ 15, 22, 269, 1, 22, 42, 6123 }, // U+0120
|
|
|
|
|
{ 12, 23, 231, 1, 18, 35, 6165 }, // U+0121
|
|
|
|
|
{ 15, 21, 269, 1, 17, 40, 6200 }, // U+0122
|
|
|
|
|
{ 12, 24, 231, 1, 19, 36, 6240 }, // U+0123
|
|
|
|
|
{ 14, 24, 282, 2, 24, 42, 6276 }, // U+0124
|
|
|
|
|
{ 12, 25, 228, 1, 25, 38, 6318 }, // U+0125
|
|
|
|
|
{ 18, 17, 292, 0, 17, 39, 6356 }, // U+0126
|
|
|
|
|
{ 13, 19, 228, 0, 19, 31, 6395 }, // U+0127
|
|
|
|
|
{ 9, 23, 108, -1, 23, 26, 6426 }, // U+0128
|
|
|
|
|
{ 9, 19, 101, -1, 19, 22, 6452 }, // U+0129
|
|
|
|
|
{ 8, 22, 108, -1, 22, 22, 6474 }, // U+012A
|
|
|
|
|
{ 8, 18, 101, -1, 18, 18, 6496 }, // U+012B
|
|
|
|
|
{ 7, 23, 108, 0, 23, 21, 6514 }, // U+012C
|
|
|
|
|
{ 8, 19, 101, -1, 19, 19, 6535 }, // U+012D
|
|
|
|
|
{ 5, 22, 108, 0, 17, 14, 6554 }, // U+012E
|
|
|
|
|
{ 5, 23, 101, 0, 18, 15, 6568 }, // U+012F
|
|
|
|
|
{ 4, 22, 108, 1, 22, 11, 6583 }, // U+0130
|
|
|
|
|
{ 3, 13, 101, 2, 13, 5, 6594 }, // U+0131
|
|
|
|
|
{ 16, 17, 307, 2, 17, 34, 6599 }, // U+0132
|
|
|
|
|
{ 11, 23, 204, 1, 18, 32, 6633 }, // U+0133
|
|
|
|
|
{ 13, 24, 200, 0, 24, 39, 6665 }, // U+0134
|
|
|
|
|
{ 9, 25, 101, -2, 20, 29, 6704 }, // U+0135
|
|
|
|
|
{ 14, 21, 252, 2, 17, 37, 6733 }, // U+0136
|
|
|
|
|
{ 11, 23, 209, 2, 19, 32, 6770 }, // U+0137
|
|
|
|
|
{ 12, 13, 215, 2, 13, 20, 6802 }, // U+0138
|
|
|
|
|
{ 11, 24, 208, 2, 24, 33, 6822 }, // U+0139
|
|
|
|
|
{ 6, 23, 109, 1, 23, 18, 6855 }, // U+013A
|
|
|
|
|
{ 11, 21, 208, 2, 17, 29, 6873 }, // U+013B
|
|
|
|
|
{ 7, 23, 109, 0, 19, 21, 6902 }, // U+013C
|
|
|
|
|
{ 11, 17, 208, 2, 17, 24, 6923 }, // U+013D
|
|
|
|
|
{ 8, 19, 114, 1, 19, 19, 6947 }, // U+013E
|
|
|
|
|
{ 11, 17, 208, 2, 17, 24, 6966 }, // U+013F
|
|
|
|
|
{ 9, 19, 148, 1, 19, 22, 6990 }, // U+0140
|
|
|
|
|
{ 15, 17, 215, -1, 17, 32, 7012 }, // U+0141
|
|
|
|
|
{ 8, 19, 115, 0, 19, 19, 7044 }, // U+0142
|
|
|
|
|
{ 15, 24, 291, 2, 24, 45, 7063 }, // U+0143
|
|
|
|
|
{ 11, 20, 230, 2, 20, 28, 7108 }, // U+0144
|
|
|
|
|
{ 15, 21, 291, 2, 17, 40, 7136 }, // U+0145
|
|
|
|
|
{ 11, 17, 230, 2, 13, 24, 7176 }, // U+0146
|
|
|
|
|
{ 15, 24, 291, 2, 24, 45, 7200 }, // U+0147
|
|
|
|
|
{ 11, 20, 230, 2, 20, 28, 7245 }, // U+0148
|
|
|
|
|
{ 13, 19, 230, 0, 19, 31, 7273 }, // U+0149
|
|
|
|
|
{ 15, 22, 291, 2, 17, 42, 7304 }, // U+014A
|
|
|
|
|
{ 11, 18, 230, 2, 13, 25, 7346 }, // U+014B
|
|
|
|
|
{ 17, 22, 311, 1, 22, 47, 7371 }, // U+014C
|
|
|
|
|
{ 13, 18, 236, 1, 18, 30, 7418 }, // U+014D
|
|
|
|
|
{ 17, 23, 311, 1, 23, 49, 7448 }, // U+014E
|
|
|
|
|
{ 13, 19, 236, 1, 19, 31, 7497 }, // U+014F
|
|
|
|
|
{ 17, 24, 311, 1, 24, 51, 7528 }, // U+0150
|
|
|
|
|
{ 13, 20, 236, 1, 20, 33, 7579 }, // U+0151
|
|
|
|
|
{ 23, 17, 397, 1, 17, 49, 7612 }, // U+0152
|
|
|
|
|
{ 22, 13, 379, 1, 13, 36, 7661 }, // U+0153
|
|
|
|
|
{ 14, 24, 252, 2, 24, 42, 7697 }, // U+0154
|
|
|
|
|
{ 8, 20, 154, 2, 20, 20, 7739 }, // U+0155
|
|
|
|
|
{ 14, 21, 252, 2, 17, 37, 7759 }, // U+0156
|
|
|
|
|
{ 10, 17, 154, 0, 13, 22, 7796 }, // U+0157
|
|
|
|
|
{ 14, 24, 252, 2, 24, 42, 7818 }, // U+0158
|
|
|
|
|
{ 8, 20, 154, 2, 20, 20, 7860 }, // U+0159
|
|
|
|
|
{ 13, 24, 213, 0, 24, 39, 7880 }, // U+015A
|
|
|
|
|
{ 10, 20, 178, 1, 20, 25, 7919 }, // U+015B
|
|
|
|
|
{ 13, 24, 213, 0, 24, 39, 7944 }, // U+015C
|
|
|
|
|
{ 10, 20, 178, 1, 20, 25, 7983 }, // U+015D
|
|
|
|
|
{ 13, 22, 213, 0, 17, 36, 8008 }, // U+015E
|
|
|
|
|
{ 10, 18, 178, 1, 13, 23, 8044 }, // U+015F
|
|
|
|
|
{ 13, 24, 213, 0, 24, 39, 8067 }, // U+0160
|
|
|
|
|
{ 10, 20, 178, 1, 20, 25, 8106 }, // U+0161
|
|
|
|
|
{ 14, 23, 226, 0, 17, 41, 8131 }, // U+0162
|
|
|
|
|
{ 9, 22, 161, 1, 17, 25, 8172 }, // U+0163
|
|
|
|
|
{ 14, 24, 226, 0, 24, 42, 8197 }, // U+0164
|
|
|
|
|
{ 9, 20, 161, 1, 20, 23, 8239 }, // U+0165
|
|
|
|
|
{ 14, 17, 226, 0, 17, 30, 8262 }, // U+0166
|
|
|
|
|
{ 9, 17, 161, 1, 17, 20, 8292 }, // U+0167
|
|
|
|
|
{ 14, 23, 275, 2, 23, 41, 8312 }, // U+0168
|
|
|
|
|
{ 12, 19, 230, 1, 19, 29, 8353 }, // U+0169
|
|
|
|
|
{ 14, 22, 275, 2, 22, 39, 8382 }, // U+016A
|
|
|
|
|
{ 12, 18, 230, 1, 18, 27, 8421 }, // U+016B
|
|
|
|
|
{ 14, 23, 275, 2, 23, 41, 8448 }, // U+016C
|
|
|
|
|
{ 12, 19, 230, 1, 19, 29, 8489 }, // U+016D
|
|
|
|
|
{ 14, 24, 275, 2, 24, 42, 8518 }, // U+016E
|
|
|
|
|
{ 12, 20, 230, 1, 20, 30, 8560 }, // U+016F
|
|
|
|
|
{ 14, 24, 275, 2, 24, 42, 8590 }, // U+0170
|
|
|
|
|
{ 12, 20, 230, 1, 20, 30, 8632 }, // U+0171
|
|
|
|
|
{ 14, 22, 275, 2, 17, 39, 8662 }, // U+0172
|
|
|
|
|
{ 12, 18, 230, 1, 13, 27, 8701 }, // U+0173
|
|
|
|
|
{ 23, 24, 372, 0, 24, 69, 8728 }, // U+0174
|
|
|
|
|
{ 19, 20, 311, 0, 20, 48, 8797 }, // U+0175
|
|
|
|
|
{ 15, 24, 239, 0, 24, 45, 8845 }, // U+0176
|
|
|
|
|
{ 12, 25, 199, 0, 20, 38, 8890 }, // U+0177
|
|
|
|
|
{ 15, 22, 239, 0, 22, 42, 8928 }, // U+0178
|
|
|
|
|
{ 14, 24, 229, 0, 24, 42, 8970 }, // U+0179
|
|
|
|
|
{ 11, 20, 188, 0, 20, 28, 9012 }, // U+017A
|
|
|
|
|
{ 14, 22, 229, 0, 22, 39, 9040 }, // U+017B
|
|
|
|
|
{ 11, 18, 188, 0, 18, 25, 9079 }, // U+017C
|
|
|
|
|
{ 14, 24, 229, 0, 24, 42, 9104 }, // U+017D
|
|
|
|
|
{ 11, 20, 188, 0, 20, 28, 9146 }, // U+017E
|
|
|
|
|
{ 8, 19, 109, 2, 19, 19, 9174 }, // U+017F
|
|
|
|
|
{ 18, 19, 311, 1, 19, 43, 9193 }, // U+01A0
|
|
|
|
|
{ 14, 15, 236, 1, 15, 27, 9236 }, // U+01A1
|
|
|
|
|
{ 17, 20, 284, 2, 20, 43, 9263 }, // U+01AF
|
|
|
|
|
{ 15, 16, 239, 1, 16, 30, 9306 }, // U+01B0
|
|
|
|
|
{ 30, 24, 516, 2, 24, 90, 9336 }, // U+01C4
|
|
|
|
|
{ 27, 20, 475, 2, 20, 68, 9426 }, // U+01C5
|
|
|
|
|
{ 25, 20, 426, 1, 20, 63, 9494 }, // U+01C6
|
|
|
|
|
{ 22, 17, 407, 2, 17, 47, 9557 }, // U+01C7
|
|
|
|
|
{ 16, 23, 310, 2, 18, 46, 9604 }, // U+01C8
|
|
|
|
|
{ 11, 24, 212, 1, 19, 33, 9650 }, // U+01C9
|
|
|
|
|
{ 27, 17, 491, 2, 17, 58, 9683 }, // U+01CA
|
|
|
|
|
{ 21, 23, 394, 2, 18, 61, 9741 }, // U+01CB
|
|
|
|
|
{ 18, 23, 332, 2, 18, 52, 9802 }, // U+01CC
|
|
|
|
|
{ 17, 24, 265, 0, 24, 51, 9854 }, // U+01CD
|
|
|
|
|
{ 11, 20, 209, 1, 20, 28, 9905 }, // U+01CE
|
|
|
|
|
{ 7, 24, 108, 0, 24, 21, 9933 }, // U+01CF
|
|
|
|
|
{ 8, 20, 101, -1, 20, 20, 9954 }, // U+01D0
|
|
|
|
|
{ 17, 24, 311, 1, 24, 51, 9974 }, // U+01D1
|
|
|
|
|
{ 13, 20, 236, 1, 20, 33, 10025 }, // U+01D2
|
|
|
|
|
{ 14, 24, 275, 2, 24, 42, 10058 }, // U+01D3
|
|
|
|
|
{ 12, 20, 230, 1, 20, 30, 10100 }, // U+01D4
|
|
|
|
|
{ 14, 25, 275, 2, 25, 44, 10130 }, // U+01D5
|
|
|
|
|
{ 12, 21, 230, 1, 21, 32, 10174 }, // U+01D6
|
|
|
|
|
{ 14, 24, 275, 2, 24, 42, 10206 }, // U+01D7
|
|
|
|
|
{ 12, 21, 230, 1, 21, 32, 10248 }, // U+01D8
|
|
|
|
|
{ 14, 25, 275, 2, 25, 44, 10280 }, // U+01D9
|
|
|
|
|
{ 12, 22, 230, 1, 22, 33, 10324 }, // U+01DA
|
|
|
|
|
{ 14, 24, 275, 2, 24, 42, 10357 }, // U+01DB
|
|
|
|
|
{ 12, 21, 230, 1, 21, 32, 10399 }, // U+01DC
|
|
|
|
|
{ 12, 13, 224, 1, 13, 20, 10431 }, // U+01DD
|
|
|
|
|
{ 17, 25, 265, 0, 25, 54, 10451 }, // U+01DE
|
|
|
|
|
{ 11, 21, 209, 1, 21, 29, 10505 }, // U+01DF
|
|
|
|
|
{ 17, 25, 265, 0, 25, 54, 10534 }, // U+01E0
|
|
|
|
|
{ 11, 21, 209, 1, 21, 29, 10588 }, // U+01E1
|
|
|
|
|
{ 23, 22, 374, 0, 22, 64, 10617 }, // U+01E2
|
|
|
|
|
{ 20, 18, 344, 1, 18, 45, 10681 }, // U+01E3
|
|
|
|
|
{ 17, 17, 281, 1, 17, 37, 10726 }, // U+01E4
|
|
|
|
|
{ 14, 18, 236, 1, 13, 32, 10763 }, // U+01E5
|
|
|
|
|
{ 15, 24, 269, 1, 24, 45, 10795 }, // U+01E6
|
|
|
|
|
{ 12, 25, 231, 1, 20, 38, 10840 }, // U+01E7
|
|
|
|
|
{ 14, 24, 252, 2, 24, 42, 10878 }, // U+01E8
|
|
|
|
|
{ 12, 25, 209, 1, 25, 38, 10920 }, // U+01E9
|
|
|
|
|
{ 17, 22, 311, 1, 17, 47, 10958 }, // U+01EA
|
|
|
|
|
{ 13, 18, 236, 1, 13, 30, 11005 }, // U+01EB
|
|
|
|
|
{ 17, 27, 311, 1, 22, 58, 11035 }, // U+01EC
|
|
|
|
|
{ 13, 23, 236, 1, 18, 38, 11093 }, // U+01ED
|
|
|
|
|
{ 13, 24, 227, 0, 24, 39, 11131 }, // U+01EE
|
|
|
|
|
{ 12, 25, 188, -1, 20, 38, 11170 }, // U+01EF
|
|
|
|
|
{ 9, 25, 101, -2, 20, 29, 11208 }, // U+01F0
|
|
|
|
|
{ 30, 17, 516, 2, 17, 64, 11237 }, // U+01F1
|
|
|
|
|
{ 27, 17, 475, 2, 17, 58, 11301 }, // U+01F2
|
|
|
|
|
{ 25, 19, 426, 1, 19, 60, 11359 }, // U+01F3
|
|
|
|
|
{ 15, 24, 269, 1, 24, 45, 11419 }, // U+01F4
|
|
|
|
|
{ 12, 25, 231, 1, 20, 38, 11464 }, // U+01F5
|
|
|
|
|
{ 23, 17, 423, 2, 17, 49, 11502 }, // U+01F6
|
|
|
|
|
{ 13, 22, 257, 2, 17, 36, 11551 }, // U+01F7
|
|
|
|
|
{ 15, 24, 291, 2, 24, 45, 11587 }, // U+01F8
|
|
|
|
|
{ 11, 20, 230, 2, 20, 28, 11632 }, // U+01F9
|
|
|
|
|
{ 17, 23, 265, 0, 23, 49, 11660 }, // U+01FA
|
|
|
|
|
{ 11, 21, 209, 1, 21, 29, 11709 }, // U+01FB
|
|
|
|
|
{ 23, 24, 374, 0, 24, 69, 11738 }, // U+01FC
|
|
|
|
|
{ 20, 20, 344, 1, 20, 50, 11807 }, // U+01FD
|
|
|
|
|
{ 17, 25, 311, 1, 24, 54, 11857 }, // U+01FE
|
|
|
|
|
{ 13, 21, 236, 1, 20, 35, 11911 }, // U+01FF
|
|
|
|
|
{ 17, 24, 265, 0, 24, 51, 11946 }, // U+0200
|
|
|
|
|
{ 11, 20, 209, 1, 20, 28, 11997 }, // U+0201
|
|
|
|
|
{ 17, 23, 265, 0, 23, 49, 12025 }, // U+0202
|
|
|
|
|
{ 11, 19, 209, 1, 19, 27, 12074 }, // U+0203
|
|
|
|
|
{ 12, 24, 228, 2, 24, 36, 12101 }, // U+0204
|
|
|
|
|
{ 12, 20, 224, 1, 20, 30, 12137 }, // U+0205
|
|
|
|
|
{ 12, 23, 228, 2, 23, 35, 12167 }, // U+0206
|
|
|
|
|
{ 12, 19, 224, 1, 19, 29, 12202 }, // U+0207
|
|
|
|
|
{ 9, 24, 108, -2, 24, 27, 12231 }, // U+0208
|
|
|
|
|
{ 10, 20, 101, -3, 20, 25, 12258 }, // U+0209
|
|
|
|
|
{ 7, 23, 108, 0, 23, 21, 12283 }, // U+020A
|
|
|
|
|
{ 8, 19, 101, -1, 19, 19, 12304 }, // U+020B
|
|
|
|
|
{ 17, 24, 311, 1, 24, 51, 12323 }, // U+020C
|
|
|
|
|
{ 13, 20, 236, 1, 20, 33, 12374 }, // U+020D
|
|
|
|
|
{ 17, 23, 311, 1, 23, 49, 12407 }, // U+020E
|
|
|
|
|
{ 13, 19, 236, 1, 19, 31, 12456 }, // U+020F
|
|
|
|
|
{ 14, 24, 252, 2, 24, 42, 12487 }, // U+0210
|
|
|
|
|
{ 10, 20, 154, 0, 20, 25, 12529 }, // U+0211
|
|
|
|
|
{ 14, 23, 252, 2, 23, 41, 12554 }, // U+0212
|
|
|
|
|
{ 8, 19, 154, 2, 19, 19, 12595 }, // U+0213
|
|
|
|
|
{ 14, 24, 275, 2, 24, 42, 12614 }, // U+0214
|
|
|
|
|
{ 12, 20, 230, 1, 20, 30, 12656 }, // U+0215
|
|
|
|
|
{ 14, 23, 275, 2, 23, 41, 12686 }, // U+0216
|
|
|
|
|
{ 12, 19, 230, 1, 19, 29, 12727 }, // U+0217
|
|
|
|
|
{ 13, 21, 213, 0, 17, 35, 12756 }, // U+0218
|
|
|
|
|
{ 10, 17, 178, 1, 13, 22, 12791 }, // U+0219
|
|
|
|
|
{ 14, 21, 226, 0, 17, 37, 12813 }, // U+021A
|
|
|
|
|
{ 9, 21, 161, 1, 17, 24, 12850 }, // U+021B
|
|
|
|
|
{ 13, 22, 214, 0, 17, 36, 12874 }, // U+021C
|
|
|
|
|
{ 10, 18, 174, 0, 13, 23, 12910 }, // U+021D
|
|
|
|
|
{ 14, 24, 282, 2, 24, 42, 12933 }, // U+021E
|
|
|
|
|
{ 12, 25, 228, 1, 25, 38, 12975 }, // U+021F
|
|
|
|
|
{ 8, 4, 150, 1, 19, 4, 13013 }, // U+0311
|
|
|
|
|
{ 12, 24, 228, 2, 24, 36, 13017 }, // U+0400
|
|
|
|
|
{ 12, 22, 228, 2, 22, 33, 13053 }, // U+0401
|
|
|
|
|
{ 18, 17, 300, 0, 17, 39, 13086 }, // U+0402
|
|
|
|
|
{ 11, 24, 204, 2, 24, 33, 13125 }, // U+0403
|
|
|
|
|
{ 14, 17, 248, 1, 17, 30, 13158 }, // U+0404
|
|
|
|
|
{ 13, 17, 213, 0, 17, 28, 13188 }, // U+0405
|
|
|
|
|
{ 3, 17, 108, 2, 17, 7, 13216 }, // U+0406
|
|
|
|
|
{ 9, 22, 108, -1, 22, 25, 13223 }, // U+0407
|
|
|
|
|
{ 11, 17, 200, 0, 17, 24, 13248 }, // U+0408
|
|
|
|
|
{ 26, 18, 417, 0, 17, 59, 13272 }, // U+0409
|
|
|
|
|
{ 23, 17, 410, 2, 17, 49, 13331 }, // U+040A
|
|
|
|
|
{ 18, 17, 300, 0, 17, 39, 13380 }, // U+040B
|
|
|
|
|
{ 14, 24, 252, 2, 24, 42, 13419 }, // U+040C
|
|
|
|
|
{ 15, 24, 291, 2, 24, 45, 13461 }, // U+040D
|
|
|
|
|
{ 15, 23, 235, 0, 23, 44, 13506 }, // U+040E
|
|
|
|
|
{ 14, 22, 280, 2, 17, 39, 13550 }, // U+040F
|
|
|
|
|
{ 17, 17, 265, 0, 17, 37, 13589 }, // U+0410
|
|
|
|
|
{ 13, 17, 248, 2, 17, 28, 13626 }, // U+0411
|
|
|
|
|
{ 13, 17, 257, 2, 17, 28, 13654 }, // U+0412
|
|
|
|
|
{ 11, 17, 204, 2, 17, 24, 13682 }, // U+0413
|
|
|
|
|
{ 18, 22, 289, 0, 17, 50, 13706 }, // U+0414
|
|
|
|
|
{ 12, 17, 228, 2, 17, 26, 13756 }, // U+0415
|
|
|
|
|
{ 23, 17, 362, 0, 17, 49, 13782 }, // U+0416
|
|
|
|
|
{ 14, 17, 229, 0, 17, 30, 13831 }, // U+0417
|
|
|
|
|
{ 15, 17, 291, 2, 17, 32, 13861 }, // U+0418
|
|
|
|
|
{ 15, 23, 291, 2, 23, 44, 13893 }, // U+0419
|
|
|
|
|
{ 14, 17, 252, 2, 17, 30, 13937 }, // U+041A
|
|
|
|
|
{ 16, 17, 283, 0, 17, 34, 13967 }, // U+041B
|
|
|
|
|
{ 20, 17, 348, 1, 17, 43, 14001 }, // U+041C
|
|
|
|
|
{ 14, 17, 282, 2, 17, 30, 14044 }, // U+041D
|
|
|
|
|
{ 17, 17, 311, 1, 17, 37, 14074 }, // U+041E
|
|
|
|
|
{ 14, 17, 280, 2, 17, 30, 14111 }, // U+041F
|
|
|
|
|
{ 13, 17, 243, 2, 17, 28, 14141 }, // U+0420
|
|
|
|
|
{ 14, 17, 248, 1, 17, 30, 14169 }, // U+0421
|
|
|
|
|
{ 14, 17, 226, 0, 17, 30, 14199 }, // U+0422
|
|
|
|
|
{ 15, 17, 235, 0, 17, 32, 14229 }, // U+0423
|
|
|
|
|
{ 20, 18, 346, 1, 18, 45, 14261 }, // U+0424
|
|
|
|
|
{ 16, 17, 252, 0, 17, 34, 14306 }, // U+0425
|
|
|
|
|
{ 16, 22, 287, 2, 17, 44, 14340 }, // U+0426
|
|
|
|
|
{ 13, 17, 252, 1, 17, 28, 14384 }, // U+0427
|
|
|
|
|
{ 21, 17, 393, 2, 17, 45, 14412 }, // U+0428
|
|
|
|
|
{ 23, 22, 401, 2, 17, 64, 14457 }, // U+0429
|
|
|
|
|
{ 18, 17, 290, 0, 17, 39, 14521 }, // U+042A
|
|
|
|
|
{ 18, 17, 342, 2, 17, 39, 14560 }, // U+042B
|
|
|
|
|
{ 13, 17, 246, 2, 17, 28, 14599 }, // U+042C
|
|
|
|
|
{ 15, 17, 248, 0, 17, 32, 14627 }, // U+042D
|
|
|
|
|
{ 23, 17, 412, 2, 17, 49, 14659 }, // U+042E
|
|
|
|
|
{ 14, 17, 254, 0, 17, 30, 14708 }, // U+042F
|
|
|
|
|
{ 11, 13, 209, 1, 13, 18, 14738 }, // U+0430
|
|
|
|
|
{ 13, 19, 238, 1, 19, 31, 14756 }, // U+0431
|
|
|
|
|
{ 11, 13, 219, 2, 13, 18, 14787 }, // U+0432
|
|
|
|
|
{ 9, 13, 167, 2, 13, 15, 14805 }, // U+0433
|
|
|
|
|
{ 15, 17, 241, 0, 13, 32, 14820 }, // U+0434
|
|
|
|
|
{ 12, 13, 224, 1, 13, 20, 14852 }, // U+0435
|
|
|
|
|
{ 19, 13, 305, 0, 13, 31, 14872 }, // U+0436
|
|
|
|
|
{ 10, 13, 196, 1, 13, 17, 14903 }, // U+0437
|
|
|
|
|
{ 11, 13, 239, 2, 13, 18, 14920 }, // U+0438
|
|
|
|
|
{ 11, 19, 239, 2, 19, 27, 14938 }, // U+0439
|
|
|
|
|
{ 12, 13, 215, 2, 13, 20, 14965 }, // U+043A
|
|
|
|
|
{ 13, 13, 239, 0, 13, 22, 14985 }, // U+043B
|
|
|
|
|
{ 16, 13, 292, 1, 13, 26, 15007 }, // U+043C
|
|
|
|
|
{ 11, 13, 234, 2, 13, 18, 15033 }, // U+043D
|
|
|
|
|
{ 13, 13, 236, 1, 13, 22, 15051 }, // U+043E
|
|
|
|
|
{ 11, 13, 231, 2, 13, 18, 15073 }, // U+043F
|
|
|
|
|
{ 12, 18, 236, 2, 13, 27, 15091 }, // U+0440
|
|
|
|
|
{ 11, 13, 186, 1, 13, 18, 15118 }, // U+0441
|
|
|
|
|
{ 12, 13, 185, 0, 13, 20, 15136 }, // U+0442
|
|
|
|
|
{ 12, 18, 199, 0, 13, 27, 15156 }, // U+0443
|
|
|
|
|
{ 17, 24, 306, 1, 19, 51, 15183 }, // U+0444
|
|
|
|
|
{ 13, 13, 204, 0, 13, 22, 15234 }, // U+0445
|
|
|
|
|
{ 13, 17, 234, 2, 13, 28, 15256 }, // U+0446
|
|
|
|
|
{ 11, 13, 213, 1, 13, 18, 15284 }, // U+0447
|
|
|
|
|
{ 17, 13, 329, 2, 13, 28, 15302 }, // U+0448
|
|
|
|
|
{ 19, 17, 334, 2, 13, 41, 15330 }, // U+0449
|
|
|
|
|
{ 15, 13, 239, 0, 13, 25, 15371 }, // U+044A
|
|
|
|
|
{ 15, 13, 303, 2, 13, 25, 15396 }, // U+044B
|
|
|
|
|
{ 11, 13, 211, 2, 13, 18, 15421 }, // U+044C
|
|
|
|
|
{ 12, 13, 199, 0, 13, 20, 15439 }, // U+044D
|
|
|
|
|
{ 17, 13, 324, 2, 13, 28, 15459 }, // U+044E
|
|
|
|
|
{ 12, 13, 219, 0, 13, 20, 15487 }, // U+044F
|
|
|
|
|
{ 12, 20, 224, 1, 20, 30, 15507 }, // U+0450
|
|
|
|
|
{ 12, 18, 224, 1, 18, 27, 15537 }, // U+0451
|
|
|
|
|
{ 13, 24, 228, 0, 19, 39, 15564 }, // U+0452
|
|
|
|
|
{ 9, 20, 167, 2, 20, 23, 15603 }, // U+0453
|
|
|
|
|
{ 11, 13, 199, 1, 13, 18, 15626 }, // U+0454
|
|
|
|
|
{ 10, 13, 178, 1, 13, 17, 15644 }, // U+0455
|
|
|
|
|
{ 4, 18, 101, 1, 18, 9, 15661 }, // U+0456
|
|
|
|
|
{ 9, 18, 101, -1, 18, 21, 15670 }, // U+0457
|
|
|
|
|
{ 7, 23, 101, -2, 18, 21, 15691 }, // U+0458
|
|
|
|
|
{ 21, 13, 345, 0, 13, 35, 15712 }, // U+0459
|
|
|
|
|
{ 19, 13, 340, 2, 13, 31, 15747 }, // U+045A
|
|
|
|
|
{ 13, 19, 228, 0, 19, 31, 15778 }, // U+045B
|
|
|
|
|
{ 12, 20, 215, 2, 20, 30, 15809 }, // U+045C
|
|
|
|
|
{ 11, 20, 239, 2, 20, 28, 15839 }, // U+045D
|
|
|
|
|
{ 12, 24, 199, 0, 19, 36, 15867 }, // U+045E
|
|
|
|
|
{ 11, 17, 230, 2, 13, 24, 15903 }, // U+045F
|
|
|
|
|
{ 18, 18, 290, 0, 18, 41, 15927 }, // U+0462
|
|
|
|
|
{ 15, 19, 239, 0, 19, 36, 15968 }, // U+0463
|
|
|
|
|
{ 17, 17, 311, 1, 17, 37, 16004 }, // U+0472
|
|
|
|
|
{ 13, 13, 236, 1, 13, 22, 16041 }, // U+0473
|
|
|
|
|
{ 20, 17, 301, 0, 17, 43, 16063 }, // U+0474
|
|
|
|
|
{ 15, 13, 226, 0, 13, 25, 16106 }, // U+0475
|
|
|
|
|
{ 17, 28, 298, 2, 23, 60, 16131 }, // U+048A
|
|
|
|
|
{ 14, 24, 243, 2, 19, 42, 16191 }, // U+048B
|
|
|
|
|
{ 15, 18, 252, 0, 18, 34, 16233 }, // U+048C
|
|
|
|
|
{ 13, 13, 210, 0, 13, 22, 16267 }, // U+048D
|
|
|
|
|
{ 13, 17, 243, 2, 17, 28, 16289 }, // U+048E
|
|
|
|
|
{ 12, 18, 236, 2, 13, 27, 16317 }, // U+048F
|
|
|
|
|
{ 11, 20, 204, 2, 20, 28, 16344 }, // U+0490
|
|
|
|
|
{ 9, 16, 171, 2, 16, 18, 16372 }, // U+0491
|
|
|
|
|
{ 13, 17, 209, 0, 17, 28, 16390 }, // U+0492
|
|
|
|
|
{ 11, 13, 170, 0, 13, 18, 16418 }, // U+0493
|
|
|
|
|
{ 13, 22, 250, 2, 17, 36, 16436 }, // U+0494
|
|
|
|
|
{ 11, 18, 218, 2, 13, 25, 16472 }, // U+0495
|
|
|
|
|
{ 23, 22, 368, 0, 17, 64, 16497 }, // U+0496
|
|
|
|
|
{ 20, 17, 310, 0, 13, 43, 16561 }, // U+0497
|
|
|
|
|
{ 14, 22, 229, 0, 17, 39, 16604 }, // U+0498
|
|
|
|
|
{ 10, 18, 196, 1, 13, 23, 16643 }, // U+0499
|
|
|
|
|
{ 15, 22, 262, 2, 17, 42, 16666 }, // U+049A
|
|
|
|
|
{ 12, 17, 218, 2, 13, 26, 16708 }, // U+049B
|
|
|
|
|
{ 16, 17, 283, 2, 17, 34, 16734 }, // U+049C
|
|
|
|
|
{ 14, 13, 248, 2, 13, 23, 16768 }, // U+049D
|
|
|
|
|
{ 16, 17, 256, 0, 17, 34, 16791 }, // U+049E
|
|
|
|
|
{ 14, 13, 222, 0, 13, 23, 16825 }, // U+049F
|
|
|
|
|
{ 19, 17, 296, 0, 17, 41, 16848 }, // U+04A0
|
|
|
|
|
{ 15, 13, 243, 0, 13, 25, 16889 }, // U+04A1
|
|
|
|
|
{ 16, 22, 290, 2, 17, 44, 16914 }, // U+04A2
|
|
|
|
|
{ 13, 17, 239, 2, 13, 28, 16958 }, // U+04A3
|
|
|
|
|
{ 18, 17, 326, 2, 17, 39, 16986 }, // U+04A4
|
|
|
|
|
{ 14, 13, 262, 2, 13, 23, 17025 }, // U+04A5
|
|
|
|
|
{ 23, 22, 413, 2, 17, 64, 17048 }, // U+04A6
|
|
|
|
|
{ 19, 18, 344, 2, 13, 43, 17112 }, // U+04A7
|
|
|
|
|
{ 18, 22, 314, 1, 17, 50, 17155 }, // U+04A8
|
|
|
|
|
{ 14, 17, 244, 1, 13, 30, 17205 }, // U+04A9
|
|
|
|
|
{ 14, 23, 248, 1, 17, 41, 17235 }, // U+04AA
|
|
|
|
|
{ 11, 19, 186, 1, 13, 27, 17276 }, // U+04AB
|
|
|
|
|
{ 14, 22, 226, 0, 17, 39, 17303 }, // U+04AC
|
|
|
|
|
{ 12, 17, 185, 0, 13, 26, 17342 }, // U+04AD
|
|
|
|
|
{ 15, 17, 239, 0, 17, 32, 17368 }, // U+04AE
|
|
|
|
|
{ 13, 18, 201, 0, 13, 30, 17400 }, // U+04AF
|
|
|
|
|
{ 15, 17, 239, 0, 17, 32, 17430 }, // U+04B0
|
|
|
|
|
{ 13, 18, 201, 0, 13, 30, 17462 }, // U+04B1
|
|
|
|
|
{ 17, 22, 261, 0, 17, 47, 17492 }, // U+04B2
|
|
|
|
|
{ 13, 17, 210, 0, 13, 28, 17539 }, // U+04B3
|
|
|
|
|
{ 21, 22, 338, 0, 17, 58, 17567 }, // U+04B4
|
|
|
|
|
{ 16, 17, 264, 0, 13, 34, 17625 }, // U+04B5
|
|
|
|
|
{ 15, 22, 260, 1, 17, 42, 17659 }, // U+04B6
|
|
|
|
|
{ 13, 17, 218, 1, 13, 28, 17701 }, // U+04B7
|
|
|
|
|
{ 13, 17, 255, 1, 17, 28, 17729 }, // U+04B8
|
|
|
|
|
{ 12, 13, 213, 0, 13, 20, 17757 }, // U+04B9
|
|
|
|
|
{ 13, 17, 256, 2, 17, 28, 17777 }, // U+04BA
|
|
|
|
|
{ 11, 19, 228, 2, 19, 27, 17805 }, // U+04BB
|
|
|
|
|
{ 20, 17, 334, 0, 17, 43, 17832 }, // U+04BC
|
|
|
|
|
{ 16, 13, 268, 0, 13, 26, 17875 }, // U+04BD
|
|
|
|
|
{ 20, 22, 334, 0, 17, 55, 17901 }, // U+04BE
|
|
|
|
|
{ 16, 17, 268, 0, 13, 34, 17956 }, // U+04BF
|
|
|
|
|
{ 3, 17, 108, 2, 17, 7, 17990 }, // U+04C0
|
|
|
|
|
{ 23, 23, 362, 0, 23, 67, 17997 }, // U+04C1
|
|
|
|
|
{ 19, 19, 305, 0, 19, 46, 18064 }, // U+04C2
|
|
|
|
|
{ 13, 22, 252, 2, 17, 36, 18110 }, // U+04C3
|
|
|
|
|
{ 11, 18, 218, 2, 13, 25, 18146 }, // U+04C4
|
|
|
|
|
{ 18, 22, 290, 0, 17, 50, 18171 }, // U+04C5
|
|
|
|
|
{ 16, 18, 244, 0, 13, 36, 18221 }, // U+04C6
|
|
|
|
|
{ 14, 22, 282, 2, 17, 39, 18257 }, // U+04C7
|
|
|
|
|
{ 11, 18, 234, 2, 13, 25, 18296 }, // U+04C8
|
|
|
|
|
{ 16, 22, 289, 2, 17, 44, 18321 }, // U+04C9
|
|
|
|
|
{ 13, 18, 239, 2, 13, 30, 18365 }, // U+04CA
|
|
|
|
|
{ 13, 22, 255, 1, 17, 36, 18395 }, // U+04CB
|
|
|
|
|
{ 11, 17, 213, 1, 13, 24, 18431 }, // U+04CC
|
|
|
|
|
{ 22, 22, 360, 1, 17, 61, 18455 }, // U+04CD
|
|
|
|
|
{ 18, 18, 300, 1, 13, 41, 18516 }, // U+04CE
|
|
|
|
|
{ 3, 17, 108, 2, 17, 7, 18557 }, // U+04CF
|
|
|
|
|
{ 17, 23, 265, 0, 23, 49, 18564 }, // U+04D0
|
|
|
|
|
{ 11, 19, 209, 1, 19, 27, 18613 }, // U+04D1
|
|
|
|
|
{ 17, 22, 265, 0, 22, 47, 18640 }, // U+04D2
|
|
|
|
|
{ 11, 18, 209, 1, 18, 25, 18687 }, // U+04D3
|
|
|
|
|
{ 23, 17, 374, 0, 17, 49, 18712 }, // U+04D4
|
|
|
|
|
{ 20, 13, 344, 1, 13, 33, 18761 }, // U+04D5
|
|
|
|
|
{ 12, 23, 228, 2, 23, 35, 18794 }, // U+04D6
|
|
|
|
|
{ 12, 19, 224, 1, 19, 29, 18829 }, // U+04D7
|
|
|
|
|
{ 16, 17, 282, 1, 17, 34, 18858 }, // U+04D8
|
|
|
|
|
{ 12, 13, 224, 1, 13, 20, 18892 }, // U+04D9
|
|
|
|
|
{ 16, 22, 282, 1, 22, 44, 18912 }, // U+04DA
|
|
|
|
|
{ 12, 18, 224, 1, 18, 27, 18956 }, // U+04DB
|
|
|
|
|
{ 23, 22, 362, 0, 22, 64, 18983 }, // U+04DC
|
|
|
|
|
{ 19, 18, 305, 0, 18, 43, 19047 }, // U+04DD
|
|
|
|
|
{ 14, 22, 229, 0, 22, 39, 19090 }, // U+04DE
|
|
|
|
|
{ 10, 18, 196, 1, 18, 23, 19129 }, // U+04DF
|
|
|
|
|
{ 13, 17, 227, 0, 17, 28, 19152 }, // U+04E0
|
|
|
|
|
{ 12, 18, 188, -1, 13, 27, 19180 }, // U+04E1
|
|
|
|
|
{ 15, 22, 291, 2, 22, 42, 19207 }, // U+04E2
|
|
|
|
|
{ 11, 18, 239, 2, 18, 25, 19249 }, // U+04E3
|
|
|
|
|
{ 15, 22, 291, 2, 22, 42, 19274 }, // U+04E4
|
|
|
|
|
{ 11, 18, 239, 2, 18, 25, 19316 }, // U+04E5
|
|
|
|
|
{ 17, 22, 311, 1, 22, 47, 19341 }, // U+04E6
|
|
|
|
|
{ 13, 18, 236, 1, 18, 30, 19388 }, // U+04E7
|
|
|
|
|
{ 17, 17, 311, 1, 17, 37, 19418 }, // U+04E8
|
|
|
|
|
{ 13, 13, 236, 1, 13, 22, 19455 }, // U+04E9
|
|
|
|
|
{ 17, 22, 311, 1, 22, 47, 19477 }, // U+04EA
|
|
|
|
|
{ 13, 18, 236, 1, 18, 30, 19524 }, // U+04EB
|
|
|
|
|
{ 15, 22, 248, 0, 22, 42, 19554 }, // U+04EC
|
|
|
|
|
{ 12, 18, 199, 0, 18, 27, 19596 }, // U+04ED
|
|
|
|
|
{ 15, 22, 235, 0, 22, 42, 19623 }, // U+04EE
|
|
|
|
|
{ 12, 23, 199, 0, 18, 35, 19665 }, // U+04EF
|
|
|
|
|
{ 15, 22, 235, 0, 22, 42, 19700 }, // U+04F0
|
|
|
|
|
{ 12, 23, 199, 0, 18, 35, 19742 }, // U+04F1
|
|
|
|
|
{ 15, 24, 235, 0, 24, 45, 19777 }, // U+04F2
|
|
|
|
|
{ 12, 25, 199, 0, 20, 38, 19822 }, // U+04F3
|
|
|
|
|
{ 13, 22, 252, 1, 22, 36, 19860 }, // U+04F4
|
|
|
|
|
{ 11, 18, 213, 1, 18, 25, 19896 }, // U+04F5
|
|
|
|
|
{ 11, 22, 204, 2, 17, 31, 19921 }, // U+04F6
|
|
|
|
|
{ 9, 17, 167, 2, 13, 20, 19952 }, // U+04F7
|
|
|
|
|
{ 18, 22, 342, 2, 22, 50, 19972 }, // U+04F8
|
|
|
|
|
{ 15, 18, 303, 2, 18, 34, 20022 }, // U+04F9
|
|
|
|
|
{ 15, 24, 239, 0, 24, 45, 20056 }, // U+1EF2
|
|
|
|
|
{ 12, 26, 199, 0, 21, 39, 20101 }, // U+1EF3
|
|
|
|
|
{ 14, 2, 198, -1, 9, 4, 20140 }, // U+2013
|
|
|
|
|
{ 26, 2, 398, -1, 9, 7, 20144 }, // U+2014
|
|
|
|
|
{ 26, 2, 398, -1, 9, 7, 20151 }, // U+2015
|
|
|
|
|
{ 5, 6, 94, 1, 19, 4, 20158 }, // U+2018
|
|
|
|
|
{ 5, 6, 94, 0, 19, 4, 20162 }, // U+2019
|
|
|
|
|
{ 5, 6, 94, 0, 3, 4, 20166 }, // U+201A
|
|
|
|
|
{ 9, 6, 167, 1, 19, 7, 20170 }, // U+201C
|
|
|
|
|
{ 9, 6, 167, 0, 19, 7, 20177 }, // U+201D
|
|
|
|
|
{ 9, 6, 167, 0, 3, 7, 20184 }, // U+201E
|
|
|
|
|
{ 11, 21, 190, 0, 17, 29, 20191 }, // U+2020
|
|
|
|
|
{ 11, 21, 190, 0, 17, 29, 20220 }, // U+2021
|
|
|
|
|
{ 7, 6, 144, 1, 12, 6, 20249 }, // U+2022
|
|
|
|
|
{ 21, 3, 398, 2, 3, 8, 20255 }, // U+2026
|
|
|
|
|
{ 29, 17, 491, 1, 17, 62, 20263 }, // U+2030
|
|
|
|
|
{ 7, 11, 116, 0, 13, 10, 20325 }, // U+2039
|
|
|
|
|
{ 6, 11, 116, 1, 13, 9, 20335 }, // U+203A
|
|
|
|
|
{ 13, 17, 59, -5, 17, 28, 20344 }, // U+2044
|
|
|
|
|
{ 9, 10, 144, 0, 18, 12, 20372 }, // U+2070
|
|
|
|
|
{ 9, 10, 144, 0, 18, 12, 20384 }, // U+2074
|
|
|
|
|
{ 7, 10, 144, 1, 18, 9, 20396 }, // U+2075
|
|
|
|
|
{ 9, 10, 144, 0, 18, 12, 20405 }, // U+2076
|
|
|
|
|
{ 9, 10, 144, 0, 18, 12, 20417 }, // U+2077
|
|
|
|
|
{ 9, 10, 144, 0, 18, 12, 20429 }, // U+2078
|
|
|
|
|
{ 9, 10, 144, 0, 18, 12, 20441 }, // U+2079
|
|
|
|
|
{ 9, 10, 144, 0, 10, 12, 20453 }, // U+2080
|
|
|
|
|
{ 5, 10, 144, 1, 10, 7, 20465 }, // U+2081
|
|
|
|
|
{ 8, 10, 144, 0, 10, 10, 20472 }, // U+2082
|
|
|
|
|
{ 8, 10, 144, 0, 10, 10, 20482 }, // U+2083
|
|
|
|
|
{ 9, 10, 144, 0, 10, 12, 20492 }, // U+2084
|
|
|
|
|
{ 7, 10, 144, 1, 10, 9, 20504 }, // U+2085
|
|
|
|
|
{ 9, 10, 144, 0, 10, 12, 20513 }, // U+2086
|
|
|
|
|
{ 9, 10, 144, 0, 10, 12, 20525 }, // U+2087
|
|
|
|
|
{ 9, 10, 144, 0, 10, 12, 20537 }, // U+2088
|
|
|
|
|
{ 9, 10, 144, 0, 10, 12, 20549 }, // U+2089
|
|
|
|
|
{ 13, 17, 226, 1, 17, 28, 20561 }, // U+20AC
|
|
|
|
|
{ 13, 17, 226, 1, 17, 28, 20589 }, // U+20AE
|
|
|
|
|
{ 14, 17, 226, 0, 17, 30, 20617 }, // U+20B4
|
|
|
|
|
{ 13, 17, 226, 1, 17, 28, 20647 }, // U+20B9
|
|
|
|
|
{ 13, 19, 234, 1, 19, 31, 20675 }, // U+2202
|
|
|
|
|
{ 17, 17, 260, 0, 17, 37, 20706 }, // U+2206
|
|
|
|
|
{ 14, 20, 279, 2, 17, 35, 20743 }, // U+220F
|
|
|
|
|
{ 13, 20, 206, 0, 17, 33, 20778 }, // U+2211
|
|
|
|
|
{ 12, 2, 226, 1, 8, 3, 20811 }, // U+2212
|
|
|
|
|
{ 13, 17, 59, -5, 17, 28, 20814 }, // U+2215
|
|
|
|
|
{ 4, 3, 98, 1, 9, 2, 20842 }, // U+2219
|
|
|
|
|
{ 15, 20, 226, 0, 20, 38, 20844 }, // U+221A
|
|
|
|
|
{ 14, 7, 235, 0, 11, 13, 20882 }, // U+221E
|
|
|
|
|
{ 10, 24, 148, 0, 19, 30, 20895 }, // U+222B
|
|
|
|
|
{ 12, 10, 226, 1, 13, 15, 20925 }, // U+2248
|
|
|
|
|
{ 12, 13, 226, 1, 14, 20, 20940 }, // U+2260
|
|
|
|
|
{ 12, 15, 226, 1, 15, 23, 20960 }, // U+2264
|
|
|
|
|
{ 12, 15, 226, 1, 15, 23, 20983 }, // U+2265
|
|
|
|
|
{ 18, 19, 310, 2, 19, 43, 21006 }, // U+FB00
|
|
|
|
|
{ 13, 19, 257, 2, 19, 31, 21049 }, // U+FB01
|
|
|
|
|
{ 14, 19, 265, 2, 19, 34, 21080 }, // U+FB02
|
|
|
|
|
{ 23, 19, 413, 2, 19, 55, 21114 }, // U+FB03
|
|
|
|
|
{ 24, 19, 421, 2, 19, 57, 21169 }, // U+FB04
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const EpdUnicodeInterval ubuntu_12_regularIntervals[] = {
|
|
|
|
|
{ 0x0, 0x0, 0x0 },
|
|
|
|
|
{ 0x8, 0x9, 0x1 },
|
|
|
|
|
{ 0xD, 0xD, 0x3 },
|
|
|
|
|
{ 0x1D, 0x1D, 0x4 },
|
|
|
|
|
{ 0x20, 0x7E, 0x5 },
|
|
|
|
|
{ 0xA0, 0xFF, 0x64 },
|
|
|
|
|
{ 0x100, 0x17F, 0xC4 },
|
feat: Vietnamese glyphs support (#1147)
## Summary
* **What is the goal of this PR?**
Add Vietnamese glyphs support for the reader's built-in fonts, enabling
proper rendering of Vietnamese text in EPUB content.
* **What changes are included?**
- Added 3 new Unicode intervals to `fontconvert.py` covering Vietnamese
characters:
- **Latin Extended-B** (Vietnamese subset only): `U+01A0–U+01B0` — Ơ/ơ,
Ư/ư
- **Vietnamese Extended**: `U+1EA0–U+1EF9` — All precomposed Vietnamese
characters with tone marks (Ả, Ấ, Ầ, Ẩ, Ẫ, Ậ, Ắ, …, Ỹ)
- Re-generated all 54 built-in font header files (Bookerly, Noto Sans,
OpenDyslexic, Ubuntu across all sizes and styles) to include the new
Vietnamese glyphs.
## Additional Context
* **Scope**: This PR only covers the **reader** fonts. The outer UI
still uses the Ubuntu font which does not fully support Vietnamese — UI
and i18n will be addressed in a follow-up PR (per discussion in PR
#1124).
* **Memory impact**:
| Metric | Before | After | Delta |
|---|---|---|---|
| Flash Data (`.rodata`) | 2,971,028 B | 3,290,748 B | **+319,720 B
(+10.8%)** |
| Total image size | 4,663,235 B | 4,982,955 B | **+319,720 B (+6.9%)**
|
| Flash usage | 69.1% | 74.0% | **+4.9 pp** |
| RAM usage | 29.0% | 29.0% | **No change** |
* **Risk**: Low — this is a data-only change (font glyph tables in
`.rodata`). No logic changes, no RAM impact. Flash headroom remains
comfortable at 74%.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
AI was used to identify the minimal set of Unicode ranges needed for
Vietnamese support and to assist with the PR description.
---------
Co-authored-by: danoooob <danoooob@example.com>
2026-02-25 00:21:39 +07:00
|
|
|
{ 0x1A0, 0x1A1, 0x144 },
|
|
|
|
|
{ 0x1AF, 0x1B0, 0x146 },
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x1C4, 0x21F, 0x148 },
|
|
|
|
|
{ 0x311, 0x311, 0x1A4 },
|
|
|
|
|
{ 0x400, 0x45F, 0x1A5 },
|
|
|
|
|
{ 0x462, 0x463, 0x205 },
|
|
|
|
|
{ 0x472, 0x475, 0x207 },
|
|
|
|
|
{ 0x48A, 0x4F9, 0x20B },
|
|
|
|
|
{ 0x1EF2, 0x1EF3, 0x27B },
|
|
|
|
|
{ 0x2013, 0x2015, 0x27D },
|
|
|
|
|
{ 0x2018, 0x201A, 0x280 },
|
|
|
|
|
{ 0x201C, 0x201E, 0x283 },
|
|
|
|
|
{ 0x2020, 0x2022, 0x286 },
|
|
|
|
|
{ 0x2026, 0x2026, 0x289 },
|
|
|
|
|
{ 0x2030, 0x2030, 0x28A },
|
|
|
|
|
{ 0x2039, 0x203A, 0x28B },
|
|
|
|
|
{ 0x2044, 0x2044, 0x28D },
|
|
|
|
|
{ 0x2070, 0x2070, 0x28E },
|
|
|
|
|
{ 0x2074, 0x2079, 0x28F },
|
|
|
|
|
{ 0x2080, 0x2089, 0x295 },
|
|
|
|
|
{ 0x20AC, 0x20AC, 0x29F },
|
|
|
|
|
{ 0x20AE, 0x20AE, 0x2A0 },
|
|
|
|
|
{ 0x20B4, 0x20B4, 0x2A1 },
|
|
|
|
|
{ 0x20B9, 0x20B9, 0x2A2 },
|
|
|
|
|
{ 0x2202, 0x2202, 0x2A3 },
|
|
|
|
|
{ 0x2206, 0x2206, 0x2A4 },
|
|
|
|
|
{ 0x220F, 0x220F, 0x2A5 },
|
|
|
|
|
{ 0x2211, 0x2212, 0x2A6 },
|
|
|
|
|
{ 0x2215, 0x2215, 0x2A8 },
|
|
|
|
|
{ 0x2219, 0x221A, 0x2A9 },
|
|
|
|
|
{ 0x221E, 0x221E, 0x2AB },
|
|
|
|
|
{ 0x222B, 0x222B, 0x2AC },
|
|
|
|
|
{ 0x2248, 0x2248, 0x2AD },
|
|
|
|
|
{ 0x2260, 0x2260, 0x2AE },
|
|
|
|
|
{ 0x2264, 0x2265, 0x2AF },
|
|
|
|
|
{ 0xFB00, 0xFB04, 0x2B1 },
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const EpdKernClassEntry ubuntu_12_regularKernLeftClasses[] = {
|
|
|
|
|
{ 0x0022, 1 }, // "
|
|
|
|
|
{ 0x0027, 1 }, // '
|
|
|
|
|
{ 0x0028, 2 }, // (
|
|
|
|
|
{ 0x0029, 3 }, // )
|
|
|
|
|
{ 0x002A, 4 }, // *
|
|
|
|
|
{ 0x002C, 5 }, // ,
|
|
|
|
|
{ 0x002D, 6 }, // -
|
|
|
|
|
{ 0x002E, 5 }, // .
|
|
|
|
|
{ 0x002F, 7 }, // /
|
|
|
|
|
{ 0x003A, 8 }, // :
|
|
|
|
|
{ 0x003B, 8 }, // ;
|
|
|
|
|
{ 0x0040, 9 }, // @
|
|
|
|
|
{ 0x0041, 10 }, // A
|
|
|
|
|
{ 0x0042, 11 }, // B
|
|
|
|
|
{ 0x0043, 12 }, // C
|
|
|
|
|
{ 0x0044, 13 }, // D
|
|
|
|
|
{ 0x0045, 14 }, // E
|
|
|
|
|
{ 0x0046, 15 }, // F
|
|
|
|
|
{ 0x0047, 16 }, // G
|
|
|
|
|
{ 0x004A, 17 }, // J
|
|
|
|
|
{ 0x004B, 18 }, // K
|
|
|
|
|
{ 0x004C, 19 }, // L
|
|
|
|
|
{ 0x004D, 20 }, // M
|
|
|
|
|
{ 0x004F, 21 }, // O
|
|
|
|
|
{ 0x0050, 22 }, // P
|
|
|
|
|
{ 0x0051, 23 }, // Q
|
|
|
|
|
{ 0x0052, 24 }, // R
|
|
|
|
|
{ 0x0054, 25 }, // T
|
|
|
|
|
{ 0x0055, 26 }, // U
|
|
|
|
|
{ 0x0056, 27 }, // V
|
|
|
|
|
{ 0x0057, 28 }, // W
|
|
|
|
|
{ 0x0058, 29 }, // X
|
|
|
|
|
{ 0x0059, 30 }, // Y
|
|
|
|
|
{ 0x005A, 31 }, // Z
|
|
|
|
|
{ 0x005B, 32 }, // [
|
|
|
|
|
{ 0x005D, 33 }, // ]
|
|
|
|
|
{ 0x0061, 34 }, // a
|
|
|
|
|
{ 0x0062, 35 }, // b
|
|
|
|
|
{ 0x0063, 36 }, // c
|
|
|
|
|
{ 0x0066, 37 }, // f
|
|
|
|
|
{ 0x0068, 38 }, // h
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x006A, 39 }, // j
|
|
|
|
|
{ 0x006B, 40 }, // k
|
|
|
|
|
{ 0x006D, 41 }, // m
|
|
|
|
|
{ 0x006E, 41 }, // n
|
|
|
|
|
{ 0x006F, 42 }, // o
|
|
|
|
|
{ 0x0070, 43 }, // p
|
|
|
|
|
{ 0x0071, 44 }, // q
|
|
|
|
|
{ 0x0072, 45 }, // r
|
|
|
|
|
{ 0x0074, 46 }, // t
|
|
|
|
|
{ 0x0076, 47 }, // v
|
|
|
|
|
{ 0x0077, 48 }, // w
|
|
|
|
|
{ 0x0078, 49 }, // x
|
|
|
|
|
{ 0x007A, 50 }, // z
|
|
|
|
|
{ 0x007B, 51 }, // {
|
|
|
|
|
{ 0x007D, 52 }, // }
|
|
|
|
|
{ 0x00A1, 53 }, // U+00A1
|
|
|
|
|
{ 0x00AB, 54 }, // U+00AB
|
|
|
|
|
{ 0x00BB, 55 }, // U+00BB
|
|
|
|
|
{ 0x00BF, 56 }, // U+00BF
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x00C0, 10 }, // U+00C0
|
|
|
|
|
{ 0x00C1, 10 }, // U+00C1
|
|
|
|
|
{ 0x00C2, 10 }, // U+00C2
|
|
|
|
|
{ 0x00C3, 10 }, // U+00C3
|
|
|
|
|
{ 0x00C4, 10 }, // U+00C4
|
|
|
|
|
{ 0x00C5, 10 }, // U+00C5
|
|
|
|
|
{ 0x00C6, 14 }, // U+00C6
|
|
|
|
|
{ 0x00C7, 12 }, // U+00C7
|
|
|
|
|
{ 0x00C8, 14 }, // U+00C8
|
|
|
|
|
{ 0x00C9, 14 }, // U+00C9
|
|
|
|
|
{ 0x00CA, 14 }, // U+00CA
|
|
|
|
|
{ 0x00CB, 14 }, // U+00CB
|
|
|
|
|
{ 0x00D0, 13 }, // U+00D0
|
|
|
|
|
{ 0x00D2, 21 }, // U+00D2
|
|
|
|
|
{ 0x00D3, 21 }, // U+00D3
|
|
|
|
|
{ 0x00D4, 21 }, // U+00D4
|
|
|
|
|
{ 0x00D5, 21 }, // U+00D5
|
|
|
|
|
{ 0x00D6, 21 }, // U+00D6
|
|
|
|
|
{ 0x00D8, 21 }, // U+00D8
|
|
|
|
|
{ 0x00D9, 26 }, // U+00D9
|
|
|
|
|
{ 0x00DA, 26 }, // U+00DA
|
|
|
|
|
{ 0x00DB, 26 }, // U+00DB
|
|
|
|
|
{ 0x00DC, 26 }, // U+00DC
|
|
|
|
|
{ 0x00DD, 30 }, // U+00DD
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x00DE, 57 }, // U+00DE
|
|
|
|
|
{ 0x00DF, 58 }, // U+00DF
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x00E0, 34 }, // U+00E0
|
|
|
|
|
{ 0x00E1, 34 }, // U+00E1
|
|
|
|
|
{ 0x00E2, 34 }, // U+00E2
|
|
|
|
|
{ 0x00E3, 34 }, // U+00E3
|
|
|
|
|
{ 0x00E4, 34 }, // U+00E4
|
|
|
|
|
{ 0x00E5, 34 }, // U+00E5
|
|
|
|
|
{ 0x00E7, 36 }, // U+00E7
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x00F0, 59 }, // U+00F0
|
|
|
|
|
{ 0x00F1, 41 }, // U+00F1
|
|
|
|
|
{ 0x00F2, 42 }, // U+00F2
|
|
|
|
|
{ 0x00F3, 42 }, // U+00F3
|
|
|
|
|
{ 0x00F4, 42 }, // U+00F4
|
|
|
|
|
{ 0x00F5, 42 }, // U+00F5
|
|
|
|
|
{ 0x00F6, 42 }, // U+00F6
|
|
|
|
|
{ 0x00F8, 42 }, // U+00F8
|
|
|
|
|
{ 0x00FE, 60 }, // U+00FE
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0100, 10 }, // U+0100
|
|
|
|
|
{ 0x0101, 34 }, // U+0101
|
|
|
|
|
{ 0x0102, 10 }, // U+0102
|
|
|
|
|
{ 0x0103, 34 }, // U+0103
|
|
|
|
|
{ 0x0104, 10 }, // U+0104
|
|
|
|
|
{ 0x0105, 34 }, // U+0105
|
|
|
|
|
{ 0x0106, 12 }, // U+0106
|
|
|
|
|
{ 0x0107, 36 }, // U+0107
|
|
|
|
|
{ 0x0108, 12 }, // U+0108
|
|
|
|
|
{ 0x0109, 36 }, // U+0109
|
|
|
|
|
{ 0x010A, 12 }, // U+010A
|
|
|
|
|
{ 0x010B, 36 }, // U+010B
|
|
|
|
|
{ 0x010C, 12 }, // U+010C
|
|
|
|
|
{ 0x010D, 36 }, // U+010D
|
|
|
|
|
{ 0x010E, 13 }, // U+010E
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x010F, 61 }, // U+010F
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0110, 13 }, // U+0110
|
|
|
|
|
{ 0x0112, 14 }, // U+0112
|
|
|
|
|
{ 0x0114, 14 }, // U+0114
|
|
|
|
|
{ 0x0116, 14 }, // U+0116
|
|
|
|
|
{ 0x0118, 14 }, // U+0118
|
|
|
|
|
{ 0x011A, 14 }, // U+011A
|
|
|
|
|
{ 0x011C, 16 }, // U+011C
|
|
|
|
|
{ 0x011E, 16 }, // U+011E
|
|
|
|
|
{ 0x0120, 16 }, // U+0120
|
|
|
|
|
{ 0x0122, 16 }, // U+0122
|
|
|
|
|
{ 0x0125, 38 }, // U+0125
|
|
|
|
|
{ 0x0127, 38 }, // U+0127
|
|
|
|
|
{ 0x0132, 17 }, // U+0132
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0133, 39 }, // U+0133
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0134, 17 }, // U+0134
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0135, 39 }, // U+0135
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0136, 18 }, // U+0136
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0137, 40 }, // U+0137
|
|
|
|
|
{ 0x0138, 40 }, // U+0138
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0139, 19 }, // U+0139
|
|
|
|
|
{ 0x013B, 19 }, // U+013B
|
|
|
|
|
{ 0x013D, 19 }, // U+013D
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x013E, 61 }, // U+013E
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x013F, 19 }, // U+013F
|
|
|
|
|
{ 0x0141, 19 }, // U+0141
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0144, 41 }, // U+0144
|
|
|
|
|
{ 0x0146, 41 }, // U+0146
|
|
|
|
|
{ 0x0148, 41 }, // U+0148
|
|
|
|
|
{ 0x0149, 41 }, // U+0149
|
|
|
|
|
{ 0x014A, 62 }, // U+014A
|
|
|
|
|
{ 0x014B, 63 }, // U+014B
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x014C, 21 }, // U+014C
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x014D, 42 }, // U+014D
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x014E, 21 }, // U+014E
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x014F, 42 }, // U+014F
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0150, 21 }, // U+0150
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0151, 42 }, // U+0151
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0152, 14 }, // U+0152
|
|
|
|
|
{ 0x0154, 24 }, // U+0154
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0155, 45 }, // U+0155
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0156, 24 }, // U+0156
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0157, 45 }, // U+0157
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0158, 24 }, // U+0158
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0159, 45 }, // U+0159
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0162, 25 }, // U+0162
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0163, 46 }, // U+0163
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0164, 25 }, // U+0164
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0165, 46 }, // U+0165
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0166, 25 }, // U+0166
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0167, 46 }, // U+0167
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0168, 26 }, // U+0168
|
|
|
|
|
{ 0x016A, 26 }, // U+016A
|
|
|
|
|
{ 0x016C, 26 }, // U+016C
|
|
|
|
|
{ 0x016E, 26 }, // U+016E
|
|
|
|
|
{ 0x0170, 26 }, // U+0170
|
|
|
|
|
{ 0x0172, 26 }, // U+0172
|
|
|
|
|
{ 0x0174, 28 }, // U+0174
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0175, 48 }, // U+0175
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0176, 30 }, // U+0176
|
|
|
|
|
{ 0x0178, 30 }, // U+0178
|
|
|
|
|
{ 0x0179, 31 }, // U+0179
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x017A, 50 }, // U+017A
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x017B, 31 }, // U+017B
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x017C, 50 }, // U+017C
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x017D, 31 }, // U+017D
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x017E, 50 }, // U+017E
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x017F, 37 }, // U+017F
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01C4, 31 }, // U+01C4
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01C5, 50 }, // U+01C5
|
|
|
|
|
{ 0x01C6, 50 }, // U+01C6
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01C7, 17 }, // U+01C7
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01C8, 39 }, // U+01C8
|
|
|
|
|
{ 0x01C9, 39 }, // U+01C9
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01CA, 17 }, // U+01CA
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01CB, 39 }, // U+01CB
|
|
|
|
|
{ 0x01CC, 39 }, // U+01CC
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01CD, 10 }, // U+01CD
|
|
|
|
|
{ 0x01CE, 34 }, // U+01CE
|
|
|
|
|
{ 0x01D1, 21 }, // U+01D1
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01D2, 42 }, // U+01D2
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01D3, 26 }, // U+01D3
|
|
|
|
|
{ 0x01D5, 26 }, // U+01D5
|
|
|
|
|
{ 0x01D7, 26 }, // U+01D7
|
|
|
|
|
{ 0x01D9, 26 }, // U+01D9
|
|
|
|
|
{ 0x01DB, 26 }, // U+01DB
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01DD, 42 }, // U+01DD
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01DE, 10 }, // U+01DE
|
|
|
|
|
{ 0x01DF, 34 }, // U+01DF
|
|
|
|
|
{ 0x01E0, 10 }, // U+01E0
|
|
|
|
|
{ 0x01E1, 34 }, // U+01E1
|
|
|
|
|
{ 0x01E2, 14 }, // U+01E2
|
|
|
|
|
{ 0x01E4, 16 }, // U+01E4
|
|
|
|
|
{ 0x01E6, 16 }, // U+01E6
|
|
|
|
|
{ 0x01E8, 18 }, // U+01E8
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01E9, 40 }, // U+01E9
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01EA, 21 }, // U+01EA
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01EB, 42 }, // U+01EB
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01EC, 21 }, // U+01EC
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01ED, 42 }, // U+01ED
|
|
|
|
|
{ 0x01EE, 64 }, // U+01EE
|
|
|
|
|
{ 0x01F0, 39 }, // U+01F0
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01F1, 31 }, // U+01F1
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01F2, 50 }, // U+01F2
|
|
|
|
|
{ 0x01F3, 50 }, // U+01F3
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01F4, 16 }, // U+01F4
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01F7, 65 }, // U+01F7
|
|
|
|
|
{ 0x01F9, 41 }, // U+01F9
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01FA, 10 }, // U+01FA
|
|
|
|
|
{ 0x01FB, 34 }, // U+01FB
|
|
|
|
|
{ 0x01FC, 14 }, // U+01FC
|
|
|
|
|
{ 0x01FE, 21 }, // U+01FE
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01FF, 42 }, // U+01FF
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x0200, 10 }, // U+0200
|
|
|
|
|
{ 0x0201, 34 }, // U+0201
|
|
|
|
|
{ 0x0202, 10 }, // U+0202
|
|
|
|
|
{ 0x0203, 34 }, // U+0203
|
|
|
|
|
{ 0x0204, 14 }, // U+0204
|
|
|
|
|
{ 0x0206, 14 }, // U+0206
|
|
|
|
|
{ 0x020C, 21 }, // U+020C
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x020D, 42 }, // U+020D
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x020E, 21 }, // U+020E
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x020F, 42 }, // U+020F
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x0210, 24 }, // U+0210
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0211, 45 }, // U+0211
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x0212, 24 }, // U+0212
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0213, 45 }, // U+0213
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x0214, 26 }, // U+0214
|
|
|
|
|
{ 0x0216, 26 }, // U+0216
|
|
|
|
|
{ 0x021A, 25 }, // U+021A
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x021B, 46 }, // U+021B
|
|
|
|
|
{ 0x021C, 66 }, // U+021C
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x021F, 38 }, // U+021F
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0400, 67 }, // U+0400
|
|
|
|
|
{ 0x0401, 67 }, // U+0401
|
|
|
|
|
{ 0x0402, 68 }, // U+0402
|
|
|
|
|
{ 0x0403, 69 }, // U+0403
|
|
|
|
|
{ 0x0404, 70 }, // U+0404
|
|
|
|
|
{ 0x0405, 71 }, // U+0405
|
|
|
|
|
{ 0x040B, 72 }, // U+040B
|
|
|
|
|
{ 0x040C, 73 }, // U+040C
|
|
|
|
|
{ 0x040E, 74 }, // U+040E
|
|
|
|
|
{ 0x0410, 75 }, // U+0410
|
|
|
|
|
{ 0x0411, 68 }, // U+0411
|
|
|
|
|
{ 0x0412, 76 }, // U+0412
|
|
|
|
|
{ 0x0413, 69 }, // U+0413
|
|
|
|
|
{ 0x0414, 77 }, // U+0414
|
|
|
|
|
{ 0x0415, 67 }, // U+0415
|
|
|
|
|
{ 0x0417, 78 }, // U+0417
|
|
|
|
|
{ 0x041A, 73 }, // U+041A
|
|
|
|
|
{ 0x041C, 79 }, // U+041C
|
|
|
|
|
{ 0x041E, 80 }, // U+041E
|
|
|
|
|
{ 0x0420, 81 }, // U+0420
|
|
|
|
|
{ 0x0421, 70 }, // U+0421
|
|
|
|
|
{ 0x0422, 82 }, // U+0422
|
|
|
|
|
{ 0x0423, 74 }, // U+0423
|
|
|
|
|
{ 0x0424, 83 }, // U+0424
|
|
|
|
|
{ 0x0425, 84 }, // U+0425
|
|
|
|
|
{ 0x0426, 85 }, // U+0426
|
|
|
|
|
{ 0x0429, 85 }, // U+0429
|
|
|
|
|
{ 0x042D, 86 }, // U+042D
|
|
|
|
|
{ 0x042E, 80 }, // U+042E
|
|
|
|
|
{ 0x0430, 87 }, // U+0430
|
|
|
|
|
{ 0x0431, 88 }, // U+0431
|
|
|
|
|
{ 0x0432, 89 }, // U+0432
|
|
|
|
|
{ 0x0433, 90 }, // U+0433
|
|
|
|
|
{ 0x0434, 91 }, // U+0434
|
|
|
|
|
{ 0x0435, 92 }, // U+0435
|
|
|
|
|
{ 0x0436, 93 }, // U+0436
|
|
|
|
|
{ 0x0437, 94 }, // U+0437
|
|
|
|
|
{ 0x043A, 95 }, // U+043A
|
|
|
|
|
{ 0x043E, 96 }, // U+043E
|
|
|
|
|
{ 0x0440, 97 }, // U+0440
|
|
|
|
|
{ 0x0441, 98 }, // U+0441
|
|
|
|
|
{ 0x0442, 99 }, // U+0442
|
|
|
|
|
{ 0x0443, 100 }, // U+0443
|
|
|
|
|
{ 0x0444, 101 }, // U+0444
|
|
|
|
|
{ 0x0445, 102 }, // U+0445
|
|
|
|
|
{ 0x0446, 103 }, // U+0446
|
|
|
|
|
{ 0x0449, 103 }, // U+0449
|
|
|
|
|
{ 0x044A, 104 }, // U+044A
|
|
|
|
|
{ 0x044C, 104 }, // U+044C
|
|
|
|
|
{ 0x044D, 105 }, // U+044D
|
|
|
|
|
{ 0x044E, 106 }, // U+044E
|
|
|
|
|
{ 0x0450, 92 }, // U+0450
|
|
|
|
|
{ 0x0451, 92 }, // U+0451
|
|
|
|
|
{ 0x0452, 107 }, // U+0452
|
|
|
|
|
{ 0x0453, 90 }, // U+0453
|
|
|
|
|
{ 0x0454, 108 }, // U+0454
|
|
|
|
|
{ 0x0455, 109 }, // U+0455
|
|
|
|
|
{ 0x0459, 104 }, // U+0459
|
|
|
|
|
{ 0x045A, 104 }, // U+045A
|
|
|
|
|
{ 0x045B, 110 }, // U+045B
|
|
|
|
|
{ 0x045C, 95 }, // U+045C
|
|
|
|
|
{ 0x045E, 100 }, // U+045E
|
|
|
|
|
{ 0x0462, 68 }, // U+0462
|
|
|
|
|
{ 0x0463, 111 }, // U+0463
|
|
|
|
|
{ 0x0472, 80 }, // U+0472
|
|
|
|
|
{ 0x0473, 96 }, // U+0473
|
|
|
|
|
{ 0x0474, 112 }, // U+0474
|
|
|
|
|
{ 0x0475, 113 }, // U+0475
|
|
|
|
|
{ 0x048A, 85 }, // U+048A
|
|
|
|
|
{ 0x048B, 103 }, // U+048B
|
|
|
|
|
{ 0x048D, 104 }, // U+048D
|
|
|
|
|
{ 0x048F, 97 }, // U+048F
|
|
|
|
|
{ 0x0490, 69 }, // U+0490
|
|
|
|
|
{ 0x0491, 90 }, // U+0491
|
|
|
|
|
{ 0x0492, 69 }, // U+0492
|
|
|
|
|
{ 0x0493, 90 }, // U+0493
|
|
|
|
|
{ 0x0494, 68 }, // U+0494
|
|
|
|
|
{ 0x0495, 114 }, // U+0495
|
|
|
|
|
{ 0x0497, 93 }, // U+0497
|
|
|
|
|
{ 0x0498, 78 }, // U+0498
|
|
|
|
|
{ 0x0499, 94 }, // U+0499
|
|
|
|
|
{ 0x049A, 73 }, // U+049A
|
|
|
|
|
{ 0x049B, 95 }, // U+049B
|
|
|
|
|
{ 0x049C, 73 }, // U+049C
|
|
|
|
|
{ 0x049D, 95 }, // U+049D
|
|
|
|
|
{ 0x049E, 73 }, // U+049E
|
|
|
|
|
{ 0x049F, 95 }, // U+049F
|
|
|
|
|
{ 0x04A0, 73 }, // U+04A0
|
|
|
|
|
{ 0x04A1, 95 }, // U+04A1
|
|
|
|
|
{ 0x04A2, 85 }, // U+04A2
|
|
|
|
|
{ 0x04A3, 103 }, // U+04A3
|
|
|
|
|
{ 0x04A4, 82 }, // U+04A4
|
|
|
|
|
{ 0x04A5, 90 }, // U+04A5
|
|
|
|
|
{ 0x04A7, 115 }, // U+04A7
|
|
|
|
|
{ 0x04A8, 116 }, // U+04A8
|
|
|
|
|
{ 0x04A9, 117 }, // U+04A9
|
|
|
|
|
{ 0x04AA, 70 }, // U+04AA
|
|
|
|
|
{ 0x04AB, 98 }, // U+04AB
|
|
|
|
|
{ 0x04AC, 82 }, // U+04AC
|
|
|
|
|
{ 0x04AD, 99 }, // U+04AD
|
|
|
|
|
{ 0x04AE, 118 }, // U+04AE
|
|
|
|
|
{ 0x04AF, 119 }, // U+04AF
|
|
|
|
|
{ 0x04B0, 118 }, // U+04B0
|
|
|
|
|
{ 0x04B1, 119 }, // U+04B1
|
|
|
|
|
{ 0x04B2, 84 }, // U+04B2
|
|
|
|
|
{ 0x04B3, 102 }, // U+04B3
|
|
|
|
|
{ 0x04B4, 85 }, // U+04B4
|
|
|
|
|
{ 0x04B5, 103 }, // U+04B5
|
|
|
|
|
{ 0x04B6, 85 }, // U+04B6
|
|
|
|
|
{ 0x04B7, 103 }, // U+04B7
|
|
|
|
|
{ 0x04BA, 120 }, // U+04BA
|
|
|
|
|
{ 0x04BB, 121 }, // U+04BB
|
|
|
|
|
{ 0x04BC, 122 }, // U+04BC
|
|
|
|
|
{ 0x04BD, 123 }, // U+04BD
|
|
|
|
|
{ 0x04BE, 122 }, // U+04BE
|
|
|
|
|
{ 0x04BF, 123 }, // U+04BF
|
|
|
|
|
{ 0x04C2, 93 }, // U+04C2
|
|
|
|
|
{ 0x04C4, 124 }, // U+04C4
|
|
|
|
|
{ 0x04C6, 103 }, // U+04C6
|
|
|
|
|
{ 0x04C8, 125 }, // U+04C8
|
|
|
|
|
{ 0x04C9, 85 }, // U+04C9
|
|
|
|
|
{ 0x04CA, 103 }, // U+04CA
|
|
|
|
|
{ 0x04CD, 85 }, // U+04CD
|
|
|
|
|
{ 0x04CE, 103 }, // U+04CE
|
|
|
|
|
{ 0x04D0, 75 }, // U+04D0
|
|
|
|
|
{ 0x04D1, 87 }, // U+04D1
|
|
|
|
|
{ 0x04D2, 75 }, // U+04D2
|
|
|
|
|
{ 0x04D3, 87 }, // U+04D3
|
|
|
|
|
{ 0x04D5, 126 }, // U+04D5
|
|
|
|
|
{ 0x04D6, 67 }, // U+04D6
|
|
|
|
|
{ 0x04D7, 92 }, // U+04D7
|
|
|
|
|
{ 0x04D8, 127 }, // U+04D8
|
|
|
|
|
{ 0x04D9, 128 }, // U+04D9
|
|
|
|
|
{ 0x04DA, 127 }, // U+04DA
|
|
|
|
|
{ 0x04DB, 128 }, // U+04DB
|
|
|
|
|
{ 0x04DD, 93 }, // U+04DD
|
|
|
|
|
{ 0x04DE, 78 }, // U+04DE
|
|
|
|
|
{ 0x04DF, 94 }, // U+04DF
|
|
|
|
|
{ 0x04E0, 129 }, // U+04E0
|
|
|
|
|
{ 0x04E1, 130 }, // U+04E1
|
|
|
|
|
{ 0x04E6, 80 }, // U+04E6
|
|
|
|
|
{ 0x04E7, 96 }, // U+04E7
|
|
|
|
|
{ 0x04E8, 80 }, // U+04E8
|
|
|
|
|
{ 0x04E9, 96 }, // U+04E9
|
|
|
|
|
{ 0x04EA, 80 }, // U+04EA
|
|
|
|
|
{ 0x04EB, 96 }, // U+04EB
|
|
|
|
|
{ 0x04EC, 86 }, // U+04EC
|
|
|
|
|
{ 0x04ED, 94 }, // U+04ED
|
|
|
|
|
{ 0x04EE, 74 }, // U+04EE
|
|
|
|
|
{ 0x04EF, 100 }, // U+04EF
|
|
|
|
|
{ 0x04F0, 74 }, // U+04F0
|
|
|
|
|
{ 0x04F1, 100 }, // U+04F1
|
|
|
|
|
{ 0x04F2, 74 }, // U+04F2
|
|
|
|
|
{ 0x04F3, 100 }, // U+04F3
|
|
|
|
|
{ 0x04F6, 69 }, // U+04F6
|
|
|
|
|
{ 0x04F7, 90 }, // U+04F7
|
feat: Vietnamese glyphs support (#1147)
## Summary
* **What is the goal of this PR?**
Add Vietnamese glyphs support for the reader's built-in fonts, enabling
proper rendering of Vietnamese text in EPUB content.
* **What changes are included?**
- Added 3 new Unicode intervals to `fontconvert.py` covering Vietnamese
characters:
- **Latin Extended-B** (Vietnamese subset only): `U+01A0–U+01B0` — Ơ/ơ,
Ư/ư
- **Vietnamese Extended**: `U+1EA0–U+1EF9` — All precomposed Vietnamese
characters with tone marks (Ả, Ấ, Ầ, Ẩ, Ẫ, Ậ, Ắ, …, Ỹ)
- Re-generated all 54 built-in font header files (Bookerly, Noto Sans,
OpenDyslexic, Ubuntu across all sizes and styles) to include the new
Vietnamese glyphs.
## Additional Context
* **Scope**: This PR only covers the **reader** fonts. The outer UI
still uses the Ubuntu font which does not fully support Vietnamese — UI
and i18n will be addressed in a follow-up PR (per discussion in PR
#1124).
* **Memory impact**:
| Metric | Before | After | Delta |
|---|---|---|---|
| Flash Data (`.rodata`) | 2,971,028 B | 3,290,748 B | **+319,720 B
(+10.8%)** |
| Total image size | 4,663,235 B | 4,982,955 B | **+319,720 B (+6.9%)**
|
| Flash usage | 69.1% | 74.0% | **+4.9 pp** |
| RAM usage | 29.0% | 29.0% | **No change** |
* **Risk**: Low — this is a data-only change (font glyph tables in
`.rodata`). No logic changes, no RAM impact. Flash headroom remains
comfortable at 74%.
---
### AI Usage
Did you use AI tools to help write this code? _**PARTIALLY**_
AI was used to identify the minimal set of Unicode ranges needed for
Vietnamese support and to assist with the PR description.
---------
Co-authored-by: danoooob <danoooob@example.com>
2026-02-25 00:21:39 +07:00
|
|
|
{ 0x1EF2, 30 }, // U+1EF2
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x2013, 6 }, // U+2013
|
|
|
|
|
{ 0x2014, 6 }, // U+2014
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x2018, 131 }, // U+2018
|
|
|
|
|
{ 0x201A, 132 }, // U+201A
|
|
|
|
|
{ 0x201C, 131 }, // U+201C
|
|
|
|
|
{ 0x201E, 132 }, // U+201E
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x2026, 5 }, // U+2026
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x2039, 54 }, // U+2039
|
|
|
|
|
{ 0x203A, 55 }, // U+203A
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0xFB00, 37 }, // U+FB00
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const EpdKernClassEntry ubuntu_12_regularKernRightClasses[] = {
|
|
|
|
|
{ 0x0021, 1 }, // !
|
|
|
|
|
{ 0x0022, 2 }, // "
|
|
|
|
|
{ 0x0027, 2 }, // '
|
|
|
|
|
{ 0x0028, 3 }, // (
|
|
|
|
|
{ 0x0029, 4 }, // )
|
|
|
|
|
{ 0x002A, 5 }, // *
|
|
|
|
|
{ 0x002C, 6 }, // ,
|
|
|
|
|
{ 0x002D, 7 }, // -
|
|
|
|
|
{ 0x002E, 6 }, // .
|
|
|
|
|
{ 0x002F, 8 }, // /
|
|
|
|
|
{ 0x003A, 9 }, // :
|
|
|
|
|
{ 0x003B, 9 }, // ;
|
|
|
|
|
{ 0x003F, 10 }, // ?
|
|
|
|
|
{ 0x0040, 11 }, // @
|
|
|
|
|
{ 0x0041, 12 }, // A
|
|
|
|
|
{ 0x0043, 13 }, // C
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0047, 14 }, // G
|
|
|
|
|
{ 0x004A, 15 }, // J
|
|
|
|
|
{ 0x004D, 16 }, // M
|
|
|
|
|
{ 0x004F, 17 }, // O
|
|
|
|
|
{ 0x0051, 18 }, // Q
|
|
|
|
|
{ 0x0053, 19 }, // S
|
|
|
|
|
{ 0x0054, 20 }, // T
|
|
|
|
|
{ 0x0055, 21 }, // U
|
|
|
|
|
{ 0x0056, 22 }, // V
|
|
|
|
|
{ 0x0057, 23 }, // W
|
|
|
|
|
{ 0x0058, 24 }, // X
|
|
|
|
|
{ 0x0059, 25 }, // Y
|
|
|
|
|
{ 0x005A, 26 }, // Z
|
|
|
|
|
{ 0x005B, 27 }, // [
|
|
|
|
|
{ 0x005C, 28 }, // <backslash>
|
|
|
|
|
{ 0x005D, 29 }, // ]
|
|
|
|
|
{ 0x0061, 30 }, // a
|
|
|
|
|
{ 0x0062, 31 }, // b
|
|
|
|
|
{ 0x0063, 32 }, // c
|
|
|
|
|
{ 0x0064, 33 }, // d
|
|
|
|
|
{ 0x0065, 34 }, // e
|
|
|
|
|
{ 0x0066, 35 }, // f
|
|
|
|
|
{ 0x0067, 36 }, // g
|
|
|
|
|
{ 0x0068, 31 }, // h
|
|
|
|
|
{ 0x0069, 37 }, // i
|
|
|
|
|
{ 0x006A, 38 }, // j
|
|
|
|
|
{ 0x006B, 31 }, // k
|
|
|
|
|
{ 0x006C, 39 }, // l
|
|
|
|
|
{ 0x006D, 40 }, // m
|
|
|
|
|
{ 0x006E, 40 }, // n
|
|
|
|
|
{ 0x006F, 41 }, // o
|
|
|
|
|
{ 0x0070, 40 }, // p
|
|
|
|
|
{ 0x0071, 42 }, // q
|
|
|
|
|
{ 0x0072, 40 }, // r
|
|
|
|
|
{ 0x0073, 43 }, // s
|
|
|
|
|
{ 0x0074, 44 }, // t
|
|
|
|
|
{ 0x0075, 45 }, // u
|
|
|
|
|
{ 0x0076, 46 }, // v
|
|
|
|
|
{ 0x0077, 47 }, // w
|
|
|
|
|
{ 0x0078, 48 }, // x
|
|
|
|
|
{ 0x0079, 49 }, // y
|
|
|
|
|
{ 0x007A, 50 }, // z
|
|
|
|
|
{ 0x007B, 51 }, // {
|
|
|
|
|
{ 0x007D, 52 }, // }
|
|
|
|
|
{ 0x00AB, 53 }, // U+00AB
|
|
|
|
|
{ 0x00BB, 54 }, // U+00BB
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x00C0, 12 }, // U+00C0
|
|
|
|
|
{ 0x00C1, 12 }, // U+00C1
|
|
|
|
|
{ 0x00C2, 12 }, // U+00C2
|
|
|
|
|
{ 0x00C3, 12 }, // U+00C3
|
|
|
|
|
{ 0x00C4, 12 }, // U+00C4
|
|
|
|
|
{ 0x00C5, 12 }, // U+00C5
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x00C6, 55 }, // U+00C6
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x00C7, 13 }, // U+00C7
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x00D2, 17 }, // U+00D2
|
|
|
|
|
{ 0x00D3, 17 }, // U+00D3
|
|
|
|
|
{ 0x00D4, 17 }, // U+00D4
|
|
|
|
|
{ 0x00D5, 17 }, // U+00D5
|
|
|
|
|
{ 0x00D6, 17 }, // U+00D6
|
|
|
|
|
{ 0x00D8, 17 }, // U+00D8
|
|
|
|
|
{ 0x00D9, 21 }, // U+00D9
|
|
|
|
|
{ 0x00DA, 21 }, // U+00DA
|
|
|
|
|
{ 0x00DB, 21 }, // U+00DB
|
|
|
|
|
{ 0x00DC, 21 }, // U+00DC
|
|
|
|
|
{ 0x00DD, 25 }, // U+00DD
|
|
|
|
|
{ 0x00DF, 56 }, // U+00DF
|
|
|
|
|
{ 0x00E0, 30 }, // U+00E0
|
|
|
|
|
{ 0x00E1, 30 }, // U+00E1
|
|
|
|
|
{ 0x00E2, 30 }, // U+00E2
|
|
|
|
|
{ 0x00E3, 30 }, // U+00E3
|
|
|
|
|
{ 0x00E4, 30 }, // U+00E4
|
|
|
|
|
{ 0x00E5, 30 }, // U+00E5
|
|
|
|
|
{ 0x00E6, 30 }, // U+00E6
|
|
|
|
|
{ 0x00E7, 32 }, // U+00E7
|
|
|
|
|
{ 0x00E8, 34 }, // U+00E8
|
|
|
|
|
{ 0x00E9, 34 }, // U+00E9
|
|
|
|
|
{ 0x00EA, 34 }, // U+00EA
|
|
|
|
|
{ 0x00EB, 34 }, // U+00EB
|
|
|
|
|
{ 0x00EC, 57 }, // U+00EC
|
|
|
|
|
{ 0x00ED, 37 }, // U+00ED
|
|
|
|
|
{ 0x00EE, 58 }, // U+00EE
|
|
|
|
|
{ 0x00EF, 59 }, // U+00EF
|
|
|
|
|
{ 0x00F0, 60 }, // U+00F0
|
|
|
|
|
{ 0x00F1, 40 }, // U+00F1
|
|
|
|
|
{ 0x00F2, 41 }, // U+00F2
|
|
|
|
|
{ 0x00F3, 41 }, // U+00F3
|
|
|
|
|
{ 0x00F4, 41 }, // U+00F4
|
|
|
|
|
{ 0x00F5, 41 }, // U+00F5
|
|
|
|
|
{ 0x00F6, 41 }, // U+00F6
|
|
|
|
|
{ 0x00F8, 41 }, // U+00F8
|
|
|
|
|
{ 0x00F9, 45 }, // U+00F9
|
|
|
|
|
{ 0x00FA, 45 }, // U+00FA
|
|
|
|
|
{ 0x00FB, 45 }, // U+00FB
|
|
|
|
|
{ 0x00FC, 45 }, // U+00FC
|
|
|
|
|
{ 0x00FD, 49 }, // U+00FD
|
|
|
|
|
{ 0x00FE, 56 }, // U+00FE
|
|
|
|
|
{ 0x00FF, 49 }, // U+00FF
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0100, 12 }, // U+0100
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0101, 30 }, // U+0101
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0102, 12 }, // U+0102
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0103, 30 }, // U+0103
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0104, 12 }, // U+0104
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0105, 30 }, // U+0105
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0106, 13 }, // U+0106
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0107, 32 }, // U+0107
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x0108, 13 }, // U+0108
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0109, 32 }, // U+0109
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x010A, 13 }, // U+010A
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x010B, 32 }, // U+010B
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x010C, 13 }, // U+010C
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x010D, 32 }, // U+010D
|
|
|
|
|
{ 0x010F, 33 }, // U+010F
|
|
|
|
|
{ 0x0111, 33 }, // U+0111
|
|
|
|
|
{ 0x0113, 34 }, // U+0113
|
|
|
|
|
{ 0x0115, 34 }, // U+0115
|
|
|
|
|
{ 0x0117, 34 }, // U+0117
|
|
|
|
|
{ 0x0119, 34 }, // U+0119
|
|
|
|
|
{ 0x011B, 34 }, // U+011B
|
|
|
|
|
{ 0x011C, 14 }, // U+011C
|
|
|
|
|
{ 0x011D, 36 }, // U+011D
|
|
|
|
|
{ 0x011E, 14 }, // U+011E
|
|
|
|
|
{ 0x011F, 36 }, // U+011F
|
|
|
|
|
{ 0x0120, 14 }, // U+0120
|
|
|
|
|
{ 0x0121, 36 }, // U+0121
|
|
|
|
|
{ 0x0122, 14 }, // U+0122
|
|
|
|
|
{ 0x0123, 36 }, // U+0123
|
|
|
|
|
{ 0x0125, 31 }, // U+0125
|
|
|
|
|
{ 0x0127, 31 }, // U+0127
|
|
|
|
|
{ 0x0129, 61 }, // U+0129
|
|
|
|
|
{ 0x012B, 62 }, // U+012B
|
|
|
|
|
{ 0x012D, 63 }, // U+012D
|
|
|
|
|
{ 0x012F, 37 }, // U+012F
|
|
|
|
|
{ 0x0131, 37 }, // U+0131
|
|
|
|
|
{ 0x0133, 37 }, // U+0133
|
|
|
|
|
{ 0x0134, 15 }, // U+0134
|
|
|
|
|
{ 0x0135, 38 }, // U+0135
|
|
|
|
|
{ 0x0137, 31 }, // U+0137
|
|
|
|
|
{ 0x013A, 39 }, // U+013A
|
|
|
|
|
{ 0x013C, 39 }, // U+013C
|
|
|
|
|
{ 0x013E, 39 }, // U+013E
|
|
|
|
|
{ 0x0140, 39 }, // U+0140
|
|
|
|
|
{ 0x0142, 39 }, // U+0142
|
|
|
|
|
{ 0x0144, 40 }, // U+0144
|
|
|
|
|
{ 0x0146, 40 }, // U+0146
|
|
|
|
|
{ 0x0148, 40 }, // U+0148
|
|
|
|
|
{ 0x0149, 40 }, // U+0149
|
|
|
|
|
{ 0x014A, 64 }, // U+014A
|
|
|
|
|
{ 0x014B, 65 }, // U+014B
|
|
|
|
|
{ 0x014C, 17 }, // U+014C
|
|
|
|
|
{ 0x014D, 41 }, // U+014D
|
|
|
|
|
{ 0x014E, 17 }, // U+014E
|
|
|
|
|
{ 0x014F, 41 }, // U+014F
|
|
|
|
|
{ 0x0150, 17 }, // U+0150
|
|
|
|
|
{ 0x0151, 41 }, // U+0151
|
|
|
|
|
{ 0x0152, 17 }, // U+0152
|
|
|
|
|
{ 0x0153, 41 }, // U+0153
|
|
|
|
|
{ 0x0155, 40 }, // U+0155
|
|
|
|
|
{ 0x0157, 40 }, // U+0157
|
|
|
|
|
{ 0x0159, 40 }, // U+0159
|
|
|
|
|
{ 0x015A, 19 }, // U+015A
|
|
|
|
|
{ 0x015B, 43 }, // U+015B
|
|
|
|
|
{ 0x015C, 19 }, // U+015C
|
|
|
|
|
{ 0x015D, 43 }, // U+015D
|
|
|
|
|
{ 0x015E, 19 }, // U+015E
|
|
|
|
|
{ 0x015F, 43 }, // U+015F
|
|
|
|
|
{ 0x0160, 19 }, // U+0160
|
|
|
|
|
{ 0x0161, 43 }, // U+0161
|
|
|
|
|
{ 0x0162, 20 }, // U+0162
|
|
|
|
|
{ 0x0163, 44 }, // U+0163
|
|
|
|
|
{ 0x0164, 20 }, // U+0164
|
|
|
|
|
{ 0x0165, 44 }, // U+0165
|
|
|
|
|
{ 0x0166, 20 }, // U+0166
|
|
|
|
|
{ 0x0167, 44 }, // U+0167
|
|
|
|
|
{ 0x0168, 21 }, // U+0168
|
|
|
|
|
{ 0x0169, 45 }, // U+0169
|
|
|
|
|
{ 0x016A, 21 }, // U+016A
|
|
|
|
|
{ 0x016B, 45 }, // U+016B
|
|
|
|
|
{ 0x016C, 21 }, // U+016C
|
|
|
|
|
{ 0x016D, 45 }, // U+016D
|
|
|
|
|
{ 0x016E, 21 }, // U+016E
|
|
|
|
|
{ 0x016F, 45 }, // U+016F
|
|
|
|
|
{ 0x0170, 21 }, // U+0170
|
|
|
|
|
{ 0x0171, 45 }, // U+0171
|
|
|
|
|
{ 0x0172, 21 }, // U+0172
|
|
|
|
|
{ 0x0173, 45 }, // U+0173
|
|
|
|
|
{ 0x0174, 23 }, // U+0174
|
|
|
|
|
{ 0x0175, 47 }, // U+0175
|
|
|
|
|
{ 0x0176, 25 }, // U+0176
|
|
|
|
|
{ 0x0177, 49 }, // U+0177
|
|
|
|
|
{ 0x0178, 25 }, // U+0178
|
|
|
|
|
{ 0x0179, 26 }, // U+0179
|
|
|
|
|
{ 0x017A, 50 }, // U+017A
|
|
|
|
|
{ 0x017B, 26 }, // U+017B
|
|
|
|
|
{ 0x017C, 50 }, // U+017C
|
|
|
|
|
{ 0x017D, 26 }, // U+017D
|
|
|
|
|
{ 0x017E, 50 }, // U+017E
|
|
|
|
|
{ 0x017F, 35 }, // U+017F
|
|
|
|
|
{ 0x01A0, 17 }, // U+01A0
|
|
|
|
|
{ 0x01A1, 41 }, // U+01A1
|
|
|
|
|
{ 0x01AF, 21 }, // U+01AF
|
|
|
|
|
{ 0x01B0, 45 }, // U+01B0
|
|
|
|
|
{ 0x01C6, 33 }, // U+01C6
|
|
|
|
|
{ 0x01C9, 39 }, // U+01C9
|
|
|
|
|
{ 0x01CC, 40 }, // U+01CC
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01CD, 12 }, // U+01CD
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01CE, 30 }, // U+01CE
|
|
|
|
|
{ 0x01D0, 37 }, // U+01D0
|
|
|
|
|
{ 0x01D1, 17 }, // U+01D1
|
|
|
|
|
{ 0x01D2, 41 }, // U+01D2
|
|
|
|
|
{ 0x01D3, 21 }, // U+01D3
|
|
|
|
|
{ 0x01D4, 45 }, // U+01D4
|
|
|
|
|
{ 0x01D5, 21 }, // U+01D5
|
|
|
|
|
{ 0x01D6, 45 }, // U+01D6
|
|
|
|
|
{ 0x01D7, 21 }, // U+01D7
|
|
|
|
|
{ 0x01D8, 45 }, // U+01D8
|
|
|
|
|
{ 0x01D9, 21 }, // U+01D9
|
|
|
|
|
{ 0x01DA, 45 }, // U+01DA
|
|
|
|
|
{ 0x01DB, 21 }, // U+01DB
|
|
|
|
|
{ 0x01DC, 45 }, // U+01DC
|
|
|
|
|
{ 0x01DD, 66 }, // U+01DD
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01DE, 12 }, // U+01DE
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01DF, 30 }, // U+01DF
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01E0, 12 }, // U+01E0
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01E1, 30 }, // U+01E1
|
|
|
|
|
{ 0x01E2, 55 }, // U+01E2
|
|
|
|
|
{ 0x01E3, 30 }, // U+01E3
|
|
|
|
|
{ 0x01E4, 14 }, // U+01E4
|
|
|
|
|
{ 0x01E5, 36 }, // U+01E5
|
|
|
|
|
{ 0x01E6, 14 }, // U+01E6
|
|
|
|
|
{ 0x01E7, 36 }, // U+01E7
|
|
|
|
|
{ 0x01E9, 31 }, // U+01E9
|
|
|
|
|
{ 0x01EA, 17 }, // U+01EA
|
|
|
|
|
{ 0x01EB, 41 }, // U+01EB
|
|
|
|
|
{ 0x01EC, 17 }, // U+01EC
|
|
|
|
|
{ 0x01ED, 41 }, // U+01ED
|
|
|
|
|
{ 0x01EE, 67 }, // U+01EE
|
|
|
|
|
{ 0x01EF, 68 }, // U+01EF
|
|
|
|
|
{ 0x01F0, 38 }, // U+01F0
|
|
|
|
|
{ 0x01F3, 33 }, // U+01F3
|
|
|
|
|
{ 0x01F4, 14 }, // U+01F4
|
|
|
|
|
{ 0x01F5, 36 }, // U+01F5
|
|
|
|
|
{ 0x01F7, 56 }, // U+01F7
|
|
|
|
|
{ 0x01F9, 40 }, // U+01F9
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x01FA, 12 }, // U+01FA
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x01FB, 30 }, // U+01FB
|
|
|
|
|
{ 0x01FC, 55 }, // U+01FC
|
|
|
|
|
{ 0x01FD, 30 }, // U+01FD
|
|
|
|
|
{ 0x01FE, 17 }, // U+01FE
|
|
|
|
|
{ 0x01FF, 41 }, // U+01FF
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x0200, 12 }, // U+0200
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0201, 30 }, // U+0201
|
2026-02-25 03:56:11 -06:00
|
|
|
{ 0x0202, 12 }, // U+0202
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x0203, 30 }, // U+0203
|
|
|
|
|
{ 0x0205, 34 }, // U+0205
|
|
|
|
|
{ 0x0207, 34 }, // U+0207
|
|
|
|
|
{ 0x0209, 37 }, // U+0209
|
|
|
|
|
{ 0x020B, 37 }, // U+020B
|
|
|
|
|
{ 0x020C, 17 }, // U+020C
|
|
|
|
|
{ 0x020D, 41 }, // U+020D
|
|
|
|
|
{ 0x020E, 17 }, // U+020E
|
|
|
|
|
{ 0x020F, 41 }, // U+020F
|
|
|
|
|
{ 0x0211, 40 }, // U+0211
|
|
|
|
|
{ 0x0213, 40 }, // U+0213
|
|
|
|
|
{ 0x0214, 21 }, // U+0214
|
|
|
|
|
{ 0x0215, 45 }, // U+0215
|
|
|
|
|
{ 0x0216, 21 }, // U+0216
|
|
|
|
|
{ 0x0217, 45 }, // U+0217
|
|
|
|
|
{ 0x0218, 19 }, // U+0218
|
|
|
|
|
{ 0x0219, 43 }, // U+0219
|
|
|
|
|
{ 0x021A, 20 }, // U+021A
|
|
|
|
|
{ 0x021B, 44 }, // U+021B
|
|
|
|
|
{ 0x021C, 69 }, // U+021C
|
|
|
|
|
{ 0x021D, 70 }, // U+021D
|
|
|
|
|
{ 0x021F, 31 }, // U+021F
|
|
|
|
|
{ 0x0402, 71 }, // U+0402
|
|
|
|
|
{ 0x0404, 72 }, // U+0404
|
|
|
|
|
{ 0x0405, 73 }, // U+0405
|
|
|
|
|
{ 0x0408, 74 }, // U+0408
|
|
|
|
|
{ 0x0409, 75 }, // U+0409
|
|
|
|
|
{ 0x040B, 71 }, // U+040B
|
|
|
|
|
{ 0x040E, 76 }, // U+040E
|
|
|
|
|
{ 0x0410, 77 }, // U+0410
|
|
|
|
|
{ 0x0414, 78 }, // U+0414
|
|
|
|
|
{ 0x0416, 79 }, // U+0416
|
|
|
|
|
{ 0x0417, 80 }, // U+0417
|
|
|
|
|
{ 0x041B, 75 }, // U+041B
|
|
|
|
|
{ 0x041C, 81 }, // U+041C
|
|
|
|
|
{ 0x041E, 82 }, // U+041E
|
|
|
|
|
{ 0x0421, 72 }, // U+0421
|
|
|
|
|
{ 0x0422, 71 }, // U+0422
|
|
|
|
|
{ 0x0423, 76 }, // U+0423
|
|
|
|
|
{ 0x0424, 83 }, // U+0424
|
|
|
|
|
{ 0x0425, 84 }, // U+0425
|
|
|
|
|
{ 0x0427, 85 }, // U+0427
|
|
|
|
|
{ 0x042A, 71 }, // U+042A
|
|
|
|
|
{ 0x042D, 86 }, // U+042D
|
|
|
|
|
{ 0x042F, 87 }, // U+042F
|
|
|
|
|
{ 0x0430, 88 }, // U+0430
|
|
|
|
|
{ 0x0431, 89 }, // U+0431
|
|
|
|
|
{ 0x0432, 90 }, // U+0432
|
|
|
|
|
{ 0x0433, 91 }, // U+0433
|
|
|
|
|
{ 0x0434, 92 }, // U+0434
|
|
|
|
|
{ 0x0435, 93 }, // U+0435
|
|
|
|
|
{ 0x0436, 94 }, // U+0436
|
|
|
|
|
{ 0x0437, 95 }, // U+0437
|
|
|
|
|
{ 0x0438, 91 }, // U+0438
|
|
|
|
|
{ 0x0439, 91 }, // U+0439
|
|
|
|
|
{ 0x043A, 91 }, // U+043A
|
|
|
|
|
{ 0x043B, 96 }, // U+043B
|
|
|
|
|
{ 0x043C, 97 }, // U+043C
|
|
|
|
|
{ 0x043D, 91 }, // U+043D
|
|
|
|
|
{ 0x043E, 98 }, // U+043E
|
|
|
|
|
{ 0x043F, 91 }, // U+043F
|
|
|
|
|
{ 0x0440, 99 }, // U+0440
|
|
|
|
|
{ 0x0441, 100 }, // U+0441
|
|
|
|
|
{ 0x0442, 101 }, // U+0442
|
|
|
|
|
{ 0x0443, 102 }, // U+0443
|
|
|
|
|
{ 0x0444, 103 }, // U+0444
|
|
|
|
|
{ 0x0445, 104 }, // U+0445
|
|
|
|
|
{ 0x0446, 91 }, // U+0446
|
|
|
|
|
{ 0x0447, 105 }, // U+0447
|
|
|
|
|
{ 0x0448, 91 }, // U+0448
|
|
|
|
|
{ 0x0449, 106 }, // U+0449
|
|
|
|
|
{ 0x044A, 107 }, // U+044A
|
|
|
|
|
{ 0x044B, 91 }, // U+044B
|
|
|
|
|
{ 0x044C, 91 }, // U+044C
|
|
|
|
|
{ 0x044D, 108 }, // U+044D
|
|
|
|
|
{ 0x044E, 90 }, // U+044E
|
|
|
|
|
{ 0x044F, 109 }, // U+044F
|
|
|
|
|
{ 0x0450, 93 }, // U+0450
|
|
|
|
|
{ 0x0451, 93 }, // U+0451
|
|
|
|
|
{ 0x0452, 110 }, // U+0452
|
|
|
|
|
{ 0x0453, 91 }, // U+0453
|
|
|
|
|
{ 0x0454, 111 }, // U+0454
|
|
|
|
|
{ 0x0455, 112 }, // U+0455
|
|
|
|
|
{ 0x0456, 113 }, // U+0456
|
|
|
|
|
{ 0x0457, 114 }, // U+0457
|
|
|
|
|
{ 0x0458, 115 }, // U+0458
|
|
|
|
|
{ 0x0459, 96 }, // U+0459
|
|
|
|
|
{ 0x045A, 91 }, // U+045A
|
|
|
|
|
{ 0x045B, 116 }, // U+045B
|
|
|
|
|
{ 0x045C, 91 }, // U+045C
|
|
|
|
|
{ 0x045D, 91 }, // U+045D
|
|
|
|
|
{ 0x045E, 102 }, // U+045E
|
|
|
|
|
{ 0x045F, 106 }, // U+045F
|
|
|
|
|
{ 0x0462, 71 }, // U+0462
|
|
|
|
|
{ 0x0463, 117 }, // U+0463
|
|
|
|
|
{ 0x0472, 82 }, // U+0472
|
|
|
|
|
{ 0x0473, 98 }, // U+0473
|
|
|
|
|
{ 0x0474, 76 }, // U+0474
|
|
|
|
|
{ 0x0475, 102 }, // U+0475
|
|
|
|
|
{ 0x048B, 91 }, // U+048B
|
|
|
|
|
{ 0x048D, 118 }, // U+048D
|
|
|
|
|
{ 0x048F, 99 }, // U+048F
|
|
|
|
|
{ 0x0491, 91 }, // U+0491
|
|
|
|
|
{ 0x0493, 91 }, // U+0493
|
|
|
|
|
{ 0x0495, 91 }, // U+0495
|
|
|
|
|
{ 0x0496, 79 }, // U+0496
|
|
|
|
|
{ 0x0497, 94 }, // U+0497
|
|
|
|
|
{ 0x0498, 80 }, // U+0498
|
|
|
|
|
{ 0x0499, 95 }, // U+0499
|
|
|
|
|
{ 0x049B, 91 }, // U+049B
|
|
|
|
|
{ 0x049D, 91 }, // U+049D
|
|
|
|
|
{ 0x049F, 91 }, // U+049F
|
|
|
|
|
{ 0x04A1, 91 }, // U+04A1
|
|
|
|
|
{ 0x04A3, 91 }, // U+04A3
|
|
|
|
|
{ 0x04A5, 91 }, // U+04A5
|
|
|
|
|
{ 0x04A7, 91 }, // U+04A7
|
|
|
|
|
{ 0x04A8, 119 }, // U+04A8
|
|
|
|
|
{ 0x04A9, 120 }, // U+04A9
|
|
|
|
|
{ 0x04AA, 72 }, // U+04AA
|
|
|
|
|
{ 0x04AB, 100 }, // U+04AB
|
|
|
|
|
{ 0x04AC, 71 }, // U+04AC
|
|
|
|
|
{ 0x04AD, 101 }, // U+04AD
|
|
|
|
|
{ 0x04AE, 121 }, // U+04AE
|
|
|
|
|
{ 0x04AF, 122 }, // U+04AF
|
|
|
|
|
{ 0x04B0, 121 }, // U+04B0
|
|
|
|
|
{ 0x04B1, 122 }, // U+04B1
|
|
|
|
|
{ 0x04B2, 84 }, // U+04B2
|
|
|
|
|
{ 0x04B3, 104 }, // U+04B3
|
|
|
|
|
{ 0x04B4, 71 }, // U+04B4
|
|
|
|
|
{ 0x04B5, 101 }, // U+04B5
|
|
|
|
|
{ 0x04B6, 85 }, // U+04B6
|
|
|
|
|
{ 0x04B7, 105 }, // U+04B7
|
|
|
|
|
{ 0x04B8, 85 }, // U+04B8
|
|
|
|
|
{ 0x04B9, 105 }, // U+04B9
|
|
|
|
|
{ 0x04BB, 123 }, // U+04BB
|
|
|
|
|
{ 0x04BC, 124 }, // U+04BC
|
|
|
|
|
{ 0x04BD, 125 }, // U+04BD
|
|
|
|
|
{ 0x04BE, 124 }, // U+04BE
|
|
|
|
|
{ 0x04BF, 125 }, // U+04BF
|
|
|
|
|
{ 0x04C1, 79 }, // U+04C1
|
|
|
|
|
{ 0x04C2, 94 }, // U+04C2
|
|
|
|
|
{ 0x04C4, 91 }, // U+04C4
|
|
|
|
|
{ 0x04C5, 75 }, // U+04C5
|
|
|
|
|
{ 0x04C6, 96 }, // U+04C6
|
|
|
|
|
{ 0x04C8, 91 }, // U+04C8
|
|
|
|
|
{ 0x04CA, 91 }, // U+04CA
|
|
|
|
|
{ 0x04CB, 85 }, // U+04CB
|
|
|
|
|
{ 0x04CC, 105 }, // U+04CC
|
|
|
|
|
{ 0x04CE, 97 }, // U+04CE
|
|
|
|
|
{ 0x04D0, 77 }, // U+04D0
|
|
|
|
|
{ 0x04D1, 88 }, // U+04D1
|
|
|
|
|
{ 0x04D2, 77 }, // U+04D2
|
|
|
|
|
{ 0x04D3, 88 }, // U+04D3
|
|
|
|
|
{ 0x04D5, 126 }, // U+04D5
|
|
|
|
|
{ 0x04D7, 93 }, // U+04D7
|
|
|
|
|
{ 0x04D8, 127 }, // U+04D8
|
|
|
|
|
{ 0x04D9, 128 }, // U+04D9
|
|
|
|
|
{ 0x04DA, 127 }, // U+04DA
|
|
|
|
|
{ 0x04DB, 128 }, // U+04DB
|
|
|
|
|
{ 0x04DC, 79 }, // U+04DC
|
|
|
|
|
{ 0x04DD, 94 }, // U+04DD
|
|
|
|
|
{ 0x04DE, 80 }, // U+04DE
|
|
|
|
|
{ 0x04DF, 129 }, // U+04DF
|
|
|
|
|
{ 0x04E0, 130 }, // U+04E0
|
|
|
|
|
{ 0x04E1, 131 }, // U+04E1
|
|
|
|
|
{ 0x04E3, 91 }, // U+04E3
|
|
|
|
|
{ 0x04E5, 91 }, // U+04E5
|
|
|
|
|
{ 0x04E6, 82 }, // U+04E6
|
|
|
|
|
{ 0x04E7, 98 }, // U+04E7
|
|
|
|
|
{ 0x04E8, 82 }, // U+04E8
|
|
|
|
|
{ 0x04E9, 98 }, // U+04E9
|
|
|
|
|
{ 0x04EA, 82 }, // U+04EA
|
|
|
|
|
{ 0x04EB, 98 }, // U+04EB
|
|
|
|
|
{ 0x04EC, 86 }, // U+04EC
|
|
|
|
|
{ 0x04ED, 132 }, // U+04ED
|
|
|
|
|
{ 0x04EE, 76 }, // U+04EE
|
|
|
|
|
{ 0x04EF, 102 }, // U+04EF
|
|
|
|
|
{ 0x04F0, 76 }, // U+04F0
|
|
|
|
|
{ 0x04F1, 102 }, // U+04F1
|
|
|
|
|
{ 0x04F2, 76 }, // U+04F2
|
|
|
|
|
{ 0x04F3, 102 }, // U+04F3
|
|
|
|
|
{ 0x04F4, 85 }, // U+04F4
|
|
|
|
|
{ 0x04F5, 105 }, // U+04F5
|
|
|
|
|
{ 0x04F7, 91 }, // U+04F7
|
|
|
|
|
{ 0x04F9, 91 }, // U+04F9
|
|
|
|
|
{ 0x1EF2, 25 }, // U+1EF2
|
|
|
|
|
{ 0x1EF3, 49 }, // U+1EF3
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x2013, 7 }, // U+2013
|
|
|
|
|
{ 0x2014, 7 }, // U+2014
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x2018, 133 }, // U+2018
|
|
|
|
|
{ 0x2019, 134 }, // U+2019
|
|
|
|
|
{ 0x201A, 135 }, // U+201A
|
|
|
|
|
{ 0x201C, 133 }, // U+201C
|
|
|
|
|
{ 0x201D, 134 }, // U+201D
|
|
|
|
|
{ 0x201E, 135 }, // U+201E
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
{ 0x2026, 6 }, // U+2026
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
{ 0x2039, 53 }, // U+2039
|
|
|
|
|
{ 0x203A, 54 }, // U+203A
|
|
|
|
|
{ 0xFB00, 35 }, // U+FB00
|
|
|
|
|
{ 0xFB01, 35 }, // U+FB01
|
|
|
|
|
{ 0xFB02, 35 }, // U+FB02
|
|
|
|
|
{ 0xFB03, 35 }, // U+FB03
|
|
|
|
|
{ 0xFB04, 35 }, // U+FB04
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const int8_t ubuntu_12_regularKernMatrix[] = {
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -32, -10, -10, -49, 0, -10, -10, 0, 0, 0, 5, 4, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -54, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -46, -29, 0, -32, -28, 0, 0, 0, -10, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, -4, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, -12, -12, 0, 0, -12, -12, -7, 4, -4, 6, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -12, -5, 0, 0, 0, 0, 11, 0, 0, 0, -12, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 7, 0, 0, -5, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -5, -5, -5, 0, -5, -5, -5, -11, 0, -14, -7, -11, -17, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -5, -6, 0, 0, -10, -8, 0, -9, -8, 0, -5, -5, -11, -8, -7, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -17, 0, 0, 0, 0, 0, -5, 0, 0, -7, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -4, -4, -49, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -39, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -45, -30, 0, -26, -29, -12, -6, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -7, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, -12, -12, 11, 0, -13, -13, 6, -28, -9, -31, -16, 4, -32, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, -12, 0, 0, 0, 0, 10, 7, 0, 0, 0, -13, -13, 4, -47, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -17, 0, -32, 0, 0, -40, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, -20, 0, -5, 0, -12, -20, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, 6, -4, -9, -14, -16, 0, -9, -16, -6, 0, 6, 0, -12, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, -20, 0, 0, 0, 0, 0, 6, 0, 0, -9, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -11, -11, -28, 0, -10, -10, 0, 12, 0, 13, 9, 8, 14, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, -11, 0, -25, -24, 10, -20, -24, 7, 0, 0, -10, -10, 8, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, 0, 0, -4, -4, 0, -34, -5, -8, -5, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -4, -4, -10, 0, -4, -4, -7, -18, -5, -14, -9, -16, -22, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -4, -6, -9, -9, -16, -10, -8, -18, -12, 0, -4, -4, -16, -5, -11, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -22, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -31, -8, 0, -25, 10, 0, 7, 0, 0, -6, 13, -8, -8, 12, 0, -8, -8, 6, -26, -4, -25, -7, 10, -32, 8, 0, 0, -11, 0, 0, -5, -5, -5, 0, -6, 0, 0, 0, 0, -5, -5, 5, 0, -6, -8, -8, 10, -6, 6, -8, -4, -8, 0, 14, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, -27, 10,
|
|
|
|
|
0, -4, -4, -10, -4, -4, 0, -8, -4, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -5, -8, -10, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, -9, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, -9, 0, 0, 9, -12, 0, 0, 6, -7, 9, -8, -8, 9, 0, -8, -8, 4, 7, 0, 7, 0, 5, 7, 4, -4, 0, 0, 0, 0, -6, -6, -6, 0, -6, 0, 0, 0, 0, -6, -6, 0, 0, -8, -6, -7, 6, -6, 0, -11, 0, -18, 0, 9, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 7,
|
|
|
|
|
-6, -10, -5, -13, 0, -13, 0, -17, 0, -16, -4, -8, 0, 0, -10, 0, 0, 0, -4, -11, 0, -7, -7, -10, -16, -6, -4, 0, -17, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -5, -14,
|
|
|
|
|
0, -6, -8, 0, -5, 4, 0, 0, 0, 0, -6, 7, -11, -11, 7, 0, -11, -11, 0, 0, -9, 0, 0, 0, 0, 0, -7, 0, -17, 0, 0, -6, -7, -6, 0, -9, 0, 0, 0, 0, -7, -6, 0, -6, -8, -10, -10, 4, -10, 0, -8, -9, -9, 0, 7, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0,
|
|
|
|
|
-4, 0, -10, 0, 0, -26, 0, -24, -7, 5, -7, -18, -5, -5, -32, 0, -5, -5, 0, 6, 0, 7, 0, 0, 8, 0, -6, 0, -16, -20, 0, -6, -6, -6, 0, -6, 0, 0, 0, -11, -6, -6, 0, 0, -10, 0, 0, -14, 0, -10, 0, 0, 0, -8, -30, 0, 0, 0, 0, -6, 0, 0, 0, 0, -10, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -41,
|
|
|
|
|
0, 0, 0, -4, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -7, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, -4, 0, -4, 0, -14, 0, 0, 0, -4, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -8, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5,
|
|
|
|
|
0, 0, -7, 0, -13, 6, -19, 7, 0, 4, -8, 14, -14, -14, 11, 0, -14, -14, 0, 7, 0, 8, 4, 10, 9, 8, 0, 0, -10, 0, 0, -8, -8, -8, 0, -8, 0, 0, 0, 0, -8, -8, 6, 0, -5, -6, -19, 11, -4, 6, -8, 0, -24, 0, 14, 0, 0, 0, 8, -10, 4, 4, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6,
|
|
|
|
|
0, -50, -4, 0, -56, 9, -28, 5, 0, 0, 0, 11, -18, -18, 11, 0, -18, -18, 6, -46, -7, -40, -18, 8, -46, 6, 0, 0, -13, 0, 0, -5, -5, -5, 0, -5, 0, 0, 0, 0, -5, -5, 0, 0, 0, -16, -15, 8, -8, 4, -10, -6, -16, 4, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -48, -47, 6,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
-6, -9, -5, -11, 0, -13, 0, -17, 0, -14, -4, -8, 0, 0, -10, 0, 0, 0, -4, -11, 0, -7, -6, -10, -16, -6, -4, 0, -17, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -4, -12,
|
|
|
|
|
-4, 0, -5, -10, 0, -36, 0, -25, 0, 0, 0, -20, 0, 0, -38, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -6, 0, -16, -8, 0, -8, -8, -13, 0, -8, 0, 0, 0, 0, -8, -4, 0, 0, 0, 4, 4, 0, 4, 0, 0, -10, -7, 0, -34, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, -46,
|
|
|
|
|
-6, -9, -5, 0, 0, -13, 0, 0, 0, -14, -4, -8, 0, 0, -10, 0, 0, 0, -4, -11, 0, -7, -6, -10, -16, -6, -4, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -4, -8,
|
|
|
|
|
0, 0, -5, 0, 0, 6, 0, 5, 0, -5, -6, 11, -4, -4, 9, 0, -4, -4, 0, 0, 0, -4, 0, 8, -8, 6, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 4, -7, -6, -8, 0, 12, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6,
|
|
|
|
|
0, 0, -11, 5, 0, -30, -20, -34, -34, 9, -20, -26, -11, -11, -36, 0, -11, -11, 0, 10, 0, 11, 7, 6, 12, 0, 0, 0, -13, -18, 0, -22, -22, -22, 0, -22, 0, 0, 0, -16, -22, -22, -18, 0, -14, -9, -32, -7, -9, -12, -10, 0, -34, -30, -29, 0, 0, 8, 12, -32, 20, 8, 4, 0, -29, -26, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27,
|
|
|
|
|
0, 0, -4, 0, 0, -4, 0, -16, -4, 0, 0, -4, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, -15, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -4, -9, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8,
|
|
|
|
|
0, 6, -13, 6, 0, -30, -4, -27, -8, 10, -15, -25, -7, -7, -38, -2, -7, -7, 0, 11, 0, 12, 8, 8, 13, 0, 0, 0, -11, -10, 0, -18, -18, -18, 0, -18, 0, 0, 0, -12, -18, -18, -6, 0, -10, 0, 0, 0, 0, -1, -10, 0, -17, -4, -33, 0, 4, 0, 13, -20, 16, 12, 4, 0, -10, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 10, -30,
|
|
|
|
|
0, 5, -6, 0, 0, -15, 0, -19, -4, 6, -8, -7, -6, -6, -24, 0, -6, -6, 0, 8, 0, 9, 5, 4, 10, 0, 0, 0, -15, -12, 0, -9, -9, -9, 0, -10, 0, 0, 0, -8, -9, -9, -7, 0, -8, 0, 0, 0, 0, 0, -7, 0, -8, 0, -20, 0, 0, 0, 7, -10, 4, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 10, -15,
|
|
|
|
|
0, 0, -10, 0, -10, 5, -11, 4, 0, 0, -10, 11, -10, -10, 8, 0, -10, -10, 0, 5, 0, 7, 0, 7, 8, 5, 0, 0, -14, 0, 0, -7, -7, -7, 0, -7, 0, 0, 0, 0, -7, -7, 0, 0, -4, -4, -15, 8, -3, 0, -10, 0, -19, 0, 11, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4,
|
|
|
|
|
0, 0, -17, 7, 0, -26, -20, -33, -15, 10, -23, -32, -16, -16, -46, -3, -16, -16, 0, 12, 0, 13, 9, 8, 14, 4, 0, 0, -10, -16, 0, -20, -20, -20, 0, -20, 0, 0, 0, -16, -20, -20, -8, 0, -14, 0, 0, -6, 0, -11, -9, 0, -30, -13, -32, 0, 0, 0, 16, -31, 16, 13, 0, 0, -17, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 7, -31,
|
|
|
|
|
0, 0, -8, 0, 0, 4, -24, 0, 0, 0, -6, 7, -14, -14, 7, 0, -14, -14, 0, 0, -6, 4, 0, 0, 5, 0, -7, 0, -17, 0, 0, -6, -7, -6, 0, -9, 0, 0, 0, 0, -7, -6, 0, -5, -8, -9, -10, 4, -9, 0, -14, -6, -20, 0, 7, 0, 0, 0, 4, -6, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -17, -17, -11, 0, -17, -17, -10, -13, -15, -12, -15, -14, -11, -16, 0, 0, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -17, -8, -9, -10, -12, -11, 10, -12, 0, 0, -17, -17, -14, -19, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, 0, -11, 0, 0, -10, 0, 0, -6, 0, 0, -12, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, 0, 0, -4, -4, 0, 0, 0, 0, -4, -4, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, 0, 0, 0, 0, 0, 0, 0, 0, -4, -4, -4, -8, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -11, 0, -10, -8, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, 0, -6, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0,
|
|
|
|
|
0, -15, 0, -11, 0, 0, 0, -9, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -7, -5, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -13, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 4, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, -5, 0, 0, -6, 0, 0, 0, 0, -5, 0, 0, 0, 0, 4, 4, 6, 4, 0, -8, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 16, 4, -21, -14, -17, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 7, 6, 8, 0, 0, 16, -8, 4, 0, 0, 12, 0, 16, -7, 16, 12, 16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, -21,
|
|
|
|
|
0, -15, 0, -10, -8, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, 0, -7, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -12, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, -11, 4, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, -10, -10, -10, 0, -12, 0, 0, 0, 0, -12, -10, 0, 0, 0, 0, 0, 8, 0, 0, -8, 0, -14, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -12, 0, -10, -9, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -6, 0, -7, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -10, 0,
|
|
|
|
|
0, -13, 0, -11, -8, 0, 0, -9, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -6, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -11, 0,
|
|
|
|
|
0, -13, 0, -11, -8, 0, 0, -9, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -6, -7, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -11, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 5, -22, -13, -18, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 8, 8, 6, 8, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, -21,
|
|
|
|
|
0, 0, 0, 0, 0, 4, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -5, -6, -4, 0, -4, 0, 0, 0, 0, -6, -5, 0, 0, 0, 0, 0, 7, 0, 0, -8, 0, -11, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 4, -14, 0, -13, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, -6, -6, -6, 0, -5, 0, 0, 0, 0, -6, -6, 0, 0, 0, 7, 7, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, -14,
|
|
|
|
|
0, 0, 0, 0, 4, -13, 0, -11, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 7, 7, 6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, -12,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -8, -8, -8, 0, -8, 0, 0, 0, 0, -8, -8, 0, 0, 0, 0, 0, 7, 0, 0, -8, 0, -12, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, -8, -8, -8, -5, -9, 0, 0, 0, 0, -8, -7, 0, -5, -5, 0, 0, 0, 0, 0, -11, -8, -13, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -10, -12, -5, 0, -12, -12, -8, 0, -9, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -7, 0, 0, 0, -5, 11, 0, 0, 0, -12, -12, 0, -4, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, -4, 0, 0, -6, 0, 0, -4, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, -8, 0, 0, 0, -7, -10, -4, -10, -8, -12, -10, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -8, -6, -7, -10, -8, -8, -9, -10, 0, 0, 0, -12, 0, -9, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, -4, -12, -8, 0, -19, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -31, 0, -5, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, -17, 0, 0, 0, -8, -34, 0, -18, -8, -20, -31, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -34, 0, -10, -14, -14, -22, -8, -13, -23, -15, 0, 0, 0, -20, -6, -16, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -31, 0, 0, -4, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -15, -15, -25, 0, -15, -15, -12, -44, -18, -19, -16, -28, -26, -27, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
-6, -13, 0, -16, 0, -24, 0, -16, 0, -14, 0, -11, 0, 0, -22, 0, 0, 0, 0, -18, 0, -8, 0, -17, -17, -19, -6, 0, -17, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -6, -23,
|
|
|
|
|
0, -16, 0, -11, -13, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -10, 0, -11, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, -12, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, -8, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -16, 0, -11, -8, 0, 0, -9, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, -7, -6, -7, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, 0,
|
|
|
|
|
0, 7, 0, 24, 0, -4, -4, 0, -4, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 36, 28, -4, 6, -4, -4, -4, 0, -4, 3, 3, 6, -4, -4, -4, -4, 4, -4, 0, 0, 0, 0, 0, 0, 28, 0, 0, 0, 0, 28, 38, 44, 0, 44, 44, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -17, 0, 0, -8, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, -14, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -9, -8, -6, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, -12, -6, -6, 0, 0, 0, -9, -5, 0, 0, 0, -5, 0, -5, -5, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -23, -5, -5, -5, -5, 0, -5, -5, -5, -5, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -6, -8, 0, -5, 4, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -9, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 6, 0, 0, 7, 0, 0, 0, 0, -11, -11, 0, 0, 0, 0, 0, -6, 0, 0, 0, -6, 0, 0, 0, 0, -7, 0, -6, -6, -10, -7, 4, -7, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -9, -4, 0, -10, 0, 0, 0, 0, -8, 0, 0, 0, -7, 0, -5, 0, 0,
|
|
|
|
|
-5, -15, 0, -7, -12, -5, 6, -6, -5, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -8, 0, 0, 0, 0, -7, -9, 0, -8, 0, 0, 0, 0, 0, 0, -7, -4, 0, 0, 0, 0, 0, -14, -6, 0, -6, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, -4, 0, 0, 0, -6, 0, -7, 0, 0, 0, 0, -4, 0, -12, -4, 0, 0, 0,
|
|
|
|
|
0, 0, -11, 5, 0, -30, -20, -34, -34, 9, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -34, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, -11, 0, -44, -26, 8, -26, -26, 0, 0, 0, -11, -11, 6, 0, 7, 0, -39, 0, -36, -37, -46, -46, -30, -32, -45, -42, -46, -37, -46, -25, -25, -32, -26, -30, -37, -25, -31, -44, 11, -46, -42, 0, 17, 0, 11, 0, -30, -9, -48, 0, -25, 0, 0, -37, -39, 0, -39, -53, 0, -26, -45, 0, 0, -27,
|
|
|
|
|
0, 0, -9, 0, 0, 9, -12, 0, 0, 6, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, -8, 0, 8, 7, 0, 9, 7, 0, 0, 0, -8, -8, 5, 0, 0, 0, 0, -6, 0, 0, 7, -6, 6, 0, 8, 0, -6, 0, -6, -13, -6, -6, 6, -19, 0, -13, 0, 0, 8, 0, 0, 0, 0, 0, 8, 0, 0, -12, -4, 7, -6, 0, 0, -20, 0, -5, 0, 0, 0, -14, 0, 0, 5, 7,
|
|
|
|
|
0, -5, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, -6, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0,
|
|
|
|
|
-6, -30, -8, -9, -14, 0, 0, 0, 0, -12, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -20, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -6, 0, -12, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -15, -9, 0, 0, 0, 0, -14, 0, 0, -6, 0, 0, -8, -8, -8, -6, -15, -4, -5, 0, 0, -8, -5, -8, 0, 0, -4, 0, 0, 0, -15, 0, 0, 0, 0,
|
|
|
|
|
0, 0, -7, 0, -13, 6, -19, 7, 0, 4, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, -14, 0, 10, 10, 0, 14, 11, 9, 6, 0, -14, -14, 10, 0, 8, 7, 0, -10, 0, 0, 11, -8, 11, 0, 10, 0, -8, 0, -8, -16, -19, -11, 11, -24, 0, -9, 8, 8, 8, -8, 6, 0, 0, 0, 8, -9, 0, -17, -8, 0, -19, 0, -15, -24, 0, -8, -6, 0, 7, -20, 0, 0, 0, 6,
|
|
|
|
|
0, 6, -13, 6, 0, -30, -4, -27, -8, 10, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -17, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 11, -7, 0, -32, -25, 10, -25, -24, 6, 0, -2, -7, -7, 8, 0, 8, 0, -10, -7, -7, -8, -25, -15, 0, 0, -25, -12, -15, -8, -15, 0, 0, -16, 0, 0, -8, 0, 0, -12, 13, -15, -8, 0, 16, 0, 13, 0, 0, -8, -15, 0, 0, 0, 0, 0, -10, 0, -10, 0, 0, 0, 0, 8, 10, -30,
|
|
|
|
|
0, -31, -8, 0, -25, 10, 0, 7, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -4, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -26, -8, 6, 10, 10, 0, 13, 10, 9, 6, 0, -8, -8, 10, -20, 8, 6, 0, -6, 0, 0, 10, -5, 11, 0, 10, 0, -5, 0, -5, -15, -6, -5, 10, -11, 0, -10, 7, 8, 0, 0, 6, 0, 0, 0, 0, -10, 0, -9, 0, -32, -8, 0, -14, -7, 0, -6, 0, 0, 8, -14, 0, -28, -27, 10,
|
|
|
|
|
0, -4, -4, -10, -4, -4, 0, -8, -4, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, -8, 0, 0, 0, -4, -8, -7, 0, -6, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -4, 0, 0, 0,
|
|
|
|
|
0, -5, 0, 10, -7, 7, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 10, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 10, 9, 0, 10, 8, 7, 6, 0, -7, -10, 7, -11, 7, 0, 0, 0, 0, 0, 8, 0, 8, 0, 9, 0, 0, 0, 0, 0, 12, 0, 8, -8, 0, 0, 0, 6, 0, 0, 0, 0, 0, 26, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 7, 12, 0, 0, 0, 0,
|
|
|
|
|
0, -5, -5, -10, 0, 0, 0, -6, -4, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -6, 0, 0, 0, -5, -6, -6, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -6, -6, -8, -6, 0, 0, 0, 0, -5, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
-6, -9, -5, -11, 0, -13, 6, -17, 0, -14, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -9, -9, -14, -8, -8, -19, -12, 0, 0, 0, -10, 0, -12, 0, -5, 0, 0, 0, -10, 0, -4, -5, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -5, -5, -6, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -5, -11, 0, -5, -6, -4, -12,
|
|
|
|
|
-4, 0, -5, -10, 0, -36, 0, -25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -37, -29, 0, -20, -28, -11, -6, 0, 0, 0, -7, 0, 0, 0, -8, 0, 0, 0, -18, 0, 0, 0, -19, -5, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 4, 0, 6, 0, -5, 0, 0, 0, 0, 0, 0, 0, 4, -46,
|
|
|
|
|
0, 0, -11, 5, 0, -30, -20, -34, -34, 9, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -34, -30, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, -11, 0, -26, -26, 8, -26, -26, 0, 0, 0, -11, -11, 6, 0, 7, 0, -18, 0, -30, -30, -28, -22, -28, -33, -27, -30, -22, -16, -22, -25, -9, -22, -7, -30, -30, -25, -31, -30, 11, -31, -31, 0, 17, 0, 11, 0, -27, -9, -31, 12, -9, 0, 0, -27, -32, 0, -32, -54, 0, -26, -45, 0, 0, -27,
|
|
|
|
|
-6, -9, -5, -11, 0, -13, 0, -17, 0, -14, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -22, -23, -15, -8, -22, -24, -18, 0, 0, 0, -10, 0, -16, 0, -7, 0, -6, -5, -16, 0, -4, -4, -17, -7, 0, -6, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, -6, 0, 0, -6, -6, -6, -6, 0, 0, 0, 0, 0, 0, -6, 0, 0, -7, 0, 0, -4, -23, 0, -4, -6, -4, -12,
|
|
|
|
|
0, 0, -10, 0, -10, 5, -11, 4, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, -10, 0, 0, 0, 0, 11, 0, 0, 0, 0, -10, -10, 7, 0, 0, 0, 0, -11, 0, 0, 8, -7, 8, 0, 8, 0, -7, 0, -7, -12, -3, -7, 8, -18, 0, -12, 0, 0, 7, -8, 0, 0, 0, 0, 7, -11, 0, 0, -9, 8, -4, 0, 0, -19, 0, 0, -7, 0, 0, -13, 0, 0, 0, 4,
|
|
|
|
|
0, -5, 0, 10, -7, 7, 0, 17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 10, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 10, 9, 0, 10, 8, 7, 6, 0, -7, -10, 7, -11, 7, 0, 0, 0, 0, 0, 8, 0, 8, 0, 9, 0, 0, 0, 0, 0, 12, 0, 8, -8, 0, 0, 0, 6, 0, 0, 0, 0, 0, 28, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 7, 12, 0, 0, 0, 0,
|
|
|
|
|
-6, -9, -5, -11, 0, -13, 6, -17, 0, -14, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -9, -8, -14, -8, -8, -18, -12, 0, 0, 0, -10, 0, -11, 0, 0, 0, 0, 0, -9, 0, 0, -4, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -11, 0, -4, -6, -4, -12,
|
|
|
|
|
0, -11, 0, -10, -8, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0,
|
|
|
|
|
-5, -9, -4, 0, -8, -8, 7, -8, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -6, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, -6, 0, 0, 0,
|
|
|
|
|
0, -14, 0, -9, -6, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, -6, 0, -32, -22, -18, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -4, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -18, 0, 0, 0, -15, 0, 0, 0, 0, 9, 9, 0, 7, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 9, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0,
|
|
|
|
|
0, -12, 0, 10, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 8, 0, 8, 0, 0, 0, 0, 0, 10, -4, 8, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0,
|
|
|
|
|
0, -10, 0, -11, -7, -5, 6, -4, -5, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, -5, 0, 0, -5, 0, -10, 0,
|
|
|
|
|
0, -4, -7, 0, 0, 0, -11, 4, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, -10, 9, 0, 8, 0, -12, 0, -10, 0, 0, -12, 8, 0, 0, 0, 0, 6, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -15, 0, -10, -7, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, -6, 0, 0, 0, -11, 4, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, -10, 9, 0, 8, 0, -12, 0, -10, 0, 0, -12, 8, 0, 0, 0, 0, 6, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -13, 0, -11, -8, -8, 7, -9, -7, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -7, 0, 0, 0, 0, 0, 0, -6, 0, -7, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, -7, 0, 0, -7, -9, -11, 0,
|
|
|
|
|
0, -13, -4, -11, -8, -8, 7, -9, -7, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -6, 0, 0, 0, 0, 0, 0, -6, 0, -7, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, -6, 0, 0, -6, -9, -11, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 4, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -5, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 8, 0, 8, 0, -5, 0, 0, 6, 4, -5, 6, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, -5, 0, -20, -12, -18, 0, -20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, -4, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, -15, 0, 0, 0, 0, 9, 9, 0, 7, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 9, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, -6, 0, -9, 0, -12, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, -5, 0, 0, -11, 0, -5, 0, -5, 8, 7, -5, 5, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 4, -12,
|
|
|
|
|
-8, -13, 0, -11, -8, -10, 7, -9, -9, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -9, -5, 0, 0, 0, 0, 0, -6, 0, -7, 0, 0, 0, -6, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -4, -9, -9, -11, 0,
|
|
|
|
|
0, 0, -6, 0, 0, 0, -6, 0, 0, -4, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, -4, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, -8, 9, 0, 8, 0, -8, 0, -8, 0, 0, -8, 7, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -12, 0, 10, 0, 0, 0, 14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 8, 0, 8, 0, 0, 0, 0, 0, 10, -4, 8, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0,
|
|
|
|
|
0, -32, 0, -10, -21, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -19, -8, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0,
|
|
|
|
|
0, -13, 0, -11, -8, -8, 7, -9, -6, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -6, 0, 0, 0, 0, 0, 0, -6, 0, -7, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, -6, -9, -11, 0,
|
|
|
|
|
0, -13, 0, -11, -8, -8, 7, -9, -7, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -7, 0, 0, 0, 0, 0, 0, -6, 0, -7, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, -7, -9, -11, 0,
|
|
|
|
|
0, -23, -8, 0, -10, 0, 0, 0, 0, -12, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -5, -8, 0, 0, 0, -6, 0, 0, -6, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -8, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -4, 0, -4, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -9, 0, -10, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -23, -7, -9, -10, 0, 0, 0, 0, -12, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -30, 0, -10, -20, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -16, -8, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0,
|
|
|
|
|
0, 0, -7, 8, 0, 0, -27, -28, -19, 15, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, -36, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, -8, 0, 0, -20, 13, -27, -20, 8, 8, 0, -8, -12, 10, 7, 13, 0, -22, 0, -18, -20, -42, -35, -13, -16, -38, -25, -35, -20, -36, -8, -8, -31, -9, -14, -20, -8, -14, -30, 18, -35, -25, 0, 0, 0, 18, 0, -13, -8, -36, 0, -8, 0, 0, -20, -22, 0, -22, -16, 7, -9, -16, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 9, -31, -8, -18, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 8, 0, -11, 0, 0, 0, 0, 13, 13, 0, 12, 7, 0, 13, 7, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 13, 0, 0, 10, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0,
|
|
|
|
|
0, -17, -4, 0, -7, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -38, 0, 0, -26, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, -20, 0, 0, 0, -6, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
-7, -23, -5, 0, -8, -14, 7, 0, -6, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -25, 0, -8, -6, -7, -14, -6, 0, -16, -9, 0, 0, 0, -15, -5, -8, -7, 0, 0, 0, 0, 0, 0, -12, -8, -8, 0, 0, -4, 0, 0, 0, 0, -8, 0, -6, 0, -8, 0, -6, 0, 0, -6, -6, 10, -6, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, -8, -8, 0, -8, 0, 0, 0,
|
|
|
|
|
0, -26, 0, 0, -12, -5, 0, 0, -5, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, -14, 0, 0, -6, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, -17, 7, 0, -26, -20, -33, -15, 10, -23, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -30, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, -16, 0, 0, 0, 0, -32, 0, 0, 0, -3, -16, 0, 8, 0, 0, 0, -16, -10, -14, -15, -30, -20, -7, -10, -30, -19, -20, -16, -20, 0, 0, -28, -6, -8, -15, 0, -8, -22, 13, -26, -17, 0, 0, 0, 13, 0, -5, 0, -26, 14, 0, 0, 0, -10, -17, 0, -16, -10, 0, 0, -10, 4, 7, -31,
|
|
|
|
|
0, 0, 0, -5, 4, -14, 0, -13, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, -6, 0, 0, -14, 0, -6, 0, -6, 9, 7, 0, 6, 0, 0, 9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 7, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 5, -14,
|
|
|
|
|
-4, -35, -6, -7, -11, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -33, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -23, -7, -9, -10, 0, 0, 0, 0, -12, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -14, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -7, -5, -13, 0, -8, 0, -8, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, -5, -5, 0, 0, 0, -5, 0, -4, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, -6, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, -6, 0, -4, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -18, 0, -12, -7, -5, 6, -4, -5, -13, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -16, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, -5, 0, 0, 0,
|
|
|
|
|
0, 0, -4, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -7, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -10, -5, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, -17, 0, -12, -7, -6, 6, -4, -5, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, 0, 0, -5, 0, 0, 0,
|
|
|
|
|
-6, -9, -5, -11, 0, -13, 6, -17, 0, -14, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -11, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, -10, 0, 0, 0, -5, 0, 0, 0, -9, 0, 0, -4, -10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -5, -5, -5, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, -4, -6, -4, -12,
|
|
|
|
|
0, -13, 0, -11, -8, -8, 7, -9, -6, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, -7, 0, 0, 0, 0, 0, 0, -6, 0, -7, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, -7, 0, 0, -7, -9, -11, 0,
|
|
|
|
|
0, 0, -4, 0, -8, -4, 0, -6, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, -17, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, -7, 0, 0, 0, 0, 0, 0, -6, 0, 0, 0, 0, 0, 0, -13, -8, 0, -4, 0, 0, -13, 0, -5, 0, 0, 0, -4, -4, -4, 0, -10, -4, 0, 0, 0, -8, 0, -10, 0, 0, 0, 0, 0, 0, -12, 0, 0, 0, 0,
|
|
|
|
|
0, -8, -8, 0, 0, 0, 0, 0, 0, -6, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -9, 0, -9, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, -5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -30, -8, -8, -46, 0, -8, -8, 0, 0, 0, 8, 7, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -53, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -8, 0, 0, 0, 0, -30, 0, 0, 0, 0, -8, -8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
|
|
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, -10, -10, 11, 0, -11, -12, 6, -27, -8, -30, -14, 5, -31, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -27, -10, 0, 0, 0, 0, 10, 0, 0, 0, 0, -11, -11, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -31, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const EpdLigaturePair ubuntu_12_regularLigaturePairs[] = {
|
|
|
|
|
{ 0x00660066, 0xFB00 }, // f f -> U+FB00
|
|
|
|
|
{ 0x00660069, 0xFB01 }, // f i -> U+FB01
|
|
|
|
|
{ 0x0066006C, 0xFB02 }, // f l -> U+FB02
|
|
|
|
|
{ 0xFB000069, 0xFB03 }, // U+FB00 i -> U+FB03
|
|
|
|
|
{ 0xFB00006C, 0xFB04 }, // U+FB00 l -> U+FB04
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const EpdFontData ubuntu_12_regular = {
|
|
|
|
|
ubuntu_12_regularBitmaps,
|
|
|
|
|
ubuntu_12_regularGlyphs,
|
|
|
|
|
ubuntu_12_regularIntervals,
|
2026-02-25 03:56:11 -06:00
|
|
|
43,
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
29,
|
|
|
|
|
24,
|
|
|
|
|
-5,
|
|
|
|
|
false,
|
perf: Reduce overall flash usage by 30.7% by compressing built-in fonts (#831)
## Summary
**What is the goal of this PR?**
Compress reader font bitmaps to reduce flash usage by 30.7%.
**What changes are included?**
- New `EpdFontGroup` struct and extended `EpdFontData` with
`groups`/`groupCount` fields
- `--compress` flag in `fontconvert.py`: groups glyphs (ASCII base group
+ groups of 8) and compresses each with raw DEFLATE
- `FontDecompressor` class with 4-slot LRU cache for on-demand
decompression during rendering
- `GfxRenderer` transparently routes bitmap access through
`getGlyphBitmap()` (compressed or direct flash)
- Uses `uzlib` for decompression with minimal heap overhead.
- 48 reader fonts (Bookerly, NotoSans 12-18pt, OpenDyslexic) regenerated
with compression; 5 UI fonts unchanged
- Round-trip verification script (`verify_compression.py`) runs as part
of font generation
## Additional Context
## Flash & RAM
| | baseline | font-compression | Difference |
|--|--------|-----------------|------------|
| Flash (ELF) | 6,302,476 B (96.2%) | 4,365,022 B (66.6%) | -1,937,454 B
(-30.7%) |
| firmware.bin | 6,468,192 B | 4,531,008 B | -1,937,184 B (-29.9%) |
| RAM | 101,700 B (31.0%) | 103,076 B (31.5%) | +1,376 B (+0.5%) |
## Script-Based Grouping (Cold Cache)
Comparison of uncompressed baseline vs script-based group compression
(4-slot LRU cache, cleared each page). Glyphs are grouped by Unicode
block (ASCII, Latin-1, Latin Extended-A, Combining Marks, Cyrillic,
General Punctuation, etc.) instead of sequential groups of 8.
### Render Time
| | Baseline | Compressed (cold cache) | Difference |
|---|---|---|---|
| **Median** | 414.9 ms | 431.6 ms | +16.7 ms (+4.0%) |
| **Pages** | 37 | 37 | |
### Memory Usage
| | Baseline | Compressed (cold cache) | Difference |
|---|---|---|---|
| **Heap free (median)** | 187.0 KB | 176.3 KB | -10.7 KB |
| **Heap free (min)** | 186.0 KB | 166.5 KB | -19.5 KB |
| **Largest block (median)** | 148.0 KB | 128.0 KB | -20.0 KB |
| **Largest block (min)** | 148.0 KB | 120.0 KB | -28.0 KB |
### Cache Effectiveness
| | Misses/page | Hit rate |
|---|---|---|
| **Compressed (cold cache)** | 2.1 | 99.85% |
------
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES**_
Implementation was done by Claude Code (Opus 4.6) based on a plan
developed collaboratively. All generated font headers were verified with
an automated round-trip decompression test. The firmware was compiled
successfully but has not yet been tested on-device.
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 20:30:15 +11:00
|
|
|
nullptr,
|
|
|
|
|
0,
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
ubuntu_12_regularKernLeftClasses,
|
|
|
|
|
ubuntu_12_regularKernRightClasses,
|
|
|
|
|
ubuntu_12_regularKernMatrix,
|
fix: Use fixed-point fractional x-advance and kerning for better text layout (#1168)
## Summary
**What is the goal of this PR?**
Hopefully fixes #1182.
_Note: I think letterforms got a "heavier" appearance after #1098, which
makes this more noticeable. The current version of this PR reverts the
change to add `--force-autohint` for Bookerly, which to me seems to
bring the font back to a more aesthetic and consistent weight._
#### Problem
Character spacing was uneven in certain words. The word "drew" in
Bookerly was the clearest example: a visible gap between `d` and `r`,
while `e` and `w` appeared tightly condensed. The root cause was
twofold:
1. **Integer-only glyph advances.** `advanceX` was stored as a `uint8_t`
of whole pixels, sourced from FreeType's hinted `advance.x` (which
grid-fits to integers). A glyph whose true advance is 15.56px was stored
as 16px -- an error of +0.44px per character that compounds across a
line.
2. **Floor-rounded kerning.** Kern adjustments were converted with
`math.floor()`, which systematically over-tightened negative kerns. A
kern of -0.3px became -1px -- a 0.7px over-correction that visibly
closed gaps.
Combined, these produced the classic symptom: some pairs too wide,
others too tight, with the imbalance varying per word.
#### Solution: fixed-point accumulation with 1/16-pixel resolution, for
sub-pixel precision during text layout
All font metrics now use a "fixed-point 4" format -- 4 fractional bits
giving 1/16-pixel (0.0625px) resolution. This is implemented with plain
integer arithmetic (shifts and adds), requiring no floating-point on the
ESP32.
**How it works:**
A value like 15.56px is stored as the integer `249`:
```
249 = 15 * 16 + 9 (where 9/16 = 0.5625, closest to 0.56)
```
Two storage widths share the same 4 fractional bits:
| Field | Type | Format | Range | Use |
|-------|------|--------|-------|-----|
| `advanceX` | `uint16_t` | 12.4 | 0 -- 4095.9375 px | Glyph advance
width |
| `kernMatrix` | `int8_t` | 4.4 | -8.0 -- +7.9375 px | Kerning
adjustment |
Because both have 4 fractional bits, they add directly into a single
`int32_t` accumulator during layout. The accumulator is only snapped to
the nearest whole pixel at the moment each glyph is rendered:
```cpp
int32_t xFP = fp4::fromPixel(startX); // pixel to 12.4: startX << 4
for each character:
xFP += kernFP; // add 4.4 kern (sign-extends into int32_t)
int xPx = fp4::toPixel(xFP); // snap to nearest pixel: (xFP + 8) >> 4
render glyph at xPx;
xFP += glyph->advanceX; // add 12.4 advance
```
Fractional remainders carry forward indefinitely. Rounding errors stay
below +/- 0.5px and never compound.
#### Concrete example: "drew" in Bookerly
**Before** (integer advances, floor-rounded kerning):
| Char | Advance | Kern | Cursor | Snap | Gap from prev |
|------|---------|------|--------|------|---------------|
| d | 16 px | -- | 33 | 33 | -- |
| r | 12 px | 0 | 49 | 49 | ~2px |
| e | 13 px | -1 | 60 | 60 | ~0px |
| w | 22 px | -1 | 72 | 72 | ~0px |
The d-to-r gap was visibly wider than the tightly packed `rew`.
**After** (12.4 advances, 4.4 kerning, fractional accumulation):
| Char | Advance (FP) | Kern (FP) | Accumulator | Snap | Ink start | Gap
from prev |
|------|-------------|-----------|-------------|------|-----------|---------------|
| d | 249 (15.56px) | -- | 528 | 33 | 34 | -- |
| r | 184 (11.50px) | 0 | 777 | 49 | 49 | 0px |
| e | 208 (13.00px) | -8 (-0.50px) | 953 | 60 | 61 | 1px |
| w | 356 (22.25px) | -4 (-0.25px) | 1157 | 72 | 72 | 0px |
Spacing is now `0, 1, 0` pixels -- nearly uniform. Verified on-device:
all 5 copies of "drew" in the test EPUB produce identical spacing,
confirming zero accumulator drift.
#### Changes
**Font conversion (`fontconvert.py`)**
- Use `linearHoriAdvance` (FreeType 16.16, unhinted) instead of
`advance.x` (26.6, grid-fitted to integers) for glyph advances
- Encode kern values as 4.4 fixed-point with `round()` instead of
`floor()`
- Add `fp4_from_ft16_16()` and `fp4_from_design_units()` helper
functions
- Add module-level documentation of fixed-point conventions
**Font data structures (`EpdFontData.h`)**
- `EpdGlyph::advanceX`: `uint8_t` to `uint16_t` (no memory cost due to
existing struct padding)
- Add `fp4` namespace with `constexpr` helpers: `fromPixel()`,
`toPixel()`, `toFloat()`
- Document fixed-point conventions
**Font API (`EpdFont.h/cpp`, `EpdFontFamily.h/cpp`)**
- `getKerning()` return type: `int8_t` to `int` (to avoid truncation of
the 4.4 value)
**Rendering (`GfxRenderer.cpp`)**
- `drawText()`: replace integer cursor with `int32_t` fixed-point
accumulator
- `drawTextRotated90CW()`: same accumulator treatment for vertical
layout
- `getTextAdvanceX()`, `getSpaceWidth()`, `getSpaceKernAdjust()`,
`getKerning()`: convert from fixed-point to pixel at API boundary
**Regenerated all built-in font headers** with new 12.4 advances and 4.4
kern values.
#### Memory impact
Zero additional RAM. The `advanceX` field grew from `uint8_t` to
`uint16_t`, but the `EpdGlyph` struct already had 1 byte of padding at
that position, so the struct size is unchanged. The fixed-point
accumulator is a single `int32_t` on the stack.
#### Test plan
- [ ] Verify "drew" spacing in Bookerly at small, medium, and large
sizes
- [ ] Verify uppercase kerning pairs: AVERY, WAVE, VALUE
- [ ] Verify ligature words: coffee, waffle, office
- [ ] Verify all built-in fonts render correctly at each size
- [ ] Verify rotated text (progress bar percentage) renders correctly
- [ ] Verify combining marks (accented characters) still position
correctly
- [ ] Spot-check a full-length book for any layout regressions
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6
helped figure out a non-floating point approach for sub-pixel error
accumulation**_
2026-03-01 10:43:37 -06:00
|
|
|
417,
|
|
|
|
|
471,
|
|
|
|
|
132,
|
|
|
|
|
135,
|
feat: Support for kerning and ligatures (#873)
## Summary
**What is the goal of this PR?**
Improved typesetting, including
[kerning](https://en.wikipedia.org/wiki/Kerning) and
[ligatures](https://en.wikipedia.org/wiki/Ligature_(writing)#Latin_alphabet).
**What changes are included?**
- The script to convert built-in fonts now adds kerning and ligature
information to the generated font headers.
- Epub page layout calculates proper kerning spaces and makes ligature
substitutions according to the selected font.



## Additional Context
- I am not a typography expert.
- The implementation has been reworked from the earlier version, so it
is no longer necessary to omit Open Dyslexic, and kerning data now
covers all fonts, styles, and codepoints for which we include bitmap
data.
- Claude Opus 4.6 helped with a lot of this.
- There's an included test epub document with lots of kerning and
ligature examples, shown in the photos.
**_After some time to mature, I think this change is in decent shape to
merge and get people testing._**
After opening this PR I came across #660, which overlaps in adding
ligature support.
---
### AI Usage
While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.
Did you use AI tools to help write this code? _**YES, Claude Opus 4.6**_
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-24 02:31:43 -06:00
|
|
|
ubuntu_12_regularLigaturePairs,
|
|
|
|
|
5,
|
Aleo, Noto Sans, Open Dyslexic fonts (#163)
## Summary
* Swap out Bookerly font due to licensing issues, replace default font
with Aleo
* I did a bunch of searching around for a nice replacement font, and
this trumped several other like Literata, Merriwether, Vollkorn, etc
* Add Noto Sans, and Open Dyslexic as font options
* They can be selected in the settings screen
* Add font size options (Small, Medium, Large, Extra Large)
* Adjustable in settings
* Swap out uses of reader font in headings and replaced with slightly
larger Ubuntu font
* Replaced PixelArial14 font as it was difficult to track down, replace
with Space Grotesk
* Remove auto formatting on generated font files
* Massively speeds up formatting step now that there is a lot more CPP
font source
* Include fonts with their licenses in the repo
## Additional Context
Line compression setting will follow
| Font | Small | Medium | Large | X Large |
| --- | --- | --- | --- | --- |
| Aleo |

|

|

|

|
| Noto Sans |

|

|

|

|
| Open Dyslexic |

|

|

|

|
2025-12-30 18:21:47 +10:00
|
|
|
};
|