diff --git a/ts/src/font/fonts/5x7.ts b/ts/src/font/fonts/5x7.ts deleted file mode 100644 index e69de29..0000000 diff --git a/ts/src/font/fonts/test.ts b/ts/src/font/fonts/test.ts deleted file mode 100644 index e69de29..0000000 diff --git a/ts/src/font/fonts/u8g2_font_5x7_tf.u8g2font.ts b/ts/src/font/fonts/u8g2_font_5x7_tf.u8g2font.ts new file mode 100644 index 0000000..c2e6979 --- /dev/null +++ b/ts/src/font/fonts/u8g2_font_5x7_tf.u8g2font.ts @@ -0,0 +1,137 @@ +export const u8g2_font_5x7_tf_u8g2font: number[] = [ + 0xbf, 0x00, 0x02, 0x02, 0x03, 0x03, 0x03, 0x04, 0x04, 0x05, 0x07, 0x00, + 0xff, 0x06, 0xff, 0x06, 0x00, 0x01, 0x0a, 0x02, 0x16, 0x06, 0x2f, 0x20, + 0x05, 0x00, 0xbd, 0x01, 0x21, 0x06, 0xb1, 0xb1, 0x19, 0x29, 0x22, 0x07, + 0x5b, 0xb7, 0x49, 0x56, 0x00, 0x23, 0x0a, 0x2d, 0xb1, 0xab, 0x86, 0xaa, + 0x86, 0xaa, 0x00, 0x24, 0x09, 0x2d, 0xb1, 0x5b, 0xf5, 0x4e, 0x69, 0x01, + 0x25, 0x08, 0x34, 0xb1, 0xc9, 0xb1, 0x77, 0x00, 0x26, 0x09, 0x2c, 0xb1, + 0x8b, 0x29, 0x56, 0x31, 0x05, 0x27, 0x05, 0x99, 0xb7, 0x19, 0x28, 0x07, + 0x72, 0xb1, 0x53, 0xcd, 0x00, 0x29, 0x08, 0x72, 0xb1, 0x89, 0xa9, 0x52, + 0x00, 0x2a, 0x07, 0x6b, 0xb1, 0x49, 0xd5, 0x6a, 0x2b, 0x0a, 0x2d, 0xb1, + 0xcd, 0x28, 0x0e, 0x99, 0x51, 0x04, 0x2c, 0x07, 0x5b, 0xaf, 0x53, 0x25, + 0x00, 0x2d, 0x06, 0x0c, 0xb5, 0x19, 0x01, 0x2e, 0x06, 0x52, 0xb1, 0x19, + 0x01, 0x2f, 0x07, 0x24, 0xb3, 0x8f, 0x6d, 0x00, 0x30, 0x08, 0x73, 0xb1, + 0xab, 0x5c, 0x15, 0x00, 0x31, 0x07, 0x73, 0xb1, 0x4b, 0xb2, 0x35, 0x32, + 0x09, 0x34, 0xb1, 0x53, 0x31, 0xc7, 0x72, 0x04, 0x33, 0x0a, 0x34, 0xb1, + 0x19, 0x39, 0x69, 0x24, 0x93, 0x02, 0x34, 0x0a, 0x34, 0xb1, 0x8d, 0xaa, + 0x1a, 0x31, 0x27, 0x00, 0x35, 0x0a, 0x34, 0xb1, 0x19, 0x7a, 0x23, 0x99, + 0x14, 0x00, 0x36, 0x0a, 0x34, 0xb1, 0x53, 0x79, 0x45, 0x99, 0x14, 0x00, + 0x37, 0x0a, 0x34, 0xb1, 0x19, 0x39, 0xe6, 0x98, 0x23, 0x00, 0x38, 0x0a, + 0x34, 0xb1, 0x53, 0x31, 0xa9, 0x28, 0x93, 0x02, 0x39, 0x0a, 0x34, 0xb1, + 0x53, 0x51, 0xa6, 0x9d, 0x14, 0x00, 0x3a, 0x07, 0x6a, 0xb1, 0x19, 0x71, + 0x04, 0x3b, 0x08, 0x33, 0xaf, 0xb3, 0x91, 0x2a, 0x01, 0x3c, 0x07, 0x6b, + 0xb1, 0x4d, 0x75, 0x01, 0x3d, 0x08, 0x1c, 0xb3, 0x19, 0x19, 0x8d, 0x00, + 0x3e, 0x07, 0x6b, 0xb1, 0xc9, 0x55, 0x09, 0x3f, 0x09, 0x73, 0xb1, 0x6b, + 0xa6, 0x0c, 0x13, 0x00, 0x40, 0x09, 0x34, 0xb1, 0x53, 0x51, 0xdd, 0x48, + 0x01, 0x41, 0x09, 0x34, 0xb1, 0x53, 0x51, 0x8e, 0x29, 0x03, 0x42, 0x0a, + 0x34, 0xb1, 0x59, 0x71, 0xa4, 0x28, 0x47, 0x02, 0x43, 0x09, 0x34, 0xb1, + 0x53, 0x51, 0x97, 0x49, 0x01, 0x44, 0x09, 0x34, 0xb1, 0x59, 0xd1, 0x39, + 0x12, 0x00, 0x45, 0x09, 0x34, 0xb1, 0x19, 0x7a, 0xe5, 0x3c, 0x02, 0x46, + 0x08, 0x34, 0xb1, 0x19, 0x7a, 0xe5, 0x1a, 0x47, 0x09, 0x34, 0xb1, 0x53, + 0x51, 0xa7, 0x99, 0x06, 0x48, 0x08, 0x34, 0xb1, 0x89, 0x72, 0x4c, 0x33, + 0x49, 0x07, 0x73, 0xb1, 0x59, 0xb1, 0x35, 0x4a, 0x08, 0x34, 0xb1, 0x6f, + 0xcb, 0xa4, 0x00, 0x4b, 0x0a, 0x34, 0xb1, 0x89, 0x2a, 0x49, 0x99, 0xca, + 0x00, 0x4c, 0x07, 0x34, 0xb1, 0xc9, 0xdd, 0x23, 0x4d, 0x09, 0x34, 0xb1, + 0x89, 0xe3, 0x88, 0x66, 0x00, 0x4e, 0x08, 0x34, 0xb1, 0x89, 0x6b, 0xa9, + 0x33, 0x4f, 0x09, 0x34, 0xb1, 0x53, 0xd1, 0x99, 0x14, 0x00, 0x50, 0x0a, + 0x34, 0xb1, 0x59, 0x51, 0x8e, 0x94, 0x33, 0x00, 0x51, 0x0a, 0x3c, 0xaf, + 0x53, 0xd1, 0x5c, 0x49, 0xa3, 0x00, 0x52, 0x09, 0x34, 0xb1, 0x59, 0x51, + 0x8e, 0xd4, 0x0c, 0x53, 0x0a, 0x34, 0xb1, 0x53, 0x31, 0x65, 0x54, 0x26, + 0x05, 0x54, 0x07, 0x73, 0xb1, 0x59, 0xb1, 0x0b, 0x55, 0x08, 0x34, 0xb1, + 0x89, 0x9e, 0x49, 0x01, 0x56, 0x09, 0x34, 0xb1, 0x89, 0xce, 0x24, 0x15, + 0x00, 0x57, 0x09, 0x34, 0xb1, 0x89, 0xe6, 0x38, 0x62, 0x00, 0x58, 0x0a, + 0x34, 0xb1, 0x89, 0x32, 0x49, 0x15, 0x65, 0x00, 0x59, 0x08, 0x73, 0xb1, + 0x49, 0x56, 0x59, 0x01, 0x5a, 0x09, 0x34, 0xb1, 0x19, 0x39, 0xb6, 0x47, + 0x00, 0x5b, 0x07, 0x73, 0xb1, 0x19, 0xb1, 0x39, 0x5c, 0x09, 0x24, 0xb3, + 0xc9, 0x28, 0xa3, 0x8c, 0x02, 0x5d, 0x07, 0x73, 0xb1, 0x99, 0xcd, 0x11, + 0x5e, 0x05, 0x53, 0xb9, 0x6b, 0x5f, 0x06, 0x0c, 0xb1, 0x19, 0x01, 0x60, + 0x06, 0x52, 0xb9, 0x89, 0x01, 0x61, 0x08, 0x24, 0xb1, 0x1b, 0x51, 0xa9, + 0x02, 0x62, 0x0a, 0x34, 0xb1, 0xc9, 0x79, 0x45, 0x39, 0x12, 0x00, 0x63, + 0x06, 0x23, 0xb1, 0x9b, 0x59, 0x64, 0x08, 0x34, 0xb1, 0xaf, 0x46, 0x94, + 0x69, 0x65, 0x08, 0x24, 0xb1, 0x53, 0x69, 0x64, 0x05, 0x66, 0x09, 0x34, + 0xb1, 0xad, 0xca, 0x99, 0x23, 0x00, 0x67, 0x09, 0x2c, 0xaf, 0x1b, 0x31, + 0xa9, 0x8c, 0x06, 0x68, 0x08, 0x34, 0xb1, 0xc9, 0x79, 0x45, 0x33, 0x69, + 0x08, 0x73, 0xb1, 0xcb, 0x48, 0x56, 0x03, 0x6a, 0x09, 0x7b, 0xaf, 0xcd, + 0xb0, 0x54, 0x15, 0x00, 0x6b, 0x09, 0x34, 0xb1, 0xc9, 0x55, 0x92, 0xa9, + 0x0c, 0x6c, 0x07, 0x73, 0xb1, 0x91, 0x5d, 0x03, 0x6d, 0x08, 0x24, 0xb1, + 0x49, 0x69, 0x48, 0x19, 0x6e, 0x07, 0x24, 0xb1, 0x59, 0xd1, 0x0c, 0x6f, + 0x08, 0x24, 0xb1, 0x53, 0x51, 0x26, 0x05, 0x70, 0x09, 0x2c, 0xaf, 0x59, + 0x51, 0x8e, 0x94, 0x01, 0x71, 0x08, 0x2c, 0xaf, 0x1b, 0x51, 0xa6, 0x1d, + 0x72, 0x08, 0x24, 0xb1, 0x59, 0x51, 0x67, 0x00, 0x73, 0x08, 0x24, 0xb1, + 0x1b, 0x1a, 0x0d, 0x05, 0x74, 0x09, 0x34, 0xb1, 0xcb, 0x71, 0xe6, 0x8c, + 0x04, 0x75, 0x07, 0x24, 0xb1, 0x89, 0x66, 0x1a, 0x76, 0x07, 0x63, 0xb1, + 0x49, 0x56, 0x05, 0x77, 0x07, 0x24, 0xb1, 0x89, 0x72, 0x1c, 0x78, 0x08, + 0x24, 0xb1, 0x89, 0x49, 0xaa, 0x18, 0x79, 0x09, 0x2c, 0xaf, 0x89, 0x32, + 0x95, 0x25, 0x00, 0x7a, 0x08, 0x24, 0xb1, 0x19, 0xb1, 0x1c, 0x01, 0x7b, + 0x08, 0x73, 0xb1, 0x4d, 0x49, 0xd6, 0x01, 0x7c, 0x05, 0xb1, 0xb1, 0x39, + 0x7d, 0x09, 0x73, 0xb1, 0xc9, 0x51, 0xc5, 0x14, 0x01, 0x7e, 0x07, 0x14, + 0xb9, 0x4b, 0x2a, 0x01, 0xa0, 0x05, 0x00, 0xbd, 0x01, 0xa1, 0x06, 0xb1, + 0xb1, 0x49, 0x23, 0xa2, 0x09, 0x34, 0xaf, 0x8d, 0x23, 0x35, 0x67, 0x02, + 0xa3, 0x08, 0x2c, 0xb1, 0x55, 0x71, 0x56, 0x02, 0xa4, 0x0a, 0x2d, 0xb1, + 0xc9, 0x69, 0xa6, 0xb8, 0x72, 0x00, 0xa5, 0x09, 0x73, 0xb1, 0x49, 0xaa, + 0x5a, 0x31, 0x01, 0xa6, 0x06, 0xa9, 0xb1, 0x51, 0x02, 0xa7, 0x08, 0x7b, + 0xaf, 0x9b, 0xaa, 0x92, 0x0b, 0xa8, 0x06, 0x4b, 0xbb, 0x49, 0x01, 0xa9, + 0x0b, 0x3d, 0xaf, 0x5b, 0x59, 0xa5, 0xa9, 0x92, 0x4e, 0x0b, 0xaa, 0x06, + 0x1b, 0xb7, 0x5b, 0x49, 0xab, 0x07, 0x1d, 0xb3, 0x8b, 0xb2, 0x01, 0xac, + 0x06, 0x14, 0xb3, 0x19, 0x39, 0xad, 0x05, 0x4b, 0xb5, 0x19, 0xae, 0x0b, + 0x3d, 0xaf, 0x5b, 0x79, 0xa4, 0x39, 0x75, 0x5a, 0x00, 0xaf, 0x06, 0x0c, + 0xbb, 0x19, 0x01, 0xb0, 0x06, 0x5b, 0xb7, 0xeb, 0x02, 0xb1, 0x0b, 0x35, + 0xb1, 0xcd, 0x28, 0x0e, 0x99, 0x51, 0x1c, 0x02, 0xb2, 0x06, 0x62, 0xb5, + 0x51, 0x06, 0xb3, 0x06, 0x62, 0xb5, 0x19, 0x69, 0xb4, 0x06, 0x52, 0xb9, + 0x53, 0x00, 0xb5, 0x08, 0x2c, 0xaf, 0x89, 0xe6, 0x48, 0x19, 0xb6, 0x08, + 0x34, 0xb1, 0x1b, 0x6a, 0xf5, 0x03, 0xb7, 0x06, 0x52, 0xb5, 0x19, 0x01, + 0xb8, 0x06, 0x52, 0xaf, 0x53, 0x00, 0xb9, 0x07, 0x63, 0xb5, 0x4b, 0x32, + 0x0d, 0xba, 0x06, 0x1b, 0xb7, 0xeb, 0x02, 0xbb, 0x08, 0x1d, 0xb3, 0x89, + 0xa5, 0x4c, 0x00, 0xbc, 0x09, 0x3c, 0xaf, 0xc9, 0xcd, 0xa8, 0x76, 0x00, + 0xbd, 0x09, 0x3c, 0xaf, 0xc9, 0x2d, 0x1d, 0xb3, 0x00, 0xbe, 0x0a, 0x3c, + 0xaf, 0x91, 0x3a, 0xaa, 0xa8, 0x76, 0x00, 0xbf, 0x09, 0x73, 0xb1, 0xcb, + 0x30, 0xc5, 0x54, 0x01, 0xc0, 0x09, 0x34, 0xb1, 0x53, 0x51, 0x8e, 0x29, + 0x03, 0xc1, 0x09, 0x34, 0xb1, 0x53, 0x51, 0x8e, 0x29, 0x03, 0xc2, 0x09, + 0x34, 0xb1, 0x53, 0x51, 0x8e, 0x29, 0x03, 0xc3, 0x09, 0x34, 0xb1, 0x53, + 0x51, 0x8e, 0x29, 0x03, 0xc4, 0x0a, 0x34, 0xb1, 0x89, 0x49, 0xc5, 0x31, + 0x65, 0x00, 0xc5, 0x09, 0x34, 0xb1, 0x93, 0x2a, 0x8e, 0x29, 0x03, 0xc6, + 0x09, 0x34, 0xb1, 0x1b, 0xa9, 0x1a, 0xaa, 0x25, 0xc7, 0x0a, 0x3c, 0xaf, + 0x53, 0x51, 0x97, 0x49, 0x46, 0x00, 0xc8, 0x09, 0x34, 0xb1, 0x19, 0x7a, + 0xe5, 0x3c, 0x02, 0xc9, 0x09, 0x34, 0xb1, 0x19, 0x7a, 0xe5, 0x3c, 0x02, + 0xca, 0x09, 0x34, 0xb1, 0x19, 0x7a, 0xe5, 0x3c, 0x02, 0xcb, 0x09, 0x34, + 0xb1, 0x19, 0x7a, 0xe5, 0x3c, 0x02, 0xcc, 0x07, 0x73, 0xb1, 0x59, 0xb1, + 0x35, 0xcd, 0x07, 0x73, 0xb1, 0x59, 0xb1, 0x35, 0xce, 0x07, 0x73, 0xb1, + 0x59, 0xb1, 0x35, 0xcf, 0x07, 0x73, 0xb1, 0x59, 0xb1, 0x35, 0xd0, 0x09, + 0x34, 0xb1, 0x99, 0x69, 0x75, 0x8d, 0x04, 0xd1, 0x08, 0x34, 0xb1, 0x49, + 0x73, 0xa9, 0x33, 0xd2, 0x09, 0x34, 0xb1, 0x53, 0xd1, 0x99, 0x14, 0x00, + 0xd3, 0x09, 0x34, 0xb1, 0x53, 0xd1, 0x99, 0x14, 0x00, 0xd4, 0x09, 0x34, + 0xb1, 0x53, 0xd1, 0x99, 0x14, 0x00, 0xd5, 0x09, 0x34, 0xb1, 0x53, 0xd1, + 0x99, 0x14, 0x00, 0xd6, 0x0a, 0x34, 0xb1, 0x89, 0x49, 0x45, 0x33, 0x29, + 0x00, 0xd7, 0x08, 0x24, 0xb1, 0x89, 0x49, 0xaa, 0x18, 0xd8, 0x09, 0x34, + 0xb1, 0x1b, 0xe9, 0x48, 0x47, 0x02, 0xd9, 0x08, 0x34, 0xb1, 0x89, 0x9e, + 0x49, 0x01, 0xda, 0x08, 0x34, 0xb1, 0x89, 0x9e, 0x49, 0x01, 0xdb, 0x08, + 0x34, 0xb1, 0x89, 0x9e, 0x49, 0x01, 0xdc, 0x0a, 0x34, 0xb1, 0x89, 0x19, + 0x45, 0x33, 0x29, 0x00, 0xdd, 0x08, 0x73, 0xb1, 0x49, 0x56, 0x59, 0x01, + 0xde, 0x0a, 0x34, 0xb1, 0xc9, 0x2b, 0x8e, 0x94, 0x33, 0x00, 0xdf, 0x09, + 0x34, 0xb1, 0x53, 0x51, 0x95, 0x56, 0x02, 0xe0, 0x0a, 0x34, 0xb1, 0xcb, + 0x28, 0x8e, 0xa8, 0x54, 0x01, 0xe1, 0x09, 0x34, 0xb1, 0xad, 0x47, 0x54, + 0xaa, 0x00, 0xe2, 0x09, 0x34, 0xb1, 0xad, 0xd2, 0x88, 0x4a, 0x15, 0xe3, + 0x0a, 0x34, 0xb1, 0x4b, 0x2a, 0x8e, 0xa8, 0x54, 0x01, 0xe4, 0x09, 0x34, + 0xb1, 0xab, 0xe1, 0x88, 0x4a, 0x15, 0xe5, 0x09, 0x34, 0xb1, 0x93, 0x72, + 0x44, 0xa5, 0x0a, 0xe6, 0x08, 0x24, 0xb1, 0x1b, 0x69, 0xc5, 0x01, 0xe7, + 0x08, 0x6b, 0xaf, 0x9b, 0x59, 0x25, 0x00, 0xe8, 0x0a, 0x34, 0xb1, 0xcb, + 0x28, 0xaa, 0x34, 0xb2, 0x02, 0xe9, 0x09, 0x34, 0xb1, 0xad, 0x55, 0x1a, + 0x59, 0x01, 0xea, 0x0a, 0x34, 0xb1, 0x8b, 0x29, 0xaa, 0x34, 0xb2, 0x02, + 0xeb, 0x0a, 0x34, 0xb1, 0x49, 0x19, 0xab, 0x34, 0xb2, 0x02, 0xec, 0x08, + 0x73, 0xb1, 0xc9, 0x49, 0x56, 0x03, 0xed, 0x07, 0x73, 0xb1, 0x2b, 0x65, + 0x35, 0xee, 0x07, 0x73, 0xb1, 0xab, 0x66, 0x35, 0xef, 0x08, 0x73, 0xb1, + 0x49, 0x59, 0x56, 0x03, 0xf0, 0x0a, 0x34, 0xb1, 0xcb, 0xc8, 0x8a, 0x32, + 0x29, 0x00, 0xf1, 0x09, 0x34, 0xb1, 0x4b, 0x2a, 0xad, 0x68, 0x06, 0xf2, + 0x0a, 0x34, 0xb1, 0xcb, 0x28, 0xaa, 0x28, 0x93, 0x02, 0xf3, 0x09, 0x34, + 0xb1, 0xad, 0x55, 0x94, 0x49, 0x01, 0xf4, 0x0a, 0x34, 0xb1, 0xd3, 0x58, + 0x45, 0x99, 0x14, 0x00, 0xf5, 0x0a, 0x34, 0xb1, 0x4b, 0x2a, 0xaa, 0x28, + 0x93, 0x02, 0xf6, 0x0a, 0x34, 0xb1, 0xab, 0xa1, 0x8a, 0x32, 0x29, 0x00, + 0xf7, 0x09, 0x2c, 0xb1, 0xd3, 0x70, 0x64, 0xa8, 0x00, 0xf8, 0x09, 0x24, + 0xb1, 0x1b, 0x69, 0xa4, 0x91, 0x00, 0xf9, 0x09, 0x34, 0xb1, 0xcb, 0xa8, + 0x34, 0xd3, 0x00, 0xfa, 0x07, 0x34, 0xb1, 0x6d, 0x9a, 0x69, 0xfb, 0x08, + 0x34, 0xb1, 0xd3, 0x30, 0x9a, 0x69, 0xfc, 0x09, 0x34, 0xb1, 0xab, 0x51, + 0x34, 0xd3, 0x00, 0xfd, 0x09, 0x3c, 0xaf, 0x6d, 0xca, 0x54, 0x96, 0x00, + 0xfe, 0x0a, 0x34, 0xaf, 0xc9, 0x2b, 0xca, 0x91, 0x32, 0x00, 0xff, 0x0a, + 0x3c, 0xaf, 0xab, 0x51, 0x94, 0xa9, 0x2c, 0x01, 0x00, 0x00, 0x00, 0x04, + 0xff, 0xff, 0x00, 0x00 +]; diff --git a/ts/src/font/index.ts b/ts/src/font/index.ts index c1f0884..d0e0e40 100644 --- a/ts/src/font/index.ts +++ b/ts/src/font/index.ts @@ -1,5 +1,8 @@ +import { u8g2_font_5x7_tf_u8g2font } from "./fonts/u8g2_font_5x7_tf.u8g2font"; +import { MonoDisplayFont } from "./types"; export * from "./types"; -export * as _5x7 from "./5x7"; -export * from "./u8g2"; \ No newline at end of file +export * from "./u8g2"; + +export const u8g2_font_5x7_tf = new MonoDisplayFont(u8g2_font_5x7_tf_u8g2font); \ No newline at end of file diff --git a/ts/src/font/types.ts b/ts/src/font/types.ts index 60f68e1..a74ff8c 100644 --- a/ts/src/font/types.ts +++ b/ts/src/font/types.ts @@ -1,5 +1,11 @@ export class MonoDisplayFont { - public static FONT_DATA: Uint8Array; + public FONT_DATA: Uint8Array; + constructor(font_data: Uint8Array | number[]) { + this.FONT_DATA = new Uint8Array(font_data.length); + for (let i = 0; i < font_data.length; i++) { + this.FONT_DATA[i] = font_data[i] || 0; + } + } } export type RasterizedText = { pixels: Uint8Array; width: number; height: number }; @@ -7,5 +13,5 @@ export type RasterizedText = { pixels: Uint8Array; width: number; height: number export type RasterizeTextFunction = ( text: string, cellHeight: number, - font?: MonoDisplayFont, + font: MonoDisplayFont, ) => RasterizedText; diff --git a/ts/src/font/u8g2.ts b/ts/src/font/u8g2.ts index 06a4887..a0e6659 100644 --- a/ts/src/font/u8g2.ts +++ b/ts/src/font/u8g2.ts @@ -1,398 +1,280 @@ // --------------------------------------------------------------------------- -// U8G2 font binary format constants (header offsets) +// U8G2 font binary format decoder +// Derived from: https://github.com/olikraus/u8g2/blob/master/csrc/u8g2_font.c +// https://github.com/olikraus/u8g2/blob/master/tools/font/bdfconv/bdf_rle.c // --------------------------------------------------------------------------- -// The U8G2 font blob layout (all offsets are byte positions): -// [0] glyph_cnt – number of glyphs -// [1] bbx_mode – bounding-box mode -// [2] bits_per_0 – RLE bits for "off" run -// [3] bits_per_1 – RLE bits for "on" run -// [4] bits_per_char_width -// [5] bits_per_char_height -// [6] bits_per_char_x -// [7] bits_per_char_y -// [8] bits_per_delta_x -// [9] max_char_width -// [10] max_char_height -// [11] x_offset (signed) -// [12] y_offset (signed) -// [13] ascent_A -// [14] descent_g (negative) -// [15] ascent_para -// [16] descent_para -// [17..18] start_pos_upper (uint16 BE) – offset of uppercase glyph block -// [19..20] start_pos_lower (uint16 BE) – offset of lowercase glyph block -// [21..22] start_pos_unicode (uint16 BE) – offset of Unicode glyph block -// Glyph records follow the three jump-table blocks. -// Each glyph record: -// • A variable-length RLE bitstream (width × height bits) – MSB first -// • Followed by a fixed-width metadata section: -// dwidth (bits_per_delta_x bits) – horizontal advance -// width (bits_per_char_width) -// height (bits_per_char_height) -// x (bits_per_char_x, signed) -// y (bits_per_char_y, signed) -// The very first two bytes of each record encode the encoding (codepoint) -// as a uint16 LE, then a uint16 LE jump offset to the next record. - -import type { MonoDisplayFont } from "./types"; -// --------------------------------------------------------------------------- -// Built-in 5×7 font (printable ASCII 0x20–0x7E) -// Each entry is 5 column bytes, bit 0 = top row. -// --------------------------------------------------------------------------- -// prettier-ignore -const BUILTIN_FONT_DATA: Record = { - 0x20: [0x00, 0x00, 0x00, 0x00, 0x00], // space - 0x21: [0x00, 0x00, 0x5F, 0x00, 0x00], // ! - 0x22: [0x00, 0x07, 0x00, 0x07, 0x00], // " - 0x23: [0x14, 0x7F, 0x14, 0x7F, 0x14], // # - 0x24: [0x24, 0x2A, 0x7F, 0x2A, 0x12], // $ - 0x25: [0x23, 0x13, 0x08, 0x64, 0x62], // % - 0x26: [0x36, 0x49, 0x55, 0x22, 0x50], // & - 0x27: [0x00, 0x05, 0x03, 0x00, 0x00], // ' - 0x28: [0x00, 0x1C, 0x22, 0x41, 0x00], // ( - 0x29: [0x00, 0x41, 0x22, 0x1C, 0x00], // ) - 0x2A: [0x14, 0x08, 0x3E, 0x08, 0x14], // * - 0x2B: [0x08, 0x08, 0x3E, 0x08, 0x08], // + - 0x2C: [0x00, 0x50, 0x30, 0x00, 0x00], // , - 0x2D: [0x08, 0x08, 0x08, 0x08, 0x08], // - - 0x2E: [0x00, 0x60, 0x60, 0x00, 0x00], // . - 0x2F: [0x20, 0x10, 0x08, 0x04, 0x02], // / - 0x30: [0x3E, 0x51, 0x49, 0x45, 0x3E], // 0 - 0x31: [0x00, 0x42, 0x7F, 0x40, 0x00], // 1 - 0x32: [0x42, 0x61, 0x51, 0x49, 0x46], // 2 - 0x33: [0x21, 0x41, 0x45, 0x4B, 0x31], // 3 - 0x34: [0x18, 0x14, 0x12, 0x7F, 0x10], // 4 - 0x35: [0x27, 0x45, 0x45, 0x45, 0x39], // 5 - 0x36: [0x3C, 0x4A, 0x49, 0x49, 0x30], // 6 - 0x37: [0x01, 0x71, 0x09, 0x05, 0x03], // 7 - 0x38: [0x36, 0x49, 0x49, 0x49, 0x36], // 8 - 0x39: [0x06, 0x49, 0x49, 0x29, 0x1E], // 9 - 0x3A: [0x00, 0x36, 0x36, 0x00, 0x00], // : - 0x3B: [0x00, 0x56, 0x36, 0x00, 0x00], // ; - 0x3C: [0x08, 0x14, 0x22, 0x41, 0x00], // < - 0x3D: [0x14, 0x14, 0x14, 0x14, 0x14], // = - 0x3E: [0x00, 0x41, 0x22, 0x14, 0x08], // > - 0x3F: [0x02, 0x01, 0x51, 0x09, 0x06], // ? - 0x40: [0x32, 0x49, 0x79, 0x41, 0x3E], // @ - 0x41: [0x7E, 0x11, 0x11, 0x11, 0x7E], // A - 0x42: [0x7F, 0x49, 0x49, 0x49, 0x36], // B - 0x43: [0x3E, 0x41, 0x41, 0x41, 0x22], // C - 0x44: [0x7F, 0x41, 0x41, 0x22, 0x1C], // D - 0x45: [0x7F, 0x49, 0x49, 0x49, 0x41], // E - 0x46: [0x7F, 0x09, 0x09, 0x09, 0x01], // F - 0x47: [0x3E, 0x41, 0x49, 0x49, 0x7A], // G - 0x48: [0x7F, 0x08, 0x08, 0x08, 0x7F], // H - 0x49: [0x00, 0x41, 0x7F, 0x41, 0x00], // I - 0x4A: [0x20, 0x40, 0x41, 0x3F, 0x01], // J - 0x4B: [0x7F, 0x08, 0x14, 0x22, 0x41], // K - 0x4C: [0x7F, 0x40, 0x40, 0x40, 0x40], // L - 0x4D: [0x7F, 0x02, 0x0C, 0x02, 0x7F], // M - 0x4E: [0x7F, 0x04, 0x08, 0x10, 0x7F], // N - 0x4F: [0x3E, 0x41, 0x41, 0x41, 0x3E], // O - 0x50: [0x7F, 0x09, 0x09, 0x09, 0x06], // P - 0x51: [0x3E, 0x41, 0x51, 0x21, 0x5E], // Q - 0x52: [0x7F, 0x09, 0x19, 0x29, 0x46], // R - 0x53: [0x46, 0x49, 0x49, 0x49, 0x31], // S - 0x54: [0x01, 0x01, 0x7F, 0x01, 0x01], // T - 0x55: [0x3F, 0x40, 0x40, 0x40, 0x3F], // U - 0x56: [0x1F, 0x20, 0x40, 0x20, 0x1F], // V - 0x57: [0x3F, 0x40, 0x38, 0x40, 0x3F], // W - 0x58: [0x63, 0x14, 0x08, 0x14, 0x63], // X - 0x59: [0x07, 0x08, 0x70, 0x08, 0x07], // Y - 0x5A: [0x61, 0x51, 0x49, 0x45, 0x43], // Z - 0x5B: [0x00, 0x7F, 0x41, 0x41, 0x00], // [ - 0x5C: [0x02, 0x04, 0x08, 0x10, 0x20], // backslash - 0x5D: [0x00, 0x41, 0x41, 0x7F, 0x00], // ] - 0x5E: [0x04, 0x02, 0x01, 0x02, 0x04], // ^ - 0x5F: [0x40, 0x40, 0x40, 0x40, 0x40], // _ - 0x60: [0x00, 0x01, 0x02, 0x04, 0x00], // ` - 0x61: [0x20, 0x54, 0x54, 0x54, 0x78], // a - 0x62: [0x7F, 0x48, 0x44, 0x44, 0x38], // b - 0x63: [0x38, 0x44, 0x44, 0x44, 0x20], // c - 0x64: [0x38, 0x44, 0x44, 0x48, 0x7F], // d - 0x65: [0x38, 0x54, 0x54, 0x54, 0x18], // e - 0x66: [0x08, 0x7E, 0x09, 0x01, 0x02], // f - 0x67: [0x0C, 0x52, 0x52, 0x52, 0x3E], // g - 0x68: [0x7F, 0x08, 0x04, 0x04, 0x78], // h - 0x69: [0x00, 0x44, 0x7D, 0x40, 0x00], // i - 0x6A: [0x20, 0x40, 0x44, 0x3D, 0x00], // j - 0x6B: [0x7F, 0x10, 0x28, 0x44, 0x00], // k - 0x6C: [0x00, 0x41, 0x7F, 0x40, 0x00], // l - 0x6D: [0x7C, 0x04, 0x18, 0x04, 0x78], // m - 0x6E: [0x7C, 0x08, 0x04, 0x04, 0x78], // n - 0x6F: [0x38, 0x44, 0x44, 0x44, 0x38], // o - 0x70: [0x7C, 0x14, 0x14, 0x14, 0x08], // p - 0x71: [0x08, 0x14, 0x14, 0x18, 0x7C], // q - 0x72: [0x7C, 0x08, 0x04, 0x04, 0x08], // r - 0x73: [0x48, 0x54, 0x54, 0x54, 0x20], // s - 0x74: [0x04, 0x3F, 0x44, 0x40, 0x20], // t - 0x75: [0x3C, 0x40, 0x40, 0x20, 0x7C], // u - 0x76: [0x1C, 0x20, 0x40, 0x20, 0x1C], // v - 0x77: [0x3C, 0x40, 0x30, 0x40, 0x3C], // w - 0x78: [0x44, 0x28, 0x10, 0x28, 0x44], // x - 0x79: [0x0C, 0x50, 0x50, 0x50, 0x3C], // y - 0x7A: [0x44, 0x64, 0x54, 0x4C, 0x44], // z - 0x7B: [0x00, 0x08, 0x36, 0x41, 0x00], // { - 0x7C: [0x00, 0x00, 0x7F, 0x00, 0x00], // | - 0x7D: [0x00, 0x41, 0x36, 0x08, 0x00], // } - 0x7E: [0x10, 0x08, 0x08, 0x10, 0x08], // ~ -}; - -// Built-in font metrics (5×7, 1px column gap → advance = 6) -export const GLYPH_W = 5; -export const GLYPH_H = 7; -export const GLYPH_ADVANCE = 6; // width + 1px gap - -/** - * Return column bytes for a codepoint from the built-in 5×7 font. - * Falls back to '?' (0x3F) for unknown codepoints. - */ -export function builtinGlyph(cp: number): Uint8Array { - const cols = BUILTIN_FONT_DATA[cp] ?? BUILTIN_FONT_DATA[0x3F]!; - return new Uint8Array(cols); -} +const HDR_SIZE = 23; // --------------------------------------------------------------------------- -// U8G2 binary font parser +// BitReader — LSB-first, matching u8g2_font_decode_get_unsigned_bits exactly: +// val = *ptr >> bit_pos +// if (bit_pos + cnt >= 8): val |= *(ptr+1) << (8 - bit_pos); ptr++ +// val &= (1 << cnt) - 1 // --------------------------------------------------------------------------- - -/** Bit-level reader over a Uint8Array, MSB-first within each byte. */ class BitReader { - private pos = 0; // bit position - constructor(private data: Uint8Array, startBit = 0) { - this.pos = startBit; + private ptr: number; + private bitPos: number; + + constructor(private data: Uint8Array, startByte: number) { + this.ptr = startByte; + this.bitPos = 0; } - read(n: number): number { - let v = 0; - for (let i = 0; i < n; i++) { - const byteIdx = (this.pos) >> 3; - const bitIdx = 7 - ((this.pos) & 7); // MSB first - v = (v << 1) | ((this.data[byteIdx]! >> bitIdx) & 1); - this.pos++; + read(cnt: number): number { + let val = (this.data[this.ptr]! >> this.bitPos) & 0xFF; + const newPos = this.bitPos + cnt; + if (newPos >= 8) { + this.ptr++; + val |= (this.data[this.ptr]! << (8 - this.bitPos)); + this.bitPos = newPos - 8; + } else { + this.bitPos = newPos; } - return v; + return val & ((1 << cnt) - 1); } - /** Read a signed value stored in `n` bits (two's complement). */ - readSigned(n: number): number { - const v = this.read(n); - const sign = 1 << (n - 1); - return v & sign ? v - (sign << 1) : v; + // Offset-binary signed: stored as unsigned + (1 << (cnt-1)), so decode = val - (1 << (cnt-1)) + // Matches: v = get_unsigned(cnt); d = 1 << (cnt-1); v -= d; + readSigned(cnt: number): number { + return this.read(cnt) - (1 << (cnt - 1)); } - - get bitPos(): number { return this.pos; } } -/** Read a big-endian uint16 from a byte array at `offset`. */ -function readU16BE(data: Uint8Array, offset: number): number { - return ((data[offset]! << 8) | data[offset + 1]!) >>> 0; +function readU16BE(data: Uint8Array, off: number): number { + return ((data[off]! << 8) | data[off + 1]!) >>> 0; } -/** - * Glyph result from a U8G2 font blob. - * `cols` – column bytes (bit 0 = top row), length = w. - */ -export interface U8G2GlyphData { - cols: Uint8Array; - w: number; - h: number; - yOff: number; // positive = shift down -} - -/** - * Parse header fields from a U8G2 font blob and return the key metrics - * needed to decode glyphs. - */ +// --------------------------------------------------------------------------- +// Font header (23 bytes) +// Offsets 17/18, 19/20, 21/22 are relative to end of header (byte 23) +// --------------------------------------------------------------------------- function parseHeader(font: Uint8Array) { return { - glyphCnt: font[0]!, - bbxMode: font[1]!, - bitsPerRle0: font[2]!, - bitsPerRle1: font[3]!, + m0: font[2]!, + m1: font[3]!, bitsPerW: font[4]!, bitsPerH: font[5]!, bitsPerX: font[6]!, bitsPerY: font[7]!, bitsPerDx: font[8]!, - maxCharW: font[9]!, - maxCharH: font[10]!, - xOff: font[11]! as number, // signed but treated as-is - yOff: font[12]! as number, - ascentA: font[13]!, - descentG: font[14]!, - startUpper: readU16BE(font, 17), - startLower: readU16BE(font, 19), - startUnicode: readU16BE(font, 21), + ascentA: font[13]! as number, + // offsets stored as BE uint16, relative to end of 23-byte header + startUpper: HDR_SIZE + readU16BE(font, 17), + startLower: HDR_SIZE + readU16BE(font, 19), + startUnicode: HDR_SIZE + readU16BE(font, 21), }; } -/** - * Decode one glyph from the U8G2 font RLE bitstream at `byteOffset`. - * Returns decoded data or null if the stream is malformed. - * - * U8G2 glyph encoding (bitstream, MSB-first): - * [bitsPerW] w glyph bitmap width - * [bitsPerH] h glyph bitmap height - * [bitsPerX] x signed x bearing - * [bitsPerY] y signed y bearing (positive = up from baseline) - * [bitsPerDx] dwidth horizontal advance - * Then w×h bits of RLE-encoded bitmap: - * repeat: - * 1-bit type (0 = off-run, 1 = on-run) - * bitsPerRleN count bits - */ +// --------------------------------------------------------------------------- +// RLE decoder — matches fd_decode / u8g2_font_decode_glyph in the C source. +// +// The bitstream is a sequence of (zeros-run, ones-run) pairs, repeated. +// Each pair: +// [m0 bits] count of zero pixels +// [m1 bits] count of one pixels +// [unary] repetition: read 1-bits until a 0-stop; reps = (number of 1s) + 1 +// +// This matches bg_rle_compress which encodes pairs and then a unary repeat. +// --------------------------------------------------------------------------- +function decodeRLE( + br: BitReader, + w: number, + h: number, + m0: number, + m1: number, +): Uint8Array { + const total = w * h; + const bits = new Uint8Array(total); + let filled = 0; + + while (filled < total) { + const zeros = br.read(m0); + const ones = br.read(m1); + let reps = 1; + while (br.read(1) === 1) reps++; + + for (let r = 0; r < reps && filled < total; r++) { + for (let p = 0; p < zeros && filled < total; p++) bits[filled++] = 0; + for (let p = 0; p < ones && filled < total; p++) bits[filled++] = 1; + } + } + return bits; +} + +// --------------------------------------------------------------------------- +// Glyph result +// --------------------------------------------------------------------------- +export interface U8G2GlyphData { + cols: Uint8Array; // column bytes (bit 0 = top row), length = w + w: number; + h: number; + dx: number; // horizontal advance + yOff: number; // downward offset from top of cell to top of glyph +} + +// --------------------------------------------------------------------------- +// Decode the glyph bitstream at byteOffset (after the record header bytes). +// Field order from C source (u8g2_font.c lines 590–625): +// glyph_width, glyph_height, x (signed), y (signed), delta_x (signed) +// --------------------------------------------------------------------------- function decodeGlyphAt( font: Uint8Array, byteOffset: number, hdr: ReturnType, -): U8G2GlyphData | null { - try { - const br = new BitReader(font, byteOffset * 8); - - const w = br.read(hdr.bitsPerW); - const h = br.read(hdr.bitsPerH); - const xBrg = br.readSigned(hdr.bitsPerX); - const yBrg = br.readSigned(hdr.bitsPerY); // positive = up - br.read(hdr.bitsPerDx); // advance – not needed here - - if (w === 0 || h === 0) { - // Blank glyph (e.g. space) - return { cols: new Uint8Array(GLYPH_W), w: GLYPH_W, h: GLYPH_H, yOff: 0 }; - } +): U8G2GlyphData { + const br = new BitReader(font, byteOffset); + + const w = br.read(hdr.bitsPerW); + const h = br.read(hdr.bitsPerH); + /* x */ br.readSigned(hdr.bitsPerX); // x bearing — consumed but not needed + const yB = br.readSigned(hdr.bitsPerY); + const dx = br.readSigned(hdr.bitsPerDx); + + if (w === 0 || h === 0) { + // Blank glyph (e.g. space) — advance is stored in dx + return { cols: new Uint8Array(1), w: 1, h: 0, dx: Math.max(dx, 1), yOff: 0 }; + } - // Decode RLE bitmap into a flat bit array [row-major, top-to-bottom] - const totalBits = w * h; - const bits = new Uint8Array(totalBits); - let filled = 0; - - while (filled < totalBits) { - const type = br.read(1); // 0=off, 1=on - const bitsN = type === 0 ? hdr.bitsPerRle0 : hdr.bitsPerRle1; - const count = br.read(bitsN) + 1; // stored as count-1 - const val = type & 1; - for (let i = 0; i < count && filled < totalBits; i++) { - bits[filled++] = val; - } - } + // Decode row-major horizontal bitmap + const bits = decodeRLE(br, w, h, hdr.m0, hdr.m1); - // Convert row-major bitmap → column bytes (bit 0 = top row) to match - // the builtin font layout consumed by rasterizeText. - const cols = new Uint8Array(w); - for (let row = 0; row < h; row++) { - for (let col = 0; col < w; col++) { - if (bits[row * w + col]) { - // ts-ignore - cols[col] |= 1 << row; - } - } + // Convert row-major → column bytes (bit 0 = top row) for rasterizeText + const cols = new Uint8Array(w); + for (let row = 0; row < h; row++) { + for (let col = 0; col < w; col++) { + if (bits[row * w + col]) cols[col] |= (1 << row); } + } - // yBrg is measured upward from baseline in U8G2 conventions. - // Convert to a downward pixel offset relative to the top of GLYPH_H. - // ascentA = pixels above baseline for capital 'A' - // yOff = (ascentA - yBrg - h): rows to push down from the top of - // the glyph box so the baseline lands in the right place. - const yOff = Math.max(0, hdr.ascentA - yBrg - h); + // yB: signed distance from baseline upward to bottom of glyph bounding box. + // ascentA: pixels above baseline for cap-height reference. + // yOff: distance downward from top of the notional box to top of this glyph. + const yOff = hdr.ascentA - yB - h; - return { cols, w, h, yOff }; - } catch { - return null; - } + return { cols, w, h, dx, yOff }; } -/** - * Scan the correct block of a U8G2 font for `cp` and return decoded glyph - * data, or null if not found. - * - * Each glyph record in the jump table starts with: - * [1 byte] encoding (codepoint, 0x00–0xFF in ASCII blocks) - * [1 byte] jump offset to next record (0 = end of block) - * then the bitstream described in decodeGlyphAt. - * - * For the Unicode block the encoding is 2 bytes (uint16 LE) and the jump - * offset is also 2 bytes (uint16 LE). - */ +// --------------------------------------------------------------------------- +// Glyph search +// +// Record layout (cp < 0x0100): +// [1 byte] encoding +// [1 byte] jump = byte distance from START of this record to next record +// [bitstream …] +// +// Record layout (cp >= 0x0100): +// [2 bytes BE] encoding +// [1 byte] jump (same semantics) +// [bitstream …] +// +// Unicode section is preceded by a lookup table (added in v2.23): +// pairs of [uint16 BE delta | uint16 BE last-encoding], terminated by last==0xFFFF +// delta is cumulative byte offset from TABLE START to the glyph sub-block. +// --------------------------------------------------------------------------- export function u8g2Glyph(cp: number, font: Uint8Array): U8G2GlyphData | null { - if (font.length < 23) return null; - + if (font.length < HDR_SIZE) return null; const hdr = parseHeader(font); - let blockStart: number; - let wide: boolean; // true for the Unicode block (2-byte encoding + 2-byte jump) - - if (cp >= 0x20 && cp <= 0x5F) { - blockStart = hdr.startUpper; - wide = false; - } else if (cp >= 0x60 && cp <= 0x9F) { - blockStart = hdr.startLower; - wide = false; - } else { - blockStart = hdr.startUnicode; - wide = true; - } - - if (blockStart === 0 || blockStart >= font.length) return null; - - let pos = blockStart; + try { + if (cp >= 0x0100) { + // ── Unicode block ──────────────────────────────────────────────────── + const tableBase = hdr.startUnicode; + if (tableBase + 4 > font.length) return null; + + // Walk the jump table (BE uint16 pairs) to find the right sub-block. + // delta is offset from tableBase to start of sub-block. + let glyphStart = tableBase; + let pos = tableBase; + while (pos + 4 <= font.length) { + const delta = readU16BE(font, pos); + const lastEncoding = readU16BE(font, pos + 2); + pos += 4; + if (lastEncoding === 0xFFFF) { + glyphStart = tableBase + delta; + break; + } + if (lastEncoding >= cp) { + glyphStart = tableBase + delta; + break; + } + } - while (pos < font.length) { - let encoding: number; - let jump: number; + // Scan glyph records in the sub-block + let recPos = glyphStart; + while (recPos + 3 <= font.length) { + const recStart = recPos; + const encoding = readU16BE(font, recPos); + const jump = font[recPos + 2]!; + if (jump === 0) break; + recPos += 3; // bitstream starts here + if (encoding === cp) return decodeGlyphAt(font, recPos, hdr); + recPos = recStart + jump; + } + return null; - if (wide) { - if (pos + 4 > font.length) break; - encoding = (font[pos]! | (font[pos + 1]! << 8)) >>> 0; // LE uint16 - jump = (font[pos + 2]! | (font[pos + 3]! << 8)) >>> 0; - pos += 4; } else { - if (pos + 2 > font.length) break; - encoding = font[pos]!; - jump = font[pos + 1]!; - pos += 2; - } - - if (jump === 0) break; // end-of-block sentinel - - if (encoding === cp) { - return decodeGlyphAt(font, pos, hdr); + // ── ASCII block (cp < 0x0100) ──────────────────────────────────────── + // Jump-start from the nearest block offset to avoid scanning from byte 0. + let recPos: number; + if (cp >= 0x61) recPos = hdr.startLower; + else if (cp >= 0x41) recPos = hdr.startUpper; + else recPos = HDR_SIZE; + + while (recPos + 2 <= font.length) { + const recStart = recPos; + const encoding = font[recPos]!; + const jump = font[recPos + 1]!; + if (jump === 0) break; + recPos += 2; // bitstream starts here + if (encoding === cp) return decodeGlyphAt(font, recPos, hdr); + recPos = recStart + jump; + } + return null; } - - pos += jump; // skip to next record + } catch { + return null; } - - return null; // codepoint not in font } // --------------------------------------------------------------------------- -// Text → pixel-image rasteriser (as specified) +// Text → pixel-image rasteriser // --------------------------------------------------------------------------- export function rasterizeText( text: string, cellHeight: number, - font?: MonoDisplayFont, + font: { FONT_DATA: Uint8Array }, ): { pixels: Uint8Array; width: number; height: number } { - const glyphs: Array<{ cols: Uint8Array; w: number; h: number; yOff: number }> = []; + const hdr = parseHeader(font.FONT_DATA); + const glyphs: Array = []; for (const char of text) { const cp = char.codePointAt(0) ?? 0x3F; - const custom = font ? u8g2Glyph(cp, font.FONT_DATA) : null; - if (custom) { - glyphs.push({ cols: custom.cols, w: custom.w, h: custom.h, yOff: custom.yOff }); - } else { - glyphs.push({ cols: builtinGlyph(cp), w: GLYPH_W, h: GLYPH_H, yOff: 0 }); - } + const g = u8g2Glyph(cp, font.FONT_DATA); + if (g) glyphs.push(g); + } + + if (glyphs.length === 0) { + return { pixels: new Uint8Array(cellHeight), width: 1, height: cellHeight }; } - const totalW = Math.max(1, glyphs.length * GLYPH_ADVANCE - 1); + // Total width: sum all advances, but last glyph uses its actual pixel width + const totalW = glyphs.reduce((sum, g, i) => + sum + (i < glyphs.length - 1 ? g.dx : g.w), 0); + const pixels = new Uint8Array(totalW * cellHeight); - const yBase = Math.max(0, Math.floor((cellHeight - GLYPH_H) / 2)); - let x = 0; + // Centre the full font (ascent + |descent|) vertically in the cell. + // header byte 14 is descent of 'g' — stored as a negative signed value. + const descent = font.FONT_DATA[14]! > 127 ? font.FONT_DATA[14]! - 256 : font.FONT_DATA[14]!; + const fontH = hdr.ascentA - descent; // total font height in pixels + const topPad = Math.floor((cellHeight - fontH) / 2); + const baseline = topPad + hdr.ascentA; + let x = 0; for (const g of glyphs) { - const top = yBase + g.yOff; + // yOff = ascentA - yBearing - h (may be negative if glyph taller than 'A') + const top = baseline - hdr.ascentA + g.yOff; for (let col = 0; col < g.w; col++) { const colByte = g.cols[col] ?? 0; for (let row = 0; row < g.h; row++) { @@ -404,7 +286,7 @@ export function rasterizeText( } } } - x += GLYPH_ADVANCE; + x += g.dx; } return { pixels, width: totalW, height: cellHeight }; diff --git a/ts/src/renderer.ts b/ts/src/renderer.ts index ba79346..b329ad0 100644 --- a/ts/src/renderer.ts +++ b/ts/src/renderer.ts @@ -16,7 +16,7 @@ import { type MonoFormatVScroll } from "./types.js"; import { type MonoDisplayDriverOptions } from "./driver"; -import { rasterizeText, MonoDisplayFont } from "./font"; +import { rasterizeText, MonoDisplayFont, u8g2_font_5x7_tf } from "./font"; /** * Renders a MonoFormatFile onto an HTMLCanvasElement. * Uses setInterval at 1000/fps ms per tick. All element state (animation @@ -46,7 +46,7 @@ export class MonoDisplayRenderer { this.ctx = ctx; this.opts = opts; this.builtinFonts = [ - + u8g2_font_5x7_tf, ]; } diff --git a/ts/src/types.ts b/ts/src/types.ts index ba6a322..0ca2cf4 100644 --- a/ts/src/types.ts +++ b/ts/src/types.ts @@ -11,27 +11,27 @@ export enum FileVersion { /** Illegal - must not appear in a valid file */ Illegal = 0, /** Current specification version */ - V1 = 1, + V1 = 1, } export enum SectionType { /** List of drawn elements - always shown */ - ElementsAlways = 1, + ElementsAlways = 1, /** List of drawn elements - shown only within a POSIX timestamp window */ ElementsTimespan = 2, /** U8G2-format custom font used by draw elements in this file */ - CustomFont = 32, + CustomFont = 32, } export enum ElementType { - Image2D = 1, - Animation = 2, + Image2D = 1, + Animation = 2, HorizontalScroll = 3, - VerticalScroll = 4, - Line = 5, - ClippedText = 16, - HScrollText = 17, - CurrentTime = 18, + VerticalScroll = 4, + Line = 5, + ClippedText = 16, + HScrollText = 17, + CurrentTime = 32, } // --------------------------------------------------------------------------- @@ -41,9 +41,9 @@ export enum ElementType { /** Bit-field flags for ElementsAlways / ElementsTimespan sections. */ export interface SectionFlags { /** Bit 0 - render elements onto the front-side buffer. */ - drawFront?: boolean; + drawFront?: boolean; /** Bit 1 - render elements onto the back-side buffer. */ - drawBack?: boolean; + drawBack?: boolean; /** Bit 2 - clear targeted buffer(s) before drawing this section's elements. */ clearBuffer?: boolean; // Bits 3-15: reserved; writers MUST write 0. @@ -55,13 +55,13 @@ export interface SectionFlags { export interface ScrollElementFlags { /** Bit 0 - wrap content endlessly. */ - endless?: boolean; + endless?: boolean; /** Bit 1 - reverse scroll direction. */ invertDirection?: boolean; /** Bit 2 - pad at start edge (left for H-scroll, top for V-scroll). */ - padStart?: boolean; + padStart?: boolean; /** Bit 3 - pad at end edge (right for H-scroll, bottom for V-scroll). */ - padEnd?: boolean; + padEnd?: boolean; // Bits 4-7: reserved. } @@ -71,9 +71,9 @@ export interface ScrollElementFlags { export interface CurrentTimeFlags { /** Bit 0 - use 12-hour clock (default 24-hour). */ - clock12h?: boolean; + clock12h?: boolean; /** Bit 1 - show hours. */ - showHours?: boolean; + showHours?: boolean; /** Bit 2 - show minutes. */ showMinutes?: boolean; /** Bit 3 - show seconds. */ @@ -123,11 +123,11 @@ export function customFontOrdinal(fontIndex: FontIndex): number { // --------------------------------------------------------------------------- export interface Image2DElement { - type: ElementType.Image2D; + type: ElementType.Image2D; /** Row-major, 1 byte/pixel (0 = off, 1 = on). Packed to 1bpp on write. */ - pixels: Uint8Array; - width: number; - height: number; + pixels: Uint8Array; + width: number; + height: number; xOffset?: number; // default 0 yOffset?: number; // default 0 } @@ -138,10 +138,10 @@ export interface AnimationFrameDescriptor { } export interface AnimationElement { - type: ElementType.Animation; - width: number; - height: number; - frames: AnimationFrameDescriptor[]; + type: ElementType.Animation; + width: number; + height: number; + frames: AnimationFrameDescriptor[]; /** * Advance frame every (updateInterval + 1) ticks. * 0 = every tick, 1 = every 2nd tick, ..., 65535 = every 65536th tick. @@ -153,86 +153,86 @@ export interface AnimationElement { } export interface HScrollElement { - type: ElementType.HorizontalScroll; + type: ElementType.HorizontalScroll; /** Viewport width in pixels. */ - width: number; + width: number; /** Viewport height in pixels. */ - height: number; + height: number; /** Scrolling content pixels (contentWidth × height), 1 byte/pixel. */ - pixels: Uint8Array; + pixels: Uint8Array; contentWidth: number; /** * Scroll speed byte SS; moves (SS + 1) / 16 pixels per tick. * Range 0-255. Default 0 (= 1/16 px/tick). */ scrollSpeed?: number; - flags?: ScrollElementFlags; + flags?: ScrollElementFlags; xOffset?: number; yOffset?: number; } export interface VScrollElement { - type: ElementType.VerticalScroll; + type: ElementType.VerticalScroll; /** Viewport width in pixels. */ - width: number; + width: number; /** Viewport height in pixels. */ - height: number; + height: number; /** Scrolling content pixels (width × contentHeight), 1 byte/pixel. */ - pixels: Uint8Array; + pixels: Uint8Array; contentHeight: number; /** Scroll speed byte SS; moves (SS + 1) / 16 pixels per tick. Default 0. */ - scrollSpeed?: number; - flags?: ScrollElementFlags; + scrollSpeed?: number; + flags?: ScrollElementFlags; xOffset?: number; yOffset?: number; } export interface LineElement { - type: ElementType.Line; - xOrigin: number; - yOrigin: number; - xTarget: number; - yTarget: number; + type: ElementType.Line; + xOrigin: number; + yOrigin: number; + xTarget: number; + yTarget: number; /** Reserved by spec; writers MUST write 0. */ - lineStyle?: number; + lineStyle?: number; invertPixels?: boolean; // flags bit 0 } export interface ClippedTextElement { - type: ElementType.ClippedText; - text: string; // UTF-8 - width: number; - height: number; + type: ElementType.ClippedText; + text: string; // UTF-8 + width: number; + height: number; /** 0-32767 = built-in font; 0x8000+ = custom font in file. Default 0. */ fontIndex?: FontIndex; - xOffset?: number; - yOffset?: number; + xOffset?: number; + yOffset?: number; } export interface HScrollTextElement { - type: ElementType.HScrollText; - text: string; // UTF-8 - width: number; - height: number; + type: ElementType.HScrollText; + text: string; // UTF-8 + width: number; + height: number; scrollSpeed?: number; // SS byte, default 0 - fontIndex?: FontIndex; - flags?: ScrollElementFlags; + fontIndex?: FontIndex; + flags?: ScrollElementFlags; xOffset?: number; yOffset?: number; } export interface CurrentTimeElement { type: ElementType.CurrentTime; - width: number; + width: number; height: number; /** 0-32767 = built-in font; 0x8000+ = custom font in file. Default 0. */ - fontIndex?: FontIndex; + fontIndex?: FontIndex; /** * UTC offset in whole minutes (signed 16-bit integer). * e.g. UTC+5:30 → 330, UTC-8 → -480. */ utcOffsetMinutes?: number; - flags?: CurrentTimeFlags; + flags?: CurrentTimeFlags; xOffset?: number; yOffset?: number; } @@ -252,17 +252,17 @@ export type DrawElement = // --------------------------------------------------------------------------- export interface ElementsAlwaysDescriptor { - flags?: SectionFlags; + flags?: SectionFlags; elements: DrawElement[]; } export interface ElementsTimespanDescriptor { - flags?: SectionFlags; - elements: DrawElement[]; + flags?: SectionFlags; + elements: DrawElement[]; /** POSIX timestamp (seconds) - section visible when now >= startTimestamp. */ - startTimestamp: bigint; + startTimestamp: bigint; /** POSIX timestamp (seconds) - section visible when now < endTimestamp. */ - endTimestamp: bigint; + endTimestamp: bigint; } export interface CustomFontDescriptor { @@ -293,107 +293,107 @@ export interface MonoDisplayFileDescriptor { export interface MonoFormatPixelImage { /** Unpacked: 1 byte per pixel, 0 = off, 1 = on, row-major. */ pixels: Uint8Array; - width: number; + width: number; height: number; } export interface MonoFormatImage2D { - type: ElementType.Image2D; + type: ElementType.Image2D; xOffset: number; yOffset: number; - image: MonoFormatPixelImage; + image: MonoFormatPixelImage; } export interface MonoFormatAnimation { - type: ElementType.Animation; - xOffset: number; - yOffset: number; - width: number; - height: number; + type: ElementType.Animation; + xOffset: number; + yOffset: number; + width: number; + height: number; updateInterval: number; - frames: MonoFormatPixelImage[]; + frames: MonoFormatPixelImage[]; } export interface MonoFormatScrollFlags { - endless: boolean; + endless: boolean; invertDirection: boolean; - padStart: boolean; - padEnd: boolean; + padStart: boolean; + padEnd: boolean; } export interface MonoFormatHScroll { - type: ElementType.HorizontalScroll; - xOffset: number; - yOffset: number; - width: number; - height: number; + type: ElementType.HorizontalScroll; + xOffset: number; + yOffset: number; + width: number; + height: number; contentWidth: number; - scrollSpeed: number; - flags: MonoFormatScrollFlags; - content: MonoFormatPixelImage; + scrollSpeed: number; + flags: MonoFormatScrollFlags; + content: MonoFormatPixelImage; } export interface MonoFormatVScroll { - type: ElementType.VerticalScroll; - xOffset: number; - yOffset: number; - width: number; - height: number; + type: ElementType.VerticalScroll; + xOffset: number; + yOffset: number; + width: number; + height: number; contentHeight: number; - scrollSpeed: number; - flags: MonoFormatScrollFlags; - content: MonoFormatPixelImage; + scrollSpeed: number; + flags: MonoFormatScrollFlags; + content: MonoFormatPixelImage; } export interface MonoFormatLine { - type: ElementType.Line; - xOrigin: number; - yOrigin: number; - xTarget: number; - yTarget: number; - lineStyle: number; + type: ElementType.Line; + xOrigin: number; + yOrigin: number; + xTarget: number; + yTarget: number; + lineStyle: number; invertPixels: boolean; } export interface MonoFormatClippedText { - type: ElementType.ClippedText; - xOffset: number; - yOffset: number; - width: number; - height: number; + type: ElementType.ClippedText; + xOffset: number; + yOffset: number; + width: number; + height: number; fontIndex: FontIndex; - text: string; + text: string; } export interface MonoFormatHScrollText { - type: ElementType.HScrollText; - xOffset: number; - yOffset: number; - width: number; - height: number; + type: ElementType.HScrollText; + xOffset: number; + yOffset: number; + width: number; + height: number; scrollSpeed: number; - fontIndex: FontIndex; - flags: MonoFormatScrollFlags; - text: string; + fontIndex: FontIndex; + flags: MonoFormatScrollFlags; + text: string; } export interface MonoFormatCurrentTimeFlags { - clock12h: boolean; - showHours: boolean; + clock12h: boolean; + showHours: boolean; showMinutes: boolean; showSeconds: boolean; } export interface MonoFormatCurrentTime { - type: ElementType.CurrentTime; - xOffset: number; - yOffset: number; - width: number; - height: number; - fontIndex: FontIndex; + type: ElementType.CurrentTime; + xOffset: number; + yOffset: number; + width: number; + height: number; + fontIndex: FontIndex; /** UTC offset in minutes (signed). */ utcOffsetMinutes: number; - flags: MonoFormatCurrentTimeFlags; + flags: MonoFormatCurrentTimeFlags; } export type MonoFormatElement = @@ -407,29 +407,29 @@ export type MonoFormatElement = | MonoFormatCurrentTime; export interface MonoFormatSectionFlags { - drawFront: boolean; - drawBack: boolean; + drawFront: boolean; + drawBack: boolean; clearBuffer: boolean; } export interface MonoFormatElementsAlways { sectionType: SectionType.ElementsAlways; - flags: MonoFormatSectionFlags; - elements: MonoFormatElement[]; + flags: MonoFormatSectionFlags; + elements: MonoFormatElement[]; } export interface MonoFormatElementsTimespan { - sectionType: SectionType.ElementsTimespan; - flags: MonoFormatSectionFlags; + sectionType: SectionType.ElementsTimespan; + flags: MonoFormatSectionFlags; startTimestamp: bigint; // POSIX seconds - endTimestamp: bigint; - elements: MonoFormatElement[]; + endTimestamp: bigint; + elements: MonoFormatElement[]; } export interface MonoFormatCustomFont { sectionType: SectionType.CustomFont; /** Raw U8G2 font data (actual bytes, not padded). */ - fontData: Uint8Array; + fontData: Uint8Array; } export type MonoFormatSection =