Compare commits

...

2 Commits

Author SHA1 Message Date
AdaSzi
fe2dcd22eb
Add LILYGO T5 4.7 Inch E-Paper S3 (#668) 2026-10-04 21:58:28 +02:00
Ken Van Hoeylandt
7ea8c346b0
Various improvements (#673)
Graphics & rendering

- Implement `binfont-module` to generate bitmap fonts on the device itself
- Remove lodepng support from LVGL library and create `Modules/lodepng-module` to make the feature re-usable in non-LVGL apps too. Add binding functions for LVGL support.
- Implement large icons for boot screen (Tactility and USB logos)
- Remove unused asset files in `Data/`

Boot

- Split up `Tactility.cpp` and its `run()` function into various `Init*.cpp` files.
- Re-implement `Boot.cpp` app as non-LVGL app
- Boot app now has a `BootSequence.cpp` for the init code.
- LVGL is now started when the Boot app finishes
- Move more init logic into Boot app

BLE

- Don't allocate scan result buffers unless BLE is on
- Allocate scan results in external memory if possible

UX & UI

- USB Settings app: remove border from the button container
2026-10-04 18:09:21 +02:00
166 changed files with 21733 additions and 16200 deletions

View File

@ -36,7 +36,7 @@ CONFIG_LV_USE_FS_STDIO=y
CONFIG_LV_FS_STDIO_LETTER=65
CONFIG_LV_FS_STDIO_PATH=""
CONFIG_LV_FS_STDIO_CACHE_SIZE=4096
CONFIG_LV_USE_LODEPNG=y
CONFIG_LV_USE_LODEPNG=n
CONFIG_LV_USE_BUILTIN_MALLOC=n
# Routes lv_malloc/realloc/free through Modules/lvgl-module/source/lv_mem_custom.c, which prefers
# PSRAM (falls back to internal RAM automatically) instead of a fixed-size pool or plain malloc.

View File

@ -170,6 +170,8 @@ if (NOT DEFINED ENV{ESP_IDF_VERSION})
add_subdirectory(Libraries/minmea)
add_subdirectory(Modules/lvgl-module)
add_subdirectory(Modules/audio-decoder-module)
add_subdirectory(Modules/binfont-module)
add_subdirectory(Modules/lodepng-module)
add_subdirectory(Modules/c-symbols-module)
add_subdirectory(Modules/cjson-module)
add_subdirectory(Modules/cpp-symbols-module)

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

View File

@ -0,0 +1,69 @@
add e145
app_registration ef40
apps e5c3
area_chart e770
battery_android_frame_1 f257
battery_android_frame_2 f256
battery_android_frame_3 f255
battery_android_frame_4 f254
battery_android_frame_5 f253
battery_android_frame_6 f252
battery_android_frame_bolt f250
battery_android_frame_full f24f
bluetooth e1a7
bluetooth_connected e1a8
bluetooth_disabled e1a9
bluetooth_searching e60f
cable efe6
calendar_month ebcc
camera e3af
check e668
circle ef4a
close e5cd
cloud f15c
delete e92e
deployed_code f720
devices e326
display_settings eb97
download f090
edit_note e745
electric_bolt ec1c
folder e2c7
forum e8af
gamepad e30f
help e8fd
hub e9f4
image e3f4
keyboard_alt f028
keyboard_arrow_up e316
language ea07
lightbulb e90f
lightstrip fff75
lists e9b9
location_on f1db
logo_dev ead6
mail e159
memory e322
menu e5d2
mop e28d
more_vert e5d4
music_note e405
navigation e55d
network_wifi_1_bar ebe4
network_wifi_2_bar ebd6
network_wifi_3_bar ebe1
note_add e89c
power_settings_new f8c7
refresh e5d5
sd_card e623
sd_card_alert f057
search ef7a
settings e8b8
signal_wifi_0_bar f0b0
signal_wifi_4_bar f065
signal_wifi_bad f064
signal_wifi_off e1da
terminal eb8e
toolbar e9f7
usb e1e0
wifi e63e

Binary file not shown.

View File

@ -1,306 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="512"
height="512"
viewBox="0 0 512 512"
version="1.1"
id="svg11"
sodipodi:docname="Desktop Icons.svg"
xml:space="preserve"
inkscape:export-filename="../assets/app_icon_i2c.png"
inkscape:export-xdpi="3"
inkscape:export-ydpi="3"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><style
id="style1"><![CDATA[
#keylines circle, #keylines rect {
vector-effect: non-scaling-stroke;
fill: none;
stroke: #0000FF;
}
#mask path, #mask rect, #mask circle, #hatching path {
vector-effect: non-scaling-stroke;
fill: none;
stroke: #FF0000;
}
#keylines, #mask {
opacity: 0.5;
}
]]></style><inkscape:_templateinfo><inkscape:_name>Android O icon template</inkscape:_name><inkscape:author>Venceslas DUET</inkscape:author><inkscape:_shortdesc>A collection of keylines and guides used to make adaptive icons for android O</inkscape:_shortdesc><inkscape:date>2021-02-14</inkscape:date><inkscape:_keywords>Android Icon Adaptive Keylines</inkscape:_keywords></inkscape:_templateinfo><defs
id="defs1"><inkscape:path-effect
effect="bspline"
id="path-effect3"
is_visible="true"
lpeversion="1.3"
weight="33.333333"
steps="2"
helper_size="0"
apply_no_weight="true"
apply_with_weight="true"
only_selected="false"
uniform="false" /><clipPath
clipPathUnits="userSpaceOnUse"
id="clipHatching"><rect
width="8"
height="8"
x="0"
y="0"
id="rect1" /></clipPath><pattern
patternUnits="userSpaceOnUse"
width="8"
height="8"
id="hatching"><path
d="M 1,-1 -1,1 M 9,7 7,9 M 9,-1 -1,9"
clip-path="url(#clipHatching)"
id="path1" /></pattern><clipPath
clipPathUnits="userSpaceOnUse"
id="clipHatching-2"><rect
width="8"
height="8"
x="0"
y="0"
id="rect1-7" /></clipPath><pattern
patternUnits="userSpaceOnUse"
width="8"
height="8"
id="hatching-0"><path
d="M 1,-1 -1,1 M 9,7 7,9 M 9,-1 -1,9"
clip-path="url(#clipHatching-2)"
id="path1-9" /></pattern><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath17"><rect
width="512"
height="512"
x="0.0081919599"
y="0.01246433"
ry="128"
id="rect17"
style="display:inline" /></clipPath><filter
style="color-interpolation-filters:sRGB;"
inkscape:label="Drop Shadow"
id="filter41"
x="-0.04317569"
y="-0.040927102"
width="1.1013429"
height="1.096065"><feFlood
result="flood"
in="SourceGraphic"
flood-opacity="0.29915108267716534"
flood-color="rgb(0,0,0)"
id="feFlood40" /><feGaussianBlur
result="blur"
in="SourceGraphic"
stdDeviation="6"
id="feGaussianBlur40" /><feOffset
result="offset"
in="blur"
dx="5"
dy="5"
id="feOffset40" /><feComposite
result="comp1"
operator="in"
in="flood"
in2="offset"
id="feComposite40" /><feComposite
result="comp2"
operator="over"
in="SourceGraphic"
in2="comp1"
id="feComposite41" /></filter></defs><sodipodi:namedview
id="base"
inkscape:current-layer="g6"
showgrid="true"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:document-units="px"
inkscape:cx="211.14908"
inkscape:cy="264.49672"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.1153257"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"><inkscape:grid
type="xygrid"
empspacing="8"
id="grid1"
visible="true"
originx="0"
originy="0"
spacingy="1"
spacingx="1"
units="px" /><sodipodi:guide
position="256,256"
orientation="0,1"
inkscape:locked="false"
id="guide1" /><sodipodi:guide
position="256,200"
orientation="0,1"
inkscape:locked="false"
id="guide2" /><sodipodi:guide
position="256,312"
orientation="0,1"
inkscape:locked="false"
id="guide3" /><sodipodi:guide
position="256,256"
orientation="1,0"
inkscape:locked="false"
id="guide4" /><sodipodi:guide
position="200,256"
orientation="1,0"
inkscape:locked="false"
id="guide5" /><sodipodi:guide
position="312,256"
orientation="1,0"
inkscape:locked="false"
id="guide6" /><sodipodi:guide
position="256,256"
orientation="1,1"
inkscape:locked="false"
id="guide7" /><sodipodi:guide
position="256,256"
orientation="1,-1"
inkscape:locked="false"
id="guide8" /></sodipodi:namedview><rect
style="display:none;opacity:1;fill:#9e9e9e;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-opacity:1;paint-order:stroke fill markers"
id="Background"
width="512"
height="512"
x="0.02173974"
y="0.030385898"
clip-path="url(#clipPath17)" /><g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Icons"><path
d="m 212.48429,510.5 h 84.3951 l 18.4617,-74.4906 c 12.30779,-4.1402 23.95604,-9.07563 34.9447,-14.80628 10.98912,-5.73277 21.31888,-13.00885 30.98932,-21.82812 l 72.527,21.16643 L 496,347.78125 441.93403,296.82377 c 1.75966,-11.88934 3.07831,-25.11001 3.95602,-39.66212 0.8777,-14.55209 -0.003,-27.56091 -2.63946,-39.02647 l 52.74749,-50.2704 -42.19787,-72.760837 -71.20854,19.843727 C 372.92125,106.1284 362.3718,98.631997 350.94345,92.458553 339.51504,86.284646 327.64696,80.99297 315.33917,76.583523 L 298.19662,2.5 H 212.4824 l -15.82412,74.083523 c -12.30736,4.409447 -24.17545,9.701123 -35.60426,15.87503 -11.42839,6.173444 -21.97781,13.669847 -31.64825,22.489117 L 58.197238,93.781248 16,167.86478 l 54.0653,50.2704 c -2.637344,12.34739 -3.956016,25.35618 -3.956016,39.02647 0,13.66984 1.318672,26.67847 3.956016,39.02583 L 16,347.78125 58.197238,421.86476 130.72486,399.375 c 9.67003,7.93749 19.9996,14.99297 30.9887,21.16643 10.98911,6.17392 22.63736,11.46556 34.94472,15.87501 z m 43.2372,-158.75 c -26.18744,0 -48.51191,-9.35395 -66.97336,-28.06191 -18.46149,-18.70839 -27.69222,-41.19798 -27.69222,-67.46879 0,-26.27119 9.32399,-48.66724 27.97199,-67.18805 18.64883,-18.52082 41.06634,-27.78125 67.25251,-27.78125 26.18701,0 48.51126,9.35395 66.97272,28.0619 18.4615,18.70836 27.69223,41.19795 27.69223,67.46876 0,26.27122 -9.32399,48.66722 -27.97198,67.18809 C 304.32494,342.48958 281.90766,351.75 255.72149,351.75 Z"
id="Settings"
style="display:none;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1.3208;stroke-opacity:1"
sodipodi:nodetypes="ccccccccsccccccccccccccscccccccscscscscs" /><path
d="M 51.047467,512 Q 28.952485,512 14.476063,497.52393 0,483.0475 0,460.95253 V 51.047462 Q 0,28.190328 14.476063,14.095334 28.952485,0 51.047467,0 H 460.95253 Q 483.80967,0 497.90466,14.095334 512,28.190328 512,51.047462 V 460.95253 q 0,22.09497 -14.09534,36.5714 Q 483.80967,512 460.95253,512 Z M 54.857141,127.99998 H 457.14285 V 54.857136 H 54.857141 Z M 457.14285,182.85714 H 54.857141 V 457.14285 H 457.14285 Z M 54.857141,127.99998 v 54.85716 z m 0,0 V 54.857136 Z m 0,54.85716 v 274.28571 z"
id="Fallback"
style="display:none;fill:#333333;fill-opacity:1;stroke-width:0.761904" /><g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="System Info"
style="display:none"><path
d="m 207.3461,368.61149 c 4.24475,0 8.2964,-1.10767 12.15495,-3.323 3.85898,-2.21507 6.79308,-5.34536 8.80231,-9.39085 l 74.74094,-136.81458 26.5827,49.17833 c 1.7715,4.4304 4.54058,7.64247 8.30725,9.63617 3.76584,1.9937 8.08555,2.99055 12.95914,2.99055 H 510.3904 V 233.03904 H 365.51384 L 324.31067,157.9424 c -2.21531,-4.43039 -5.25245,-7.64244 -9.11143,-9.63616 -3.85853,-1.99263 -7.91018,-2.98895 -12.15494,-2.98895 -4.24475,0 -8.2964,0.99632 -12.15496,2.98895 -3.85897,1.99266 -6.8961,5.20471 -9.11139,9.63616 L 207.3461,294.84409 180.49221,245.81347 c -2.03515,-4.0859 -4.9819,-7.23639 -8.84026,-9.45148 -3.85856,-2.2153 -7.9102,-3.32295 -12.15494,-3.32295 H 0 v 47.84907 h 144.87656 l 41.5177,75.09183 c 2.00541,3.99063 4.93771,7.09348 8.7969,9.30855 3.85853,2.21533 7.91018,3.323 12.15494,3.323 z"
style="fill:#0d00be;fill-opacity:1;stroke-width:0.664569"
id="path1-8" /><path
d="m 0.48281402,192.83596 v -79.74848 c 0,-14.177585 4.99842408,-25.696778 14.99527098,-34.557575 C 25.47472,69.668868 37.492176,65.238358 51.530467,65.238358 H 459.91073 c 14.03785,0 26.04106,4.43051 36.00962,13.291547 9.96858,8.860797 14.95286,20.37999 14.95286,34.557575 v 79.74848 H 463.02408 V 113.08748 H 48.331914 v 79.74848 z M 51.445288,448.03116 c -14.037857,0 -26.041069,-4.43051 -36.009631,-13.29154 -9.9685626,-8.8608 -14.95284298,-20.38 -14.95284298,-34.55758 V 320.43356 H 48.331914 v 79.74848 H 463.02408 v -79.74848 h 47.84913 v 79.74848 c 0,14.17758 -4.99842,25.69678 -14.99528,34.55758 -9.99707,8.86103 -22.01453,13.29154 -36.0524,13.29154 z"
id="System_Info"
style="display:inline;fill:#333333;fill-opacity:1;stroke-width:0.664569"
sodipodi:nodetypes="cscsscsccccccscsccccccssss" /></g><g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="Power_Settings"
style="display:none"><rect
style="display:inline;fill:#000000;fill-opacity:1;stroke-width:0.8862;paint-order:stroke fill markers"
id="rect4"
width="346.68829"
height="197.63705"
x="65.055527"
y="164.69754"
inkscape:label="Power_at_100" /><rect
style="display:none;fill:#000000;fill-opacity:1;stroke-width:0.8862;paint-order:stroke fill markers"
id="rect16"
width="277"
height="197.63705"
x="65.055527"
y="164.69754"
inkscape:label="Power_at_080" /><rect
style="display:none;fill:#000000;fill-opacity:1;stroke-width:0.8862;paint-order:stroke fill markers"
id="rect18"
width="208"
height="197.63705"
x="65.055527"
y="164.69754"
inkscape:label="Power_at_060" /><rect
style="display:none;fill:#000000;fill-opacity:1;stroke-width:0.8862;paint-order:stroke fill markers"
id="rect19"
width="139"
height="197.63705"
x="65.055527"
y="164.69754"
inkscape:label="Power_at_040" /><rect
style="display:none;fill:#000000;fill-opacity:1;stroke-width:0.8862;paint-order:stroke fill markers"
id="rect20"
width="70"
height="197.63705"
x="65.055527"
y="164.69754"
inkscape:label="Power_at_020" /><g
id="g3"
inkscape:label="Shared"
transform="translate(0.41209841,0.82986943)"><path
id="rect15"
style="fill:#000000;paint-order:stroke fill markers"
d="M 1.6464844,104.58203 V 417.50781 H 475.15234 V 104.58203 Z M 23.880859,125.99414 H 451.27148 v 269.2793 H 23.880859 Z"
transform="translate(-0.41209841,-0.82986943)" /><rect
style="display:inline;fill:#000000;paint-order:stroke fill markers"
id="rect2-7"
width="39.527409"
height="113.6413"
x="471.03497"
y="201.28403" /></g></g><path
d="m 128.42457,319.70188 h 31.96224 V 223.81515 H 128.42457 V 255.7774 H 96.462329 v 31.96225 h 31.962241 z m 63.92449,-31.96223 H 416.08475 V 255.7774 H 192.34906 Z m 159.81121,-63.9245 h 31.96224 v -31.96223 h 31.96224 V 159.89067 H 384.12251 V 127.92843 H 352.16027 Z M 96.462329,191.85292 H 320.19802 V 159.89067 H 96.462329 Z m 63.924481,287.66019 v -63.9245 H 48.518961 q -19.776638,0 -33.860161,-14.09119 Q 0.575596,387.40591 0.575596,367.61871 V 79.796797 q 0,-19.788137 14.083204,-33.77162 Q 28.742323,32.041695 48.518961,32.041695 H 464.02812 q 19.77663,0 33.85984,14.091514 14.08352,14.091196 14.08352,33.878376 V 367.83383 q 0,19.78783 -14.08352,33.7713 -14.08321,13.98348 -33.85984,13.98348 H 352.16027 v 63.9245 z M 48.518961,367.64525 H 464.02812 V 79.98506 H 48.518961 Z m 0,0 V 79.98506 Z"
id="Display_Settings"
style="display:none;fill:#333333;fill-opacity:1;stroke-width:0.665879" /><path
d="m 47.867683,447.55649 q -19.326766,0 -33.654939,-14.32849 Q -0.1157482,418.89983 -0.1157482,399.57306 V 111.67248 q 0,-19.793166 14.3284922,-33.888136 Q 28.540917,63.689052 47.867683,63.689052 H 191.81797 l 63.97791,63.977908 h 207.9282 q 19.79316,0 33.88814,14.09529 14.09529,14.09497 14.09529,33.88814 v 223.92267 q 0,19.32677 -14.09529,33.65494 -14.09498,14.32849 -33.88814,14.32849 z m 0,-47.98343 H 463.72408 V 175.65039 h -227.9213 l -63.9779,-63.97791 H 47.867683 Z m 0,0 V 111.67248 Z"
id="Files"
style="display:none;fill:#333333;fill-opacity:1;stroke-width:0.666435" /><g
id="g6"
inkscape:label="I2C"><path
style="fill:#333333;stroke-width:1.10895;paint-order:stroke fill markers"
d="M 1.1034012,386.61813 V 121.14457 H 512.16486 V 388.6067 H 406.77087 v -75.56551 h 27.83993 v 44.74274 h 51.70271 V 146.99592 H 28.943326 V 360.76677 H 82.634608 V 313.04119 H 106.4974 v 75.56551 z"
id="path4"
inkscape:label="housing" /><circle
style="fill:#1a1a1a;stroke-width:1.68907;paint-order:stroke fill markers"
id="path5"
cx="416.15726"
cy="233.15318"
inkscape:label="pin4"
r="36.346325" /><circle
style="fill:#800000;stroke-width:1.68909;paint-order:stroke fill markers"
id="path5-62"
cx="311.80624"
cy="232.68082"
inkscape:label="pin3"
r="36.346325" /><circle
style="fill:#aa8800;stroke-width:1.68907;paint-order:stroke fill markers"
id="path5-6"
cx="201.49115"
cy="232.88933"
inkscape:label="pin2"
r="36.346325" /><circle
style="fill:#000080;stroke-width:1.68907;paint-order:stroke fill markers"
id="path5-9"
cx="94.05365"
cy="233.18108"
inkscape:label="pin1"
r="36.346325" /></g></g><style
id="style1-9"><![CDATA[
#keylines circle, #keylines rect {
vector-effect: non-scaling-stroke;
fill: none;
stroke: #0000FF;
}
#mask path, #mask rect, #mask circle, #hatching path {
vector-effect: non-scaling-stroke;
fill: none;
stroke: #FF0000;
}
#keylines, #mask {
opacity: 0.5;
}
]]></style></svg>

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 522 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 535 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 B

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="apps.svg"
inkscape:export-filename="icon_apps.png"
inkscape:export-xdpi="160"
inkscape:export-ydpi="160"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="17.088414"
inkscape:cx="14.600536"
inkscape:cy="18.960215"
inkscape:window-width="1503"
inkscape:window-height="933"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 122.37644,-3.1685927 q -49.173234,0 -84.190541,-35.0173063 -35.0173063,-35.017307 -35.0173063,-84.190541 0,-49.17324 35.0173063,-84.19055 35.017307,-35.01731 84.190541,-35.01731 49.17324,0 84.19055,35.01731 35.01731,35.01731 35.01731,84.19055 0,49.173234 -35.01731,84.190541 -35.01731,35.0173063 -84.19055,35.0173063 z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173063 -35.0173,-35.017307 -35.0173,-84.190541 0,-49.17324 35.0173,-84.19055 35.01731,-35.01731 84.19055,-35.01731 49.17324,0 84.19055,35.01731 35.0173,35.01731 35.0173,84.19055 0,49.173234 -35.0173,84.190541 Q 529.17324,-3.1685927 480,-3.1685927 Z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173063 -35.01731,-35.017307 -35.01731,-84.190541 0,-49.17324 35.01731,-84.19055 35.01731,-35.01731 84.19055,-35.01731 49.17323,0 84.19054,35.01731 35.01731,35.01731 35.01731,84.19055 0,49.173234 -35.01731,84.190541 -35.01731,35.0173063 -84.19054,35.0173063 z M 122.37644,-360.79215 q -49.173234,0 -84.190541,-35.0173 Q 3.1685927,-430.82676 3.1685927,-480 q 0,-49.17324 35.0173063,-84.19055 35.017307,-35.0173 84.190541,-35.0173 49.17324,0 84.19055,35.0173 35.01731,35.01731 35.01731,84.19055 0,49.17324 -35.01731,84.19055 -35.01731,35.0173 -84.19055,35.0173 z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173 -35.0173,-35.01731 -35.0173,-84.19055 0,-49.17324 35.0173,-84.19055 35.01731,-35.0173 84.19055,-35.0173 49.17324,0 84.19055,35.0173 35.0173,35.01731 35.0173,84.19055 0,49.17324 -35.0173,84.19055 -35.01731,35.0173 -84.19055,35.0173 z m 357.62356,0 q -49.17324,0 -84.19055,-35.0173 Q 718.4157,-430.82676 718.4157,-480 q 0,-49.17324 35.01731,-84.19055 35.01731,-35.0173 84.19055,-35.0173 49.17323,0 84.19054,35.0173 35.01731,35.01731 35.01731,84.19055 0,49.17324 -35.01731,84.19055 -35.01731,35.0173 -84.19054,35.0173 z M 122.37644,-718.4157 q -49.173234,0 -84.190541,-35.01731 -35.0173063,-35.01731 -35.0173063,-84.19055 0,-49.17323 35.0173063,-84.19054 35.017307,-35.01731 84.190541,-35.01731 49.17324,0 84.19055,35.01731 35.01731,35.01731 35.01731,84.19054 0,49.17324 -35.01731,84.19055 -35.01731,35.01731 -84.19055,35.01731 z m 357.62356,0 q -49.17324,0 -84.19055,-35.01731 -35.0173,-35.01731 -35.0173,-84.19055 0,-49.17323 35.0173,-84.19054 35.01731,-35.01731 84.19055,-35.01731 49.17324,0 84.19055,35.01731 35.0173,35.01731 35.0173,84.19054 0,49.17324 -35.0173,84.19055 Q 529.17324,-718.4157 480,-718.4157 Z m 357.62356,0 q -49.17324,0 -84.19055,-35.01731 -35.01731,-35.01731 -35.01731,-84.19055 0,-49.17323 35.01731,-84.19054 35.01731,-35.01731 84.19055,-35.01731 49.17323,0 84.19054,35.01731 35.01731,35.01731 35.01731,84.19054 0,49.17324 -35.01731,84.19055 -35.01731,35.01731 -84.19054,35.01731 z"
id="path1"
style="stroke-width:1.4901" />
</svg>

Before

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="folder.svg"
inkscape:export-filename="icon_files.png"
inkscape:export-xdpi="160"
inkscape:export-ydpi="160"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="48.333333"
inkscape:cx="11.989655"
inkscape:cy="12"
inkscape:window-width="1503"
inkscape:window-height="933"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 96.441379,-96.441379 q -39.554482,0 -67.722069,-28.167591 -28.16758586,-28.16758 -28.16758586,-67.72206 v -575.33794 q 0,-39.55448 28.16758586,-67.72206 28.167587,-28.16759 67.722069,-28.16759 H 384.11034 L 480,-767.66897 h 383.55862 q 39.55448,0 67.72207,28.16759 28.16759,28.16759 28.16759,67.72207 v 479.44828 q 0,39.55448 -28.16759,67.72206 -28.16759,28.167591 -67.72207,28.167591 z m 0,-95.889651 H 863.55862 v -479.44828 h -423.1131 l -95.88966,-95.88966 H 96.441379 Z m 0,0 v -575.33794 z"
id="path1"
style="stroke-width:1.19862" />
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="settings.svg"
inkscape:export-filename="icon_settings.png"
inkscape:export-xdpi="160"
inkscape:export-ydpi="160"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="48.333333"
inkscape:cx="11.989655"
inkscape:cy="12"
inkscape:window-width="1503"
inkscape:window-height="933"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 347.80751,0.69994853 328.57952,-153.12403 q -15.62275,-6.00875 -29.44288,-14.421 -13.82012,-8.41225 -27.03937,-18.02625 L 129.08904,-125.48379 -3.1034483,-353.81626 120.67679,-447.55275 q -1.20175,-8.41225 -1.20175,-16.22363 v -32.44724 q 0,-7.81138 1.20175,-16.22363 L -3.1034483,-606.18374 129.08904,-834.51621 l 143.00823,60.08749 q 13.21925,-9.614 27.64025,-18.02625 14.421,-8.41225 28.842,-14.421 l 19.22799,-153.82398 h 264.38498 l 19.22799,153.82398 q 15.62275,6.00875 29.44288,14.421 13.82012,8.41225 27.03937,18.02625 l 143.00823,-60.08749 132.19249,228.33247 -123.78024,93.73649 q 1.20175,8.41225 1.20175,16.22363 v 32.44724 q 0,7.81138 -2.4035,16.22363 l 123.78024,93.73649 -132.19249,228.33247 -141.80648,-60.08749 q -13.21925,9.614 -27.64025,18.02625 -14.421,8.41225 -28.842,14.421 L 612.19249,0.69994853 Z m 84.1225,-96.13998953 h 94.93823 l 16.8245,-127.385489 q 37.25425,-9.614 69.10062,-28.24112 31.84637,-18.62712 58.28487,-45.06562 l 118.97324,49.27175 46.86824,-81.719 -103.35049,-78.11374 q 6.00875,-16.8245 8.41225,-35.45162 2.4035,-18.62712 2.4035,-37.85512 0,-19.228 -2.4035,-37.85512 -2.4035,-18.62712 -8.41225,-35.45162 l 103.35049,-78.11374 -46.86824,-81.719 -118.97324,50.4735 q -26.4385,-27.64025 -58.28487,-46.26737 -31.84637,-18.62712 -69.10062,-28.24112 l -15.62275,-127.38549 h -94.93823 l -16.8245,127.38549 q -37.25425,9.614 -69.10062,28.24112 -31.84637,18.62712 -58.28487,45.06562 l -118.97324,-49.27175 -46.86824,81.719 103.35049,76.91199 q -6.00875,18.02625 -8.41225,36.05249 -2.4035,18.02625 -2.4035,38.456 0,19.228 2.4035,37.25425 2.4035,18.02624 8.41225,36.05249 l -103.35049,78.11374 46.86824,81.719 118.97324,-50.4735 q 26.4385,27.64025 58.28487,46.26737 31.84637,18.62712 69.10062,28.24112 z M 482.4035,-311.75502 q 69.70149,0 118.97324,-49.27174 49.27174,-49.27175 49.27174,-118.97324 0,-69.70149 -49.27174,-118.97324 -49.27175,-49.27174 -118.97324,-49.27174 -70.90324,0 -119.57411,49.27174 -48.67087,49.27175 -48.67087,118.97324 0,69.70149 48.67087,118.97324 48.67087,49.27174 119.57411,49.27174 z M 480,-480 Z"
id="path1"
style="stroke-width:1.20175" />
</svg>

Before

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="search.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
inkscape:export-filename="search.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="47.5"
inkscape:cx="12"
inkscape:cy="10.989474"
inkscape:window-width="2560"
inkscape:window-height="1368"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 884.20117,0.42105034 548.79064,-334.98948 q -39.92982,31.94386 -91.8386,50.57778 -51.90877,18.63392 -110.47251,18.63392 -145.07836,0 -245.56842,-100.49006 Q 0.42105263,-466.7579 0.42105263,-611.83626 q 0,-145.07836 100.49005737,-245.56842 100.49006,-100.49006 245.56842,-100.49006 145.07836,0 245.56842,100.49006 100.49006,100.49006 100.49006,245.56842 0,58.56374 -18.63392,110.47251 -18.63392,51.90878 -50.57778,91.8386 L 958.73684,-74.114622 Z M 346.47953,-372.25731 q 99.82456,0 169.70175,-69.8772 69.8772,-69.87719 69.8772,-169.70175 0,-99.82456 -69.8772,-169.70175 -69.87719,-69.8772 -169.70175,-69.8772 -99.82456,0 -169.70176,69.8772 -69.87718,69.87719 -69.87718,169.70175 0,99.82456 69.87718,169.70175 69.8772,69.8772 169.70176,69.8772 z"
id="path1"
style="stroke-width:1.33099" />
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="wifi_lock_black.svg"
inkscape:export-filename="wifi_lock_black.png"
inkscape:export-xdpi="64"
inkscape:export-ydpi="64"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="27.5"
inkscape:cx="12.054545"
inkscape:cy="12.072727"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M240-80q-33 0-56.5-23.5T160-160v-400q0-33 23.5-56.5T240-640h40v-80q0-83 58.5-141.5T480-920q83 0 141.5 58.5T680-720v80h40q33 0 56.5 23.5T800-560v400q0 33-23.5 56.5T720-80H240Zm0-80h480v-400H240v400Zm240-120q33 0 56.5-23.5T560-360q0-33-23.5-56.5T480-440q-33 0-56.5 23.5T400-360q0 33 23.5 56.5T480-280ZM360-640h240v-80q0-50-35-85t-85-35q-50 0-85 35t-35 85v80ZM240-160v-400 400Z"
id="path1"
style="fill:#000000" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="wifi_signal_medium_black.svg"
inkscape:export-filename="../assets/wifi_signal_medium_black.png"
inkscape:export-xdpi="64"
inkscape:export-ydpi="64"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="27.5"
inkscape:cx="10.563636"
inkscape:cy="12.072727"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 480,-120 c -28,0 -51.66667,-9.66667 -71,-29 -19.33333,-19.33333 -29,-43 -29,-71 0,-28 9.66667,-51.66667 29,-71 19.33333,-19.33333 43,-29 71,-29 28,0 51.66667,9.66667 71,29 19.33333,19.33333 29,43 29,71 0,28 -9.66667,51.66667 -29,71 -19.33333,19.33333 -43,29 -71,29 z"
id="path1"
style="fill:#000000" />
<path
d="m 254,-346 -84,-86 c 39.33333,-39.33333 85.5,-70.5 138.5,-93.5 53,-23 110.16667,-34.5 171.5,-34.5 61.33333,0 118.5,11.66667 171.5,35 53,23.33333 99.16667,55 138.5,95 l -84,84 c -29.33333,-29.33333 -63.33333,-52.33333 -102,-69 -38.66667,-16.66667 -80,-25 -124,-25 -44,0 -85.33333,8.33333 -124,25 -38.66667,16.66667 -72.66667,39.66667 -102,69 z"
id="path2"
style="fill:#000000" />
<path
d="M 84,-516 0,-600 c 61.333333,-62.66667 133,-111.66667 215,-147 82,-35.33333 170.33333,-53 265,-53 94.66667,0 183,17.66667 265,53 82,35.33333 153.66667,84.33333 215,147 l -84,84 C 824.66667,-567.33333 765.16667,-607.5 697.5,-636.5 629.83333,-665.5 557.33333,-680 480,-680 c -77.33333,0 -149.83333,14.5 -217.5,43.5 -67.66667,29 -127.16667,69.16667 -178.5,120.5 z"
id="path3"
style="fill:#b3b3b3" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="wifi_signal_strong_black.svg"
inkscape:export-filename="wifi_signal_full_black.png"
inkscape:export-xdpi="64"
inkscape:export-ydpi="64"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="27.5"
inkscape:cx="10.563636"
inkscape:cy="12.072727"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 480,-120 c -28,0 -51.66667,-9.66667 -71,-29 -19.33333,-19.33333 -29,-43 -29,-71 0,-28 9.66667,-51.66667 29,-71 19.33333,-19.33333 43,-29 71,-29 28,0 51.66667,9.66667 71,29 19.33333,19.33333 29,43 29,71 0,28 -9.66667,51.66667 -29,71 -19.33333,19.33333 -43,29 -71,29 z"
id="path1"
style="fill:#000000" />
<path
d="m 254,-346 -84,-86 c 39.33333,-39.33333 85.5,-70.5 138.5,-93.5 53,-23 110.16667,-34.5 171.5,-34.5 61.33333,0 118.5,11.66667 171.5,35 53,23.33333 99.16667,55 138.5,95 l -84,84 c -29.33333,-29.33333 -63.33333,-52.33333 -102,-69 -38.66667,-16.66667 -80,-25 -124,-25 -44,0 -85.33333,8.33333 -124,25 -38.66667,16.66667 -72.66667,39.66667 -102,69 z"
id="path2"
style="fill:#000000" />
<path
d="M 84,-516 0,-600 c 61.333333,-62.66667 133,-111.66667 215,-147 82,-35.33333 170.33333,-53 265,-53 94.66667,0 183,17.66667 265,53 82,35.33333 153.66667,84.33333 215,147 l -84,84 C 824.66667,-567.33333 765.16667,-607.5 697.5,-636.5 629.83333,-665.5 557.33333,-680 480,-680 c -77.33333,0 -149.83333,14.5 -217.5,43.5 -67.66667,29 -127.16667,69.16667 -178.5,120.5 z"
id="path3"
style="fill:#000000" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,50 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="wifi_signal_weak_black.svg"
inkscape:export-filename="../assets/wifi_signal_weak_black.png"
inkscape:export-xdpi="64"
inkscape:export-ydpi="64"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="27.5"
inkscape:cx="10.563636"
inkscape:cy="12.072727"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 480,-120 c -28,0 -51.66667,-9.66667 -71,-29 -19.33333,-19.33333 -29,-43 -29,-71 0,-28 9.66667,-51.66667 29,-71 19.33333,-19.33333 43,-29 71,-29 28,0 51.66667,9.66667 71,29 19.33333,19.33333 29,43 29,71 0,28 -9.66667,51.66667 -29,71 -19.33333,19.33333 -43,29 -71,29 z"
id="path1"
style="fill:#000000" />
<path
d="m 254,-346 -84,-86 c 39.33333,-39.33333 85.5,-70.5 138.5,-93.5 53,-23 110.16667,-34.5 171.5,-34.5 61.33333,0 118.5,11.66667 171.5,35 53,23.33333 99.16667,55 138.5,95 l -84,84 c -29.33333,-29.33333 -63.33333,-52.33333 -102,-69 -38.66667,-16.66667 -80,-25 -124,-25 -44,0 -85.33333,8.33333 -124,25 -38.66667,16.66667 -72.66667,39.66667 -102,69 z"
id="path2"
style="fill:#b3b3b3" />
<path
d="M 84,-516 0,-600 c 61.333333,-62.66667 133,-111.66667 215,-147 82,-35.33333 170.33333,-53 265,-53 94.66667,0 183,17.66667 265,53 82,35.33333 153.66667,84.33333 215,147 l -84,84 C 824.66667,-567.33333 765.16667,-607.5 697.5,-636.5 629.83333,-665.5 557.33333,-680 480,-680 c -77.33333,0 -149.83333,14.5 -217.5,43.5 -67.66667,29 -127.16667,69.16667 -178.5,120.5 z"
id="path3"
style="fill:#b3b3b3" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: Apache. Made by Iconscout: https://github.com/Iconscout/unicons -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" data-name="Layer 1"><path d="M12.71,17.29a1,1,0,0,0-.16-.12.56.56,0,0,0-.17-.09.6.6,0,0,0-.19-.06.93.93,0,0,0-.57.06.9.9,0,0,0-.54.54A.84.84,0,0,0,11,18a1,1,0,0,0,.07.38,1.46,1.46,0,0,0,.22.33A1,1,0,0,0,12,19a.84.84,0,0,0,.38-.08,1.15,1.15,0,0,0,.33-.21A1,1,0,0,0,13,18a1,1,0,0,0-.08-.38A1,1,0,0,0,12.71,17.29ZM8.55,13.17a.56.56,0,0,0-.17-.09A.6.6,0,0,0,8.19,13a.86.86,0,0,0-.39,0l-.18.06-.18.09-.15.12A1.05,1.05,0,0,0,7,14a1,1,0,0,0,.29.71,1.15,1.15,0,0,0,.33.21A1,1,0,0,0,9,14a1.05,1.05,0,0,0-.29-.71Zm.16,4.12a1,1,0,0,0-.33-.21A1,1,0,0,0,7.8,17l-.18.06a.76.76,0,0,0-.18.09,1.58,1.58,0,0,0-.15.12,1,1,0,0,0-.21.33.94.94,0,0,0,0,.76,1.15,1.15,0,0,0,.21.33A1,1,0,0,0,8,19a.84.84,0,0,0,.38-.08,1.15,1.15,0,0,0,.33-.21,1.15,1.15,0,0,0,.21-.33.94.94,0,0,0,0-.76A1,1,0,0,0,8.71,17.29Zm2.91-4.21a1,1,0,0,0-.33.21A1.05,1.05,0,0,0,11,14a1,1,0,0,0,1.38.92,1.15,1.15,0,0,0,.33-.21A1,1,0,0,0,13,14a1.05,1.05,0,0,0-.29-.71A1,1,0,0,0,11.62,13.08Zm5.09,4.21a1.15,1.15,0,0,0-.33-.21,1,1,0,0,0-1.09.21,1,1,0,0,0-.21.33.94.94,0,0,0,0,.76,1.15,1.15,0,0,0,.21.33A1,1,0,0,0,16,19a.84.84,0,0,0,.38-.08,1.15,1.15,0,0,0,.33-.21,1,1,0,0,0,.21-1.09A1,1,0,0,0,16.71,17.29ZM16,5H8A1,1,0,0,0,7,6v4a1,1,0,0,0,1,1h8a1,1,0,0,0,1-1V6A1,1,0,0,0,16,5ZM15,9H9V7h6Zm3-8H6A3,3,0,0,0,3,4V20a3,3,0,0,0,3,3H18a3,3,0,0,0,3-3V4A3,3,0,0,0,18,1Zm1,19a1,1,0,0,1-1,1H6a1,1,0,0,1-1-1V4A1,1,0,0,1,6,3H18a1,1,0,0,1,1,1Zm-2.45-6.83a.56.56,0,0,0-.17-.09.6.6,0,0,0-.19-.06.86.86,0,0,0-.39,0l-.18.06-.18.09-.15.12A1.05,1.05,0,0,0,15,14a1,1,0,0,0,1.38.92,1.15,1.15,0,0,0,.33-.21A1,1,0,0,0,17,14a1.05,1.05,0,0,0-.29-.71Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: PD. Made by Mary Akveo: https://maryakveo.com/ -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" id="chat" data-name="Line Color" xmlns="http://www.w3.org/2000/svg" class="icon line-color"><path id="primary" d="M18.81,16.23,20,21l-4.95-2.48A9.84,9.84,0,0,1,12,19c-5,0-9-3.58-9-8s4-8,9-8,9,3.58,9,8A7.49,7.49,0,0,1,18.81,16.23Z" style="fill: none; stroke: #000000; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></path></svg>

Before

Width:  |  Height:  |  Size: 518 B

View File

@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: PD. Made by Mary Akveo: https://maryakveo.com/ -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 24 24" id="chip" data-name="Flat Line" xmlns="http://www.w3.org/2000/svg" class="icon flat-line"><rect id="secondary" x="6" y="6" width="12" height="12" rx="1" style="fill: none; stroke-width: 2;"></rect><path id="primary" d="M12,6V3m4,3V4M8,6V4m10,8h3m-3,4h2M18,8h2M12,18v3M8,18v2m8-2v2M6,12H3M6,8H4m2,8H4m14,1V7a1,1,0,0,0-1-1H7A1,1,0,0,0,6,7V17a1,1,0,0,0,1,1H17A1,1,0,0,0,18,17Z" style="fill: none; stroke: #333333; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;"></path></svg>

Before

Width:  |  Height:  |  Size: 662 B

View File

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- License: MIT. Made by vmware: https://github.com/vmware/clarity-assets -->
<svg fill="#000000" width="800px" height="800px" viewBox="0 0 36 36" version="1.1" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>note-line</title>
<path d="M28,30H6V8H19.22l2-2H6A2,2,0,0,0,4,8V30a2,2,0,0,0,2,2H28a2,2,0,0,0,2-2V15l-2,2Z" class="clr-i-outline clr-i-outline-path-1"></path><path d="M33.53,5.84,30.16,2.47a1.61,1.61,0,0,0-2.28,0L14.17,16.26l-1.11,4.81A1.61,1.61,0,0,0,14.63,23,1.69,1.69,0,0,0,15,23l4.85-1.07L33.53,8.12A1.61,1.61,0,0,0,33.53,5.84ZM18.81,20.08l-3.66.81L16,17.26,26.32,6.87l2.82,2.82ZM30.27,8.56,27.45,5.74,29,4.16,31.84,7Z" class="clr-i-outline clr-i-outline-path-2"></path>
<rect x="0" y="0" width="36" height="36" fill-opacity="0"/>
</svg>

Before

Width:  |  Height:  |  Size: 878 B

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16"
viewBox="0 -960 640 640"
width="16"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="calendar_clock_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.svg"
inkscape:export-filename="calendar_clock_24dp_E8EAED_FILL0_wght400_GRAD0_opsz24.png"
inkscape:export-xdpi="96.18351"
inkscape:export-ydpi="96.18351"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="47.5"
inkscape:cx="10.842105"
inkscape:cy="10.821053"
inkscape:window-width="2560"
inkscape:window-height="1368"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 61.257143,-775.80752 H 487.10977 v -60.83609 H 61.257143 Z m 0,0 v -60.83609 z m 0,425.85263 q -25.094888,0 -42.965489,-17.8706 -17.870601,-17.8706 -17.870601,-42.96549 v -425.85263 q 0,-25.09489 17.870601,-42.96549 17.870601,-17.8706 42.965489,-17.8706 H 91.67518 v -60.83609 h 60.83609 v 60.83609 h 243.34437 v -60.83609 h 60.83609 v 60.83609 h 30.41804 q 25.09489,0 42.96549,17.8706 17.8706,17.8706 17.8706,42.96549 v 172.62241 q -14.44857,-6.84406 -29.6576,-11.40677 -15.20901,-4.56271 -31.17849,-6.84406 v -32.6994 H 61.257143 v 304.18045 H 252.89083 q 5.32315,16.72993 12.54744,31.93895 7.22428,15.20902 17.11015,28.89714 z m 395.434587,30.41805 q -63.11745,0 -107.60384,-44.48639 -44.48639,-44.4864 -44.48639,-107.60384 0,-63.11744 44.48639,-107.60384 44.48639,-44.48638 107.60384,-44.48638 63.11744,0 107.60383,44.48638 44.48639,44.4864 44.48639,107.60384 0,63.11744 -44.48639,107.60384 -44.48639,44.48639 -107.60383,44.48639 z m 50.95022,-79.84737 21.29264,-21.29263 -57.03384,-57.03384 V -562.8812 H 441.4827 v 97.33774 z"
id="path1"
style="stroke-width:0.760454;fill:#000000" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="16"
viewBox="0 -960 640 640"
width="16"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="location.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
inkscape:export-filename="location.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="47.5"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="2560"
inkscape:window-height="1368"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M 347.36841,-361.6842 259.36842,-579.36842 41.684211,-667.36841 V -710.5965 L 597.47368,-917.47368 390.59649,-361.6842 Z m 20.07018,-114.24563 125.05263,-336.5614 -336.5614,125.05264 151.29825,60.21052 z m -60.21052,-151.29824 z"
id="path1"
style="stroke-width:0.771924" />
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="20"
viewBox="0 -960 960 960"
width="20"
version="1.1"
id="svg1"
sodipodi:docname="wifi_off_white.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
inkscape:export-filename="../assets/wifi_off_white.png"
inkscape:export-xdpi="76.800003"
inkscape:export-ydpi="76.800003"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="33"
inkscape:cx="10.045455"
inkscape:cy="10"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m699-363-51-51 206-205q-81-61-176-93t-198-32q-36.667 0-73.333 4.5Q370-735 335-727l-59-59q48-15 99.241-22.5T480-816q140 0 263 51t217 141L699-363ZM480-246l66-66-359-359q-21 11-41 24.5T106-619l374 373ZM813-45 597-261 480-144 0-624q30-29 63.533-54.224Q97.065-703.447 134-724l-89-89 51-51L864-96l-51 51ZM492-571Zm-125 79Z"
id="path1"
style="fill:#ffffff" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,41 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="20"
viewBox="0 -960 960 960"
width="20"
version="1.1"
id="svg1"
sodipodi:docname="wifi_scan_white.svg"
inkscape:export-filename="../assets/wifi_scan_white.png"
inkscape:export-xdpi="76.800003"
inkscape:export-ydpi="76.800003"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="33"
inkscape:cx="10.015152"
inkscape:cy="10"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="M480-144 0-624q97-94 221-143t259-49q135 0 259 49t221 143l-51 52q-86-85-197-128.5T480-744q-102 0-197.5 32T106-619l427 422-53 53Zm381-48L745-308q-17 10-35 15t-38 5q-60 0-102-42t-42-102q0-60 42-102t102-42q60 0 102 42t42 102q0 20-4.5 38.5T796-359l116 116-51 51ZM672-360q30 0 51-21t21-51q0-30-21-51t-51-21q-30 0-51 21t-21 51q0 30 21 51t51 21ZM480-196Z"
id="path1"
style="fill:#ffffff" />
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="wifi_signal_medium_white.svg"
inkscape:export-filename="../assets/wifi_signal_medium_white.png"
inkscape:export-xdpi="64"
inkscape:export-ydpi="64"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="27.5"
inkscape:cx="10.563636"
inkscape:cy="12.072727"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 480,-120 c -28,0 -51.66667,-9.66667 -71,-29 -19.33333,-19.33333 -29,-43 -29,-71 0,-28 9.66667,-51.66667 29,-71 19.33333,-19.33333 43,-29 71,-29 28,0 51.66667,9.66667 71,29 19.33333,19.33333 29,43 29,71 0,28 -9.66667,51.66667 -29,71 -19.33333,19.33333 -43,29 -71,29 z"
id="path1" />
<path
d="m 254,-346 -84,-86 c 39.33333,-39.33333 85.5,-70.5 138.5,-93.5 53,-23 110.16667,-34.5 171.5,-34.5 61.33333,0 118.5,11.66667 171.5,35 53,23.33333 99.16667,55 138.5,95 l -84,84 c -29.33333,-29.33333 -63.33333,-52.33333 -102,-69 -38.66667,-16.66667 -80,-25 -124,-25 -44,0 -85.33333,8.33333 -124,25 -38.66667,16.66667 -72.66667,39.66667 -102,69 z"
id="path2" />
<path
d="M 84,-516 0,-600 c 61.333333,-62.66667 133,-111.66667 215,-147 82,-35.33333 170.33333,-53 265,-53 94.66667,0 183,17.66667 265,53 82,35.33333 153.66667,84.33333 215,147 l -84,84 C 824.66667,-567.33333 765.16667,-607.5 697.5,-636.5 629.83333,-665.5 557.33333,-680 480,-680 c -77.33333,0 -149.83333,14.5 -217.5,43.5 -67.66667,29 -127.16667,69.16667 -178.5,120.5 z"
id="path3"
style="fill:#666666" />
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="wifi_signal_strong_white.svg"
inkscape:export-filename="../assets/wifi_signal_strong_white.png"
inkscape:export-xdpi="64"
inkscape:export-ydpi="64"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="27.5"
inkscape:cx="10.563636"
inkscape:cy="12.036364"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 480,-120 c -28,0 -51.66667,-9.66667 -71,-29 -19.33333,-19.33333 -29,-43 -29,-71 0,-28 9.66667,-51.66667 29,-71 19.33333,-19.33333 43,-29 71,-29 28,0 51.66667,9.66667 71,29 19.33333,19.33333 29,43 29,71 0,28 -9.66667,51.66667 -29,71 -19.33333,19.33333 -43,29 -71,29 z"
id="path1" />
<path
d="m 254,-346 -84,-86 c 39.33333,-39.33333 85.5,-70.5 138.5,-93.5 53,-23 110.16667,-34.5 171.5,-34.5 61.33333,0 118.5,11.66667 171.5,35 53,23.33333 99.16667,55 138.5,95 l -84,84 c -29.33333,-29.33333 -63.33333,-52.33333 -102,-69 -38.66667,-16.66667 -80,-25 -124,-25 -44,0 -85.33333,8.33333 -124,25 -38.66667,16.66667 -72.66667,39.66667 -102,69 z"
id="path2" />
<path
d="M 84,-516 0,-600 c 61.333333,-62.66667 133,-111.66667 215,-147 82,-35.33333 170.33333,-53 265,-53 94.66667,0 183,17.66667 265,53 82,35.33333 153.66667,84.33333 215,147 l -84,84 C 824.66667,-567.33333 765.16667,-607.5 697.5,-636.5 629.83333,-665.5 557.33333,-680 480,-680 c -77.33333,0 -149.83333,14.5 -217.5,43.5 -67.66667,29 -127.16667,69.16667 -178.5,120.5 z"
id="path3" />
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
height="24px"
viewBox="0 -960 960 960"
width="24px"
fill="#e8eaed"
version="1.1"
id="svg1"
sodipodi:docname="wifi_signal_weak_white.svg"
inkscape:export-filename="../assets/wifi_signal_weak_white.png"
inkscape:export-xdpi="64"
inkscape:export-ydpi="64"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="27.5"
inkscape:cx="10.563636"
inkscape:cy="12.072727"
inkscape:window-width="1503"
inkscape:window-height="930"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" />
<path
d="m 480,-120 c -28,0 -51.66667,-9.66667 -71,-29 -19.33333,-19.33333 -29,-43 -29,-71 0,-28 9.66667,-51.66667 29,-71 19.33333,-19.33333 43,-29 71,-29 28,0 51.66667,9.66667 71,29 19.33333,19.33333 29,43 29,71 0,28 -9.66667,51.66667 -29,71 -19.33333,19.33333 -43,29 -71,29 z"
id="path1" />
<path
d="m 254,-346 -84,-86 c 39.33333,-39.33333 85.5,-70.5 138.5,-93.5 53,-23 110.16667,-34.5 171.5,-34.5 61.33333,0 118.5,11.66667 171.5,35 53,23.33333 99.16667,55 138.5,95 l -84,84 c -29.33333,-29.33333 -63.33333,-52.33333 -102,-69 -38.66667,-16.66667 -80,-25 -124,-25 -44,0 -85.33333,8.33333 -124,25 -38.66667,16.66667 -72.66667,39.66667 -102,69 z"
id="path2"
style="fill:#666666" />
<path
d="M 84,-516 0,-600 c 61.333333,-62.66667 133,-111.66667 215,-147 82,-35.33333 170.33333,-53 265,-53 94.66667,0 183,17.66667 265,53 82,35.33333 153.66667,84.33333 215,147 l -84,84 C 824.66667,-567.33333 765.16667,-607.5 697.5,-636.5 629.83333,-665.5 557.33333,-680 480,-680 c -77.33333,0 -149.83333,14.5 -217.5,43.5 -67.66667,29 -127.16667,69.16667 -178.5,120.5 z"
id="path3"
style="fill:#666666" />
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,26 @@
general.vendor=LilyGO
general.name=T5 4.7 Inch E-Paper S3
general.incubating=true
apps.launcherAppId=tactility.launcher
hardware.target=ESP32S3
hardware.flashSize=16MB
hardware.spiRam=true
hardware.spiRamMode=OCT
hardware.spiRamSpeed=80M
hardware.esptoolFlashFreq=80M
hardware.bluetooth=true
storage.userDataLocation=Internal
display.size=4.7"
display.shape=rectangle
display.dpi=235
lvgl.colorDepth=8
lvgl.fontSize=24
lvgl.theme=Mono
lvgl.statusbarColorsInverted=true
sdkconfig.CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=0

View File

@ -0,0 +1,92 @@
/dts-v1/;
#include <tactility/bindings/root.h>
#include <tactility/bindings/esp32_ble.h>
#include <tactility/bindings/esp32_wifi_pinned.h>
#include <tactility/bindings/esp32_gpio.h>
#include <tactility/bindings/esp32_i2c_master.h>
#include <tactility/bindings/esp32_spi.h>
#include <tactility/bindings/esp32_adc_oneshot.h>
#include <tactility/bindings/battery_sense.h>
#include <bindings/bm8563.h>
#include <bindings/gt911.h>
#include <tactility/bindings/esp32_sdspi.h>
#include <bindings/t5s3_display.h>
/ {
compatible = "root";
model = "LILYGO T5 4.7 Inch E-Paper S3";
wifi0 {
compatible = "espressif,esp32-wifi-pinned";
};
ble0 {
compatible = "espressif,esp32-ble";
};
gpio0 {
compatible = "espressif,esp32-gpio";
gpio-count = <49>;
};
// Listed before the I2C bus on purpose, the touch controller was only tested with the display started first
display {
compatible = "lilygo,t5s3-display";
};
i2c_internal {
compatible = "espressif,esp32-i2c-master";
port = <I2C_NUM_0>;
clock-frequency = <400000>;
// The board relies on the internal pull-ups, as the Arduino Wire library enables them
pin-sda = <&gpio0 18 GPIO_FLAG_PULL_UP>;
pin-scl = <&gpio0 17 GPIO_FLAG_PULL_UP>;
// PCF8563, register-compatible with the BM8563
bm8563 {
compatible = "belling,bm8563";
reg = <0x51>;
};
// The controller reports 540x960 (portrait) and the panel runs in landscape
touch {
compatible = "goodix,gt911";
reg = <0x5D>;
x-max = <540>;
y-max = <960>;
swap-xy;
mirror-x;
pin-interrupt = <&gpio0 47 GPIO_FLAG_NONE>;
};
};
spi0 {
compatible = "espressif,esp32-spi";
host = <SPI2_HOST>;
cs-gpios = <&gpio0 42 GPIO_FLAG_NONE>;
pin-mosi = <&gpio0 15 GPIO_FLAG_NONE>;
pin-miso = <&gpio0 16 GPIO_FLAG_NONE>;
pin-sclk = <&gpio0 11 GPIO_FLAG_NONE>;
max-transfer-size = <4096>;
sdcard@0 {
compatible = "espressif,esp32-sdspi";
frequency-khz = <20000>;
};
};
// Battery voltage behind a 1:2 divider on GPIO14 (ADC2 channel 3), not yet calibrated
adc0 {
compatible = "espressif,esp32-adc-oneshot";
unit-id = <ADC_UNIT_2>;
channels = <ADC_CHANNEL_3 ADC_ATTEN_DB_12 ADC_BITWIDTH_DEFAULT>;
};
battery-sense {
compatible = "battery-sense";
io-channel = <&adc0 0>;
reference-voltage-mv = <3300>;
multiplier = <2000>;
};
};

View File

@ -0,0 +1,7 @@
dependencies:
- Platforms/platform-esp32
- Drivers/bm8563-module
- Drivers/gt911-module
- Drivers/lilygo-t5s3-epd-module
dts: lilygo,t5-epd47-s3.dts
dts-only: true

View File

@ -67,7 +67,7 @@
i2c_internal: i2c0 {
compatible = "espressif,esp32-i2c-master";
port = <I2C_NUM_0>;
clock-frequency = <100000>;
clock-frequency = <400000>;
pin-sda = <&gpio0 31 GPIO_FLAG_PULL_UP>;
pin-scl = <&gpio0 32 GPIO_FLAG_PULL_UP>;

View File

@ -32,7 +32,7 @@
- Improve Setup: Show "Step done" screen
- Make it more clear to end-users that an SD card is required to run Tactility
- Warn if file operations are done from prohibited tasks (e.g. lvgl task)
- stopAppFromToolbar() in Tactility.cpp stops the top-most app. Change it so the toolbar knows for which app id it is created, so it can rely on that.
- stopAppFromToolbar() in lvgl/Lvgl.cpp stops the top-most app. Change it so the toolbar knows for which app id it is created, so it can rely on that.
- Consider not unpacking `.app` files and executing them directly. Might want to cache file offsets. Cache file must be pinned to app version.
- lvgl-module's spinner relies on hard-coded spinner asset from Tactility main project.
- esp_lvgl_port settings has a large stack size (~9kB) to fix stackoverflow when LVGL events (e.g. button click) do actions like file operations do actions like file operations. Can we reduce the callstack?

View File

@ -0,0 +1,18 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
tactility_add_module(lilygo-t5s3-epd-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES TactilityKernel esp_lcd esp_driver_gpio esp_driver_rmt esp_timer esp_rom hal soc heap freertos
)
# The pixel loops of the display are too slow with the debug optimization level of the project
set_source_files_properties(
source/t5s3_epd.cpp
source/t5s3_display.cpp
PROPERTIES COMPILE_OPTIONS -O2
)

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,165 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates
the terms and conditions of version 3 of the GNU General Public
License, supplemented by the additional permissions listed below.
0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the GNU
General Public License.
"The Library" refers to a covered work governed by this License,
other than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
a) under this License, provided that you make a good faith effort to
ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from
a header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
a) Give prominent notice with each copy of the object code that the
Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the object code with a copy of the GNU GPL and this license
document.
4. Combined Works.
You may convey a Combined Work under terms of your choice that,
taken together, effectively do not restrict modification of the
portions of the Library contained in the Combined Work and reverse
engineering for debugging such modifications, if you also do each of
the following:
a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
b) Accompany the Combined Work with a copy of the GNU GPL and this license
document.
c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
d) Do one of the following:
0) Convey the Minimal Corresponding Source under the terms of this
License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
1) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (a) uses at run time
a copy of the Library already present on the user's computer
system, and (b) will operate properly with a modified version
of the Library that is interface-compatible with the Linked
Version.
e) Provide Installation Information, but only if you would otherwise
be required to provide such information under section 6 of the
GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the
Application with a modified version of the Linked Version. (If
you use option 4d0, the Installation Information must accompany
the Minimal Corresponding Source and Corresponding Application
Code. If you use option 4d1, you must provide the Installation
Information in the manner specified by section 6 of the GNU GPL
for conveying Corresponding Source.)
5. Combined Libraries.
You may place library facilities that are a work based on the
Library side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
a) Accompany the combined library with a copy of the same work based
on the Library, uncombined with any other library facilities,
conveyed under the terms of this License.
b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the
Library as you received it specifies that a certain numbered version
of the GNU Lesser General Public License "or any later version"
applies to it, you have the option of following the terms and
conditions either of that published version or of any later version
published by the Free Software Foundation. If the Library as you
received it does not specify a version number of the GNU Lesser
General Public License, you may choose any version of the GNU Lesser
General Public License ever published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

View File

@ -0,0 +1,28 @@
# lilygo-t5s3-epd-module
A kernel driver for the 4.7" 960x540 `ED047TC1` e-paper panel of the LILYGO T5 4.7 Inch E-Paper S3 (V2.3/V2.4).
The driver only works on this board. The pins of the panel, the 74HCT4094 shift register that carries the panel supplies and the sequence of the panel are fixed in the code, so the devicetree node only has the pixel clock as property.
- `t5s3_display.cpp`: the Tactility display driver. It decides between fast and quality updates.
- `t5s3_epd.cpp`: the engine. It controls the shift register, the CKV gate clock (RMT peripheral), the row transfer (LCD peripheral in i80 mode), the update loop and the clear sequence.
- `t5s3_epd_rows.h`: the conversion of framebuffer rows into bus data. It has no hardware access.
- `t5s3_epd_waveform.h`: the 30 phase drive table of the quality updates.
There are three update modes. Fast draws black and white only. Quality draws 16 levels and leaves white pixels that stay white alone, which is used for partial updates. Full also flashes those white pixels, which is used for the clear and the refresh.
## License
The engine and its tables are derived from [epdiy](https://github.com/vroland/epdiy) and are licensed under the
[LGPL v3.0 or later](LICENSE-LGPL-3.0.md) (the LGPL supplements the [GPL v3.0](../../Documentation/LICENSE-GPL-3.0.md)):
- `source/t5s3_epd.cpp`
- `source/t5s3_epd.h`
- `source/t5s3_epd_rows.h`
- `source/t5s3_epd_waveform.h`
These files follow the sequences and timings of epdiy and contain its ED047TC1 waveform data. Each of them has an `SPDX-License-Identifier: LGPL-3.0-or-later` header.
The rest of the module (`source/t5s3_display.cpp`, `source/module.cpp`, the bindings and the headers in `include/`) is licensed under [Apache License v2.0](LICENSE-Apache-2.0.md).
A project that contains this module has to follow the LGPL for the four files above.

View File

@ -0,0 +1,11 @@
description: >
LILYGO T5 4.7 Inch E-Paper S3 (V2.3/V2.4) display with an ED047TC1 panel. The pins of the panel
are fixed in the driver, so this node only has the pixel clock as property.
compatible: "lilygo,t5s3-display"
properties:
pixel-clock-hz:
type: int
default: 10000000
description: Clock of the bus that sends the pixel data of a row. Faster speeds up updates if the panel accepts it.

View File

@ -0,0 +1,15 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <tactility/bindings/bindings.h>
#include <drivers/t5s3_display.h>
DEFINE_DEVICETREE(t5s3_display, struct T5s3DisplayConfig)
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,16 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct T5s3DisplayConfig {
uint32_t pixel_clock_hz;
};
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module lilygo_t5s3_epd_module;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,3 @@
dependencies:
- TactilityKernel
bindings: bindings

View File

@ -0,0 +1,23 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/driver.h>
#include <tactility/module.h>
extern "C" {
extern Driver t5s3_display_driver;
static Driver* const lilygo_t5s3_epd_drivers[] = {
&t5s3_display_driver,
nullptr
};
Module lilygo_t5s3_epd_module = {
.name = "lilygo-t5s3-epd",
.start = nullptr,
.stop = nullptr,
.drivers = lilygo_t5s3_epd_drivers,
.symbols = nullptr,
.internal = nullptr
};
}

View File

@ -0,0 +1,309 @@
// SPDX-License-Identifier: Apache-2.0
#include <drivers/t5s3_display.h>
#include <lilygo_t5s3_epd_module.h>
#include <tactility/device.h>
#include <tactility/driver.h>
#include <tactility/drivers/display.h>
#include <tactility/error.h>
#include <tactility/log.h>
#include <tactility/module.h>
#include <tactility/time.h>
#include "t5s3_epd.h"
#include <cstdlib>
#include <cstring>
#define TAG "T5s3Display"
#define GET_CONFIG(device) (static_cast<const T5s3DisplayConfig*>((device)->config))
// An update covering at least this fraction of the panel is a full-screen change and gets a quality refresh
static constexpr float FULL_AREA_QUALITY_THRESHOLD = 0.6f;
// Fast updates allowed before a quality refresh limits ghosting. A full-screen redraw takes about 10 updates, so this must be higher
static constexpr uint32_t QUALITY_REFRESH_PARTIAL_COUNT = 20;
// Updates within this time after a quality update also use quality mode, so the tiles of one redraw stay consistent.
// It must stay well below the duration of a quality update.
static constexpr uint32_t QUALITY_HOLD_MS = 50;
// Maximum time that consecutive quality updates keep extending the hold window
static constexpr uint32_t QUALITY_HOLD_SESSION_MAX_MS = 200;
// 4x4 ordered (Bayer) dither thresholds, spread evenly across a 0-15 nibble range.
static constexpr uint8_t BAYER_4X4[4][4] = {
{ 0, 8, 2, 10 },
{ 12, 4, 14, 6 },
{ 3, 11, 1, 9 },
{ 15, 7, 13, 5 },
};
// Dithers an 8-bit luminance (0x00 black to 0xFF white) to a 4-bit level (0x0 black to 0xF white)
static inline uint8_t dither_to_nibble(uint8_t luminance, int32_t x, int32_t y) {
// Scaling by 17 spreads the thresholds over one full quantization step
const uint32_t threshold = BAYER_4X4[y & 3][x & 3] * 17U;
const uint32_t level = (static_cast<uint32_t>(luminance) * 15U + threshold) / 255U;
return static_cast<uint8_t>(level > 15U ? 15U : level);
}
// Binary variant for fast updates, which only draw black and white
static inline uint8_t dither_to_bw_nibble(uint8_t luminance, int32_t x, int32_t y) {
// Scaling by 16 keeps the highest threshold at 240, so pure white (0xFF) is never classified as black
const uint32_t threshold = BAYER_4X4[y & 3][x & 3] * 16U;
return luminance > threshold ? 0xF : 0x0;
}
extern "C" {
struct T5s3DisplayInternal {
uint8_t* framebuffer;
bool powered;
// Fast updates since the last quality refresh
uint32_t partial_count_since_quality;
// Until this tick every update uses quality mode
TickType_t quality_hold_until_tick;
// Tick at which the current hold window started
TickType_t quality_hold_session_start_tick;
};
static void power_on(T5s3DisplayInternal* internal) {
if (!internal->powered) {
t5s3_epd_power_on();
internal->powered = true;
}
}
// region DisplayApi
static error_t t5s3_display_reset(Device* device) {
auto* internal = static_cast<T5s3DisplayInternal*>(device_get_driver_data(device));
// The panel has no reset line, so a power cycle is the closest equivalent
t5s3_epd_power_off();
internal->powered = false;
power_on(internal);
return ERROR_NONE;
}
/** Initialization is done in start() */
static error_t t5s3_display_init(Device*) {
return ERROR_NONE;
}
/** Fills the panel white with one full update, without the flashing cycles of the initial clear */
static error_t t5s3_display_clear(Device* device) {
auto* internal = static_cast<T5s3DisplayInternal*>(device_get_driver_data(device));
power_on(internal);
t5s3_epd_fill_white();
const bool success = t5s3_epd_update(T5s3EpdMode::Full, 0, T5S3_EPD_HEIGHT);
if (success) {
internal->partial_count_since_quality = 0;
internal->quality_hold_until_tick = 0;
}
return success ? ERROR_NONE : ERROR_RESOURCE;
}
/**
* Redraws the current content with a quality update to remove ghosting.
* The update skips pixels that did not change, so every pixel is invalidated first.
*/
static error_t t5s3_display_refresh(Device* device) {
auto* internal = static_cast<T5s3DisplayInternal*>(device_get_driver_data(device));
power_on(internal);
t5s3_epd_invalidate();
const bool success = t5s3_epd_update(T5s3EpdMode::Full, 0, T5S3_EPD_HEIGHT);
if (success) {
internal->partial_count_since_quality = 0;
internal->quality_hold_until_tick = 0;
}
return success ? ERROR_NONE : ERROR_RESOURCE;
}
// Decides between a quality refresh and a fast update, *out_within_hold tells if the hold window applied
static bool should_use_quality_mode(T5s3DisplayInternal* internal, int32_t width, int32_t height, bool* out_within_hold) {
const TickType_t now = get_ticks();
const uint32_t area = static_cast<uint32_t>(width) * static_cast<uint32_t>(height);
const bool is_full_screen_change = area >= static_cast<uint32_t>(
static_cast<float>(T5S3_EPD_WIDTH * T5S3_EPD_HEIGHT) * FULL_AREA_QUALITY_THRESHOLD
);
const bool partial_count_exceeded = internal->partial_count_since_quality >= QUALITY_REFRESH_PARTIAL_COUNT;
// Signed difference stays correct across the tick counter wrap, 0 means no hold
const TickType_t hold_until = internal->quality_hold_until_tick;
const bool within_hold = hold_until != 0 && static_cast<int32_t>(hold_until - now) > 0;
*out_within_hold = within_hold;
return is_full_screen_change || partial_count_exceeded || within_hold;
}
// Updates the counters after an update. A failed quality update leaves them unchanged and a failed fast update still counts.
// A quality update starts or extends the hold window, up to QUALITY_HOLD_SESSION_MAX_MS.
static void commit_quality_mode_decision(T5s3DisplayInternal* internal, bool used_quality, bool draw_succeeded, bool was_within_hold) {
if (used_quality) {
if (draw_succeeded) {
internal->partial_count_since_quality = 0;
const TickType_t now = get_ticks();
if (!was_within_hold) {
internal->quality_hold_session_start_tick = now;
}
const TickType_t session_elapsed = now - internal->quality_hold_session_start_tick;
if (session_elapsed < millis_to_ticks(QUALITY_HOLD_SESSION_MAX_MS)) {
internal->quality_hold_until_tick = now + millis_to_ticks(QUALITY_HOLD_MS);
} else {
internal->quality_hold_until_tick = 0;
}
}
} else {
internal->partial_count_since_quality++;
}
}
// Reports GRAYSCALE8 so that draw_bitmap() is called per changed tile
static error_t t5s3_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) {
auto* internal = static_cast<T5s3DisplayInternal*>(device_get_driver_data(device));
const int32_t width = x_end - x_start;
const int32_t height = y_end - y_start;
bool within_hold = false;
const bool use_quality = should_use_quality_mode(internal, width, height, &within_hold);
// color_data is 8-bit luminance (0x00 black to 0xFF white) and the panel wants 4-bit levels (0x0 black to 0xF white)
// Dithering uses 16 levels for quality updates and 2 levels for fast updates
const auto* src = static_cast<const uint8_t*>(color_data);
const size_t src_stride = static_cast<size_t>(width);
for (int32_t row = 0; row < height; row++) {
const uint8_t* src_row = src + static_cast<size_t>(row) * src_stride;
const int32_t display_y = y_start + row;
for (int32_t col = 0; col < width; col++) {
const int32_t display_x = x_start + col;
const uint8_t nibble = use_quality
? dither_to_nibble(src_row[col], display_x, display_y)
: dither_to_bw_nibble(src_row[col], display_x, display_y);
t5s3_epd_set_pixel(internal->framebuffer, display_x, display_y, nibble);
}
}
power_on(internal);
const bool success = t5s3_epd_update(use_quality ? T5s3EpdMode::Quality : T5s3EpdMode::Fast, y_start, y_end);
commit_quality_mode_decision(internal, use_quality, success, within_hold);
return success ? ERROR_NONE : ERROR_RESOURCE;
}
static error_t t5s3_display_disp_on_off(Device* device, bool on_off) {
auto* internal = static_cast<T5s3DisplayInternal*>(device_get_driver_data(device));
if (on_off) {
power_on(internal);
} else if (internal->powered) {
t5s3_epd_power_off();
internal->powered = false;
}
return ERROR_NONE;
}
static DisplayColorFormat t5s3_display_get_color_format(Device*) {
return DISPLAY_COLOR_FORMAT_GRAYSCALE8;
}
static uint16_t t5s3_display_get_resolution_x(Device*) {
return T5S3_EPD_WIDTH;
}
static uint16_t t5s3_display_get_resolution_y(Device*) {
return T5S3_EPD_HEIGHT;
}
// endregion
static const DisplayApi t5s3_display_api = {
// The driver converts into its own framebuffer, so the LVGL draw buffers may live in external RAM
.capabilities = DISPLAY_CAPABILITY_ON_OFF | DISPLAY_CAPABILITY_SLOW_REFRESH | DISPLAY_CAPABILITY_PREFER_EXTERNAL_RAM,
.reset = t5s3_display_reset,
.init = t5s3_display_init,
.draw_bitmap = t5s3_display_draw_bitmap,
.clear = t5s3_display_clear,
.refresh = t5s3_display_refresh,
.mirror = nullptr,
.swap_xy = nullptr,
.get_swap_xy = nullptr,
.get_mirror_x = nullptr,
.get_mirror_y = nullptr,
.set_gap = nullptr,
.get_gap_x = nullptr,
.get_gap_y = nullptr,
.invert_color = nullptr,
.disp_on_off = t5s3_display_disp_on_off,
.disp_sleep = nullptr,
.get_color_format = t5s3_display_get_color_format,
.get_resolution_x = t5s3_display_get_resolution_x,
.get_resolution_y = t5s3_display_get_resolution_y,
// The framebuffer is 4bpp and not in the reported GRAYSCALE8 format
.get_frame_buffer = nullptr,
.get_frame_buffer_count = nullptr,
.get_backlight = nullptr,
.has_capability = nullptr,
};
// region Driver lifecycle
static error_t start(Device* device) {
const auto* config = GET_CONFIG(device);
auto* internal = static_cast<T5s3DisplayInternal*>(malloc(sizeof(T5s3DisplayInternal)));
if (internal == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
internal->powered = false;
if (!t5s3_epd_init(config->pixel_clock_hz)) {
free(internal);
return ERROR_RESOURCE;
}
internal->framebuffer = t5s3_epd_framebuffer();
internal->partial_count_since_quality = 0;
internal->quality_hold_until_tick = 0;
internal->quality_hold_session_start_tick = 0;
device_set_driver_data(device, internal);
// The boot splash leaves ghosting, so clear the panel with the full flash cycle before LVGL draws
power_on(internal);
t5s3_epd_clear();
LOG_I(TAG, "Initialized (%dx%d)", T5S3_EPD_WIDTH, T5S3_EPD_HEIGHT);
return ERROR_NONE;
}
static error_t stop(Device* device) {
auto* internal = static_cast<T5s3DisplayInternal*>(device_get_driver_data(device));
if (internal->powered) {
t5s3_epd_power_off();
internal->powered = false;
}
t5s3_epd_deinit();
free(internal);
device_set_driver_data(device, nullptr);
return ERROR_NONE;
}
// endregion
Driver t5s3_display_driver = {
.name = "t5s3-display",
.compatible = (const char*[]) { "lilygo,t5s3-display", nullptr },
.start_device = start,
.stop_device = stop,
.probe = nullptr,
.api = &t5s3_display_api,
.device_type = &DISPLAY_TYPE,
.owner = &lilygo_t5s3_epd_module,
.internal = nullptr
};
}

View File

@ -0,0 +1,596 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Minimal driver for the ED047TC1 panel of the LILYGO T5 4.7 Inch E-Paper S3.
// The sequences and timings follow the epdiy project (https://github.com/vroland/epdiy).
//
// The panel is controlled by a 74HCT4094 shift register (latch, output enable, mode, start of frame and the
// panel supplies), the CKV gate clock (RMT peripheral) and an 8 bit bus (LCD peripheral in i80 mode) that
// carries the 2 bit actions of 4 pixels per byte. Rows are sent one at a time. The data of a row is applied by the
// CKV pulse after the one that was running during its transfer.
#include "t5s3_epd.h"
#include <tactility/log.h>
#include <driver/gpio.h>
#include <driver/rmt_encoder.h>
#include <driver/rmt_tx.h>
#include <esp_heap_caps.h>
#include <esp_lcd_io_i80.h>
#include <esp_lcd_panel_io.h>
#include <esp_rom_sys.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <hal/gpio_ll.h>
#include <soc/gpio_struct.h>
#include <cstring>
#define TAG "T5s3Epd"
static constexpr gpio_num_t PIN_REGISTER_DATA = GPIO_NUM_13;
static constexpr gpio_num_t PIN_REGISTER_CLOCK = GPIO_NUM_12;
static constexpr gpio_num_t PIN_REGISTER_STROBE = GPIO_NUM_0;
static constexpr gpio_num_t PIN_CKV = GPIO_NUM_38;
// Start pulse of a row, driven as the data/command line of the bus
static constexpr gpio_num_t PIN_STH = GPIO_NUM_40;
// Pixel clock, driven as the write line of the bus
static constexpr gpio_num_t PIN_CKH = GPIO_NUM_41;
static constexpr gpio_num_t BUS_PINS[8] = {GPIO_NUM_6, GPIO_NUM_7, GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_2, GPIO_NUM_3, GPIO_NUM_8, GPIO_NUM_1};
// A row has headroom after the pixel data for the timing of the source driver
static constexpr int BUS_TRANSFER_BYTES = ((T5S3_EPD_WIDTH + 32) / 4 + 3) & ~3;
// Time base of the CKV pulses, 10 MHz
static constexpr uint32_t PULSE_RESOLUTION_HZ = 10000000;
// Low time after the pulse of a row in ticks
static constexpr uint32_t ROW_LOW_TICKS = 50;
// More phases drive the pixels further into black or white, which leaves less ghosting but makes fast updates slower
static constexpr uint32_t FAST_PHASES = 10;
// Hold time of a phase of the fast update in ticks
static constexpr uint32_t FAST_HOLD_TICKS = 1000;
// Hold time of the frames that flash the panel to white
static constexpr uint32_t CLEAR_HOLD_TICKS = 120;
static constexpr int CLEAR_CYCLES = 3;
static constexpr int CLEAR_DARK_FRAMES = 10;
static constexpr int CLEAR_LIGHT_FRAMES = 10;
static constexpr int CLEAR_REST_FRAMES = 2;
static constexpr uint8_t ALL_DARK = 0x55;
static constexpr uint8_t ALL_LIGHT = 0xAA;
// Limit for waiting on a flag that an interrupt handler sets, so that a lost interrupt fails the update instead of spinning forever
static constexpr uint32_t WAIT_SPIN_LIMIT = 20000000;
struct Engine {
uint8_t* front = nullptr;
uint8_t* back = nullptr;
uint8_t* row_buffer[2] = {nullptr, nullptr};
int current = 0;
esp_lcd_i80_bus_handle_t bus = nullptr;
esp_lcd_panel_io_handle_t io = nullptr;
rmt_channel_handle_t pulse_channel = nullptr;
rmt_encoder_handle_t pulse_encoder = nullptr;
volatile bool transfer_done = true;
volatile bool pulse_done = true;
bool failed = false;
// Shift register
bool latch = false;
bool output_enable = false;
bool mode = false;
bool start_of_frame = true;
bool power_disable = true;
bool positive_power = false;
bool negative_power = false;
// Rows written since the last row that carried data
int rows_skipped = 0;
bool row_dirty[T5S3_EPD_HEIGHT];
alignas(4) uint8_t changes[T5S3_EPD_FB_ROW_BYTES];
alignas(4) uint8_t line_mask[T5S3_EPD_BUS_ROW_BYTES];
};
static Engine engine;
static rmt_symbol_word_t pulse_symbol;
static inline void IRAM_ATTR pin_set(gpio_num_t pin, uint32_t level) {
gpio_ll_set_level(GPIO_LL_GET_HW(GPIO_PORT_0), pin, level);
}
static bool IRAM_ATTR on_pulse_done(rmt_channel_handle_t, const rmt_tx_done_event_data_t*, void*) {
engine.pulse_done = true;
return false;
}
static bool IRAM_ATTR on_transfer_done(esp_lcd_panel_io_handle_t, esp_lcd_panel_io_event_data_t*, void*) {
engine.transfer_done = true;
return false;
}
// After a failure nothing is waited for anymore, so the rest of the update ends quickly
static void IRAM_ATTR wait_for(volatile bool& flag) {
uint32_t spins = 0;
while (!flag) {
if (engine.failed || ++spins > WAIT_SPIN_LIMIT) {
engine.failed = true;
flag = true;
}
}
}
// region Shift register
static void IRAM_ATTR push_register_bit(bool bit) {
pin_set(PIN_REGISTER_CLOCK, 0);
pin_set(PIN_REGISTER_DATA, bit ? 1 : 0);
pin_set(PIN_REGISTER_CLOCK, 1);
}
static void IRAM_ATTR push_register() {
pin_set(PIN_REGISTER_STROBE, 0);
push_register_bit(engine.output_enable);
push_register_bit(engine.mode);
// Always set
push_register_bit(true);
push_register_bit(engine.start_of_frame);
push_register_bit(engine.negative_power);
push_register_bit(engine.positive_power);
push_register_bit(engine.power_disable);
push_register_bit(engine.latch);
pin_set(PIN_REGISTER_STROBE, 1);
}
// endregion
// region CKV pulses and row transfer
// Pulses the gate clock for high_ticks and keeps it low for low_ticks, repeated for the given number of pulses.
// Without a high time the line is high for low_ticks.
static void IRAM_ATTR pulse_ckv(uint32_t high_ticks, uint32_t low_ticks, bool wait, int count = 1) {
wait_for(engine.pulse_done);
if (high_ticks > 0) {
pulse_symbol.duration0 = high_ticks;
pulse_symbol.level0 = 1;
pulse_symbol.duration1 = low_ticks;
pulse_symbol.level1 = 0;
} else {
pulse_symbol.duration0 = low_ticks;
pulse_symbol.level0 = 1;
pulse_symbol.duration1 = 0;
pulse_symbol.level1 = 0;
}
engine.pulse_done = false;
rmt_transmit_config_t config = {};
config.loop_count = count;
if (rmt_transmit(engine.pulse_channel, engine.pulse_encoder, &pulse_symbol, sizeof(pulse_symbol), &config) != ESP_OK) {
engine.pulse_done = true;
engine.failed = true;
return;
}
if (wait) {
wait_for(engine.pulse_done);
}
}
static void IRAM_ATTR pulse_ckv_us(uint32_t high_us, uint32_t low_us, bool wait) {
pulse_ckv(high_us * 10, low_us * 10, wait);
}
static void IRAM_ATTR start_transfer() {
engine.transfer_done = false;
if (esp_lcd_panel_io_tx_color(engine.io, 0, engine.row_buffer[engine.current], BUS_TRANSFER_BYTES) != ESP_OK) {
engine.transfer_done = true;
engine.failed = true;
}
}
// Applies the data that was sent last and starts the transfer of the current row buffer while the CKV pulse runs
static void IRAM_ATTR output_row(uint32_t hold_ticks) {
wait_for(engine.transfer_done);
wait_for(engine.pulse_done);
engine.latch = true;
push_register();
engine.latch = false;
push_register();
pulse_ckv(hold_ticks, ROW_LOW_TICKS, false);
start_transfer();
engine.current ^= 1;
}
static void IRAM_ATTR write_row(uint32_t hold_ticks) {
output_row(hold_ticks);
engine.rows_skipped = 0;
}
// Skips a row after rows with data. The first two skipped rows push blank data through the pipeline.
static void IRAM_ATTR skip_row(uint32_t hold_ticks) {
memset(engine.row_buffer[engine.current], 0, T5S3_EPD_BUS_ROW_BYTES);
output_row(hold_ticks);
engine.rows_skipped++;
}
// Clocks the gate driver through rows that carry no data with one train of pulses
static void IRAM_ATTR skip_rows(int count) {
pulse_ckv(45, 5, false, count);
engine.rows_skipped += count;
}
static void IRAM_ATTR start_frame() {
wait_for(engine.transfer_done);
wait_for(engine.pulse_done);
engine.rows_skipped = 0;
engine.mode = true;
push_register();
pulse_ckv_us(1, 1, true);
// The start of frame line must go low and high again while the long pulse runs
engine.start_of_frame = false;
push_register();
pulse_ckv_us(1000, 100, false);
engine.start_of_frame = true;
push_register();
for (int i = 0; i < 4; i++) {
pulse_ckv_us(1, 1, true);
}
engine.output_enable = true;
push_register();
}
static void IRAM_ATTR end_frame() {
engine.start_of_frame = false;
push_register();
for (int i = 0; i < 5; i++) {
pulse_ckv_us(1, 1, true);
}
engine.mode = false;
push_register();
pulse_ckv_us(0, 10, true);
engine.output_enable = false;
push_register();
for (int i = 0; i < 3; i++) {
pulse_ckv_us(1, 1, true);
}
}
// endregion
// region Frames
static void IRAM_ATTR draw_frame(const uint8_t* table, uint32_t hold_ticks) {
start_frame();
int y = 0;
bool ends_with_train = false;
while (y < T5S3_EPD_HEIGHT && !engine.failed) {
ends_with_train = false;
if (engine.row_dirty[y]) {
t5s3_epd_prepare_row(
engine.row_buffer[engine.current],
engine.front + y * T5S3_EPD_FB_ROW_BYTES,
engine.back + y * T5S3_EPD_FB_ROW_BYTES,
table,
engine.line_mask
);
write_row(hold_ticks);
y++;
} else if (engine.rows_skipped < 2) {
skip_row(hold_ticks);
y++;
} else {
int run = 1;
while (y + run < T5S3_EPD_HEIGHT && !engine.row_dirty[y + run]) {
run++;
}
skip_rows(run);
ends_with_train = true;
y += run;
}
}
// The last row is applied by one more pulse
if (engine.rows_skipped == 0 && !engine.failed) {
write_row(hold_ticks);
} else if (ends_with_train) {
// The end of the frame must not overlap the train of pulses of the skipped rows
wait_for(engine.pulse_done);
}
end_frame();
}
static void IRAM_ATTR draw_uniform_frame(uint8_t pattern, uint32_t hold_ticks) {
memset(engine.row_buffer[0], pattern, T5S3_EPD_BUS_ROW_BYTES);
memset(engine.row_buffer[1], pattern, T5S3_EPD_BUS_ROW_BYTES);
start_frame();
for (int y = 0; y < T5S3_EPD_HEIGHT && !engine.failed; y++) {
write_row(hold_ticks);
}
if (!engine.failed) {
write_row(hold_ticks);
}
end_frame();
}
// endregion
// region Setup
static bool setup_register_pins() {
gpio_config_t config = {};
config.pin_bit_mask = (1ULL << PIN_REGISTER_DATA) | (1ULL << PIN_REGISTER_CLOCK) | (1ULL << PIN_REGISTER_STROBE);
config.mode = GPIO_MODE_OUTPUT;
if (gpio_config(&config) != ESP_OK) {
return false;
}
pin_set(PIN_REGISTER_STROBE, 0);
return true;
}
static bool setup_bus(uint32_t pixel_clock_hz) {
esp_lcd_i80_bus_config_t bus_config = {};
bus_config.dc_gpio_num = PIN_STH;
bus_config.wr_gpio_num = PIN_CKH;
bus_config.clk_src = LCD_CLK_SRC_DEFAULT;
for (int i = 0; i < 8; i++) {
bus_config.data_gpio_nums[i] = BUS_PINS[i];
}
bus_config.bus_width = 8;
bus_config.max_transfer_bytes = BUS_TRANSFER_BYTES;
if (esp_lcd_new_i80_bus(&bus_config, &engine.bus) != ESP_OK) {
return false;
}
// The data/command line is low when idle, high during a dummy command phase and low during the data of a row
esp_lcd_panel_io_i80_config_t io_config = {};
io_config.cs_gpio_num = GPIO_NUM_NC;
io_config.pclk_hz = pixel_clock_hz;
io_config.trans_queue_depth = 4;
io_config.on_color_trans_done = on_transfer_done;
io_config.lcd_cmd_bits = 10;
io_config.lcd_param_bits = 0;
io_config.dc_levels.dc_idle_level = 0;
io_config.dc_levels.dc_cmd_level = 1;
io_config.dc_levels.dc_dummy_level = 0;
io_config.dc_levels.dc_data_level = 0;
return esp_lcd_new_panel_io_i80(engine.bus, &io_config, &engine.io) == ESP_OK;
}
static bool setup_pulse() {
rmt_tx_channel_config_t channel_config = {};
channel_config.gpio_num = PIN_CKV;
channel_config.clk_src = RMT_CLK_SRC_DEFAULT;
channel_config.resolution_hz = PULSE_RESOLUTION_HZ;
channel_config.mem_block_symbols = 48;
channel_config.trans_queue_depth = 1;
if (rmt_new_tx_channel(&channel_config, &engine.pulse_channel) != ESP_OK) {
return false;
}
rmt_copy_encoder_config_t encoder_config = {};
if (rmt_new_copy_encoder(&encoder_config, &engine.pulse_encoder) != ESP_OK) {
return false;
}
rmt_tx_event_callbacks_t callbacks = {};
callbacks.on_trans_done = on_pulse_done;
if (rmt_tx_register_event_callbacks(engine.pulse_channel, &callbacks, nullptr) != ESP_OK) {
return false;
}
return rmt_enable(engine.pulse_channel) == ESP_OK;
}
bool t5s3_epd_init(uint32_t pixel_clock_hz) {
if (engine.front != nullptr) {
return true;
}
engine.front = static_cast<uint8_t*>(heap_caps_aligned_alloc(16, T5S3_EPD_FB_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
engine.back = static_cast<uint8_t*>(heap_caps_aligned_alloc(16, T5S3_EPD_FB_BYTES, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
for (auto& buffer: engine.row_buffer) {
buffer = static_cast<uint8_t*>(heap_caps_aligned_calloc(16, 1, BUS_TRANSFER_BYTES, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
}
if (engine.front == nullptr || engine.back == nullptr || engine.row_buffer[0] == nullptr || engine.row_buffer[1] == nullptr) {
LOG_E(TAG, "Out of memory");
t5s3_epd_deinit();
return false;
}
memset(engine.front, 0xFF, T5S3_EPD_FB_BYTES);
memset(engine.back, 0xFF, T5S3_EPD_FB_BYTES);
if (!setup_register_pins() || !setup_bus(pixel_clock_hz) || !setup_pulse()) {
LOG_E(TAG, "Failed to set up the pins and peripherals");
t5s3_epd_deinit();
return false;
}
engine.latch = false;
engine.output_enable = false;
engine.mode = false;
engine.start_of_frame = true;
engine.power_disable = true;
engine.positive_power = false;
engine.negative_power = false;
engine.current = 0;
engine.transfer_done = true;
engine.pulse_done = true;
push_register();
return true;
}
void t5s3_epd_deinit() {
if (engine.pulse_channel != nullptr) {
// Both calls fail harmlessly when the channel was not enabled
rmt_disable(engine.pulse_channel);
rmt_del_channel(engine.pulse_channel);
engine.pulse_channel = nullptr;
}
if (engine.pulse_encoder != nullptr) {
rmt_del_encoder(engine.pulse_encoder);
engine.pulse_encoder = nullptr;
}
if (engine.io != nullptr) {
esp_lcd_panel_io_del(engine.io);
engine.io = nullptr;
}
if (engine.bus != nullptr) {
esp_lcd_del_i80_bus(engine.bus);
engine.bus = nullptr;
}
heap_caps_free(engine.front);
heap_caps_free(engine.back);
heap_caps_free(engine.row_buffer[0]);
heap_caps_free(engine.row_buffer[1]);
engine.front = nullptr;
engine.back = nullptr;
engine.row_buffer[0] = nullptr;
engine.row_buffer[1] = nullptr;
}
// endregion
// region Power
void t5s3_epd_power_on() {
engine.power_disable = false;
push_register();
esp_rom_delay_us(100);
engine.negative_power = true;
push_register();
esp_rom_delay_us(500);
engine.positive_power = true;
push_register();
esp_rom_delay_us(100);
engine.start_of_frame = true;
push_register();
}
void t5s3_epd_power_off() {
engine.positive_power = false;
push_register();
esp_rom_delay_us(10);
engine.negative_power = false;
push_register();
esp_rom_delay_us(100);
engine.start_of_frame = false;
engine.output_enable = false;
engine.mode = false;
engine.power_disable = true;
push_register();
}
// endregion
// region Updates
uint8_t* t5s3_epd_framebuffer() {
return engine.front;
}
void t5s3_epd_fill_white() {
memset(engine.front, 0xFF, T5S3_EPD_FB_BYTES);
}
void t5s3_epd_invalidate() {
auto* words = reinterpret_cast<uint32_t*>(engine.back);
for (int i = 0; i < T5S3_EPD_FB_BYTES / 4; i++) {
words[i] = ~words[i];
}
}
bool t5s3_epd_update(T5s3EpdMode mode, int32_t y_start, int32_t y_end) {
if (y_start < 0) {
y_start = 0;
}
if (y_end > T5S3_EPD_HEIGHT) {
y_end = T5S3_EPD_HEIGHT;
}
if (y_start >= y_end) {
return true;
}
const int64_t started = esp_timer_get_time();
// Find the rows and columns that changed
memset(engine.row_dirty, 0, sizeof(engine.row_dirty));
memset(engine.changes, 0, sizeof(engine.changes));
auto* changes = reinterpret_cast<uint32_t*>(engine.changes);
bool any_changes = false;
for (int y = y_start; y < y_end; y++) {
const auto* to_row = reinterpret_cast<const uint32_t*>(engine.front + y * T5S3_EPD_FB_ROW_BYTES);
const auto* from_row = reinterpret_cast<const uint32_t*>(engine.back + y * T5S3_EPD_FB_ROW_BYTES);
uint32_t row_changes = 0;
for (int i = 0; i < T5S3_EPD_FB_ROW_BYTES / 4; i++) {
const uint32_t difference = to_row[i] ^ from_row[i];
changes[i] |= difference;
row_changes |= difference;
}
engine.row_dirty[y] = row_changes != 0;
any_changes = any_changes || row_changes != 0;
}
if (!any_changes) {
return true;
}
t5s3_epd_build_line_mask(engine.line_mask, engine.changes);
engine.failed = false;
uint8_t table[256];
if (mode == T5s3EpdMode::Fast) {
t5s3_epd_build_fast_table(table);
for (uint32_t phase = 0; phase < FAST_PHASES && !engine.failed; phase++) {
draw_frame(table, FAST_HOLD_TICKS);
taskYIELD();
}
} else {
// Long updates sleep between frames so that the idle task can feed the watchdog
for (int phase = 0; phase < T5S3_EPD_QUALITY_PHASES && !engine.failed; phase++) {
t5s3_epd_build_quality_table(table, phase, mode == T5s3EpdMode::Full);
draw_frame(table, T5S3_EPD_QUALITY_HOLD[phase]);
vTaskDelay(1);
}
}
// A failed update leaves the back buffer alone, so the next update draws the same rows again
if (engine.failed) {
LOG_E(TAG, "Update failed, a peripheral did not respond");
return false;
}
for (int y = y_start; y < y_end; y++) {
if (engine.row_dirty[y]) {
memcpy(engine.back + y * T5S3_EPD_FB_ROW_BYTES, engine.front + y * T5S3_EPD_FB_ROW_BYTES, T5S3_EPD_FB_ROW_BYTES);
}
}
const int elapsed_ms = static_cast<int>((esp_timer_get_time() - started) / 1000);
// Quality and slow updates are logged at info level, frequent fast updates would flood the log
if (mode != T5s3EpdMode::Fast || elapsed_ms > 150) {
LOG_I(TAG, "Updated rows %d to %d in %d ms", static_cast<int>(y_start), static_cast<int>(y_end), elapsed_ms);
} else {
LOG_D(TAG, "Updated rows %d to %d in %d ms", static_cast<int>(y_start), static_cast<int>(y_end), elapsed_ms);
}
return true;
}
static void draw_clear_frames(uint8_t pattern, int count) {
for (int i = 0; i < count && !engine.failed; i++) {
draw_uniform_frame(pattern, CLEAR_HOLD_TICKS);
vTaskDelay(1);
}
}
bool t5s3_epd_clear() {
const int64_t started = esp_timer_get_time();
t5s3_epd_fill_white();
if (!t5s3_epd_update(T5s3EpdMode::Full, 0, T5S3_EPD_HEIGHT)) {
return false;
}
engine.failed = false;
for (int cycle = 0; cycle < CLEAR_CYCLES; cycle++) {
draw_clear_frames(ALL_DARK, CLEAR_DARK_FRAMES);
draw_clear_frames(ALL_LIGHT, CLEAR_LIGHT_FRAMES);
draw_clear_frames(0, CLEAR_REST_FRAMES);
}
if (engine.failed) {
LOG_E(TAG, "Clear failed, a peripheral did not respond");
return false;
}
LOG_I(TAG, "Cleared in %d ms", static_cast<int>((esp_timer_get_time() - started) / 1000));
return true;
}
// endregion

View File

@ -0,0 +1,54 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Minimal driver for the ED047TC1 panel of the LILYGO T5 4.7 Inch E-Paper S3.
// The calls must be serialized by the user, the driver has no locking.
#pragma once
#include "t5s3_epd_rows.h"
#include <cstdint>
enum class T5s3EpdMode {
// Black and white only
Fast,
// 16 levels in 30 phases, white pixels that stay white are not touched
Quality,
// 16 levels in 30 phases, white pixels that stay white are flashed too, which clears ghosting
Full
};
// Allocates the frame buffers and sets up the pins and peripherals. The panel stays unpowered.
bool t5s3_epd_init(uint32_t pixel_clock_hz);
void t5s3_epd_deinit();
// Frame buffer to draw in, 4 bits per pixel. The panel shows it after t5s3_epd_update().
uint8_t* t5s3_epd_framebuffer();
void t5s3_epd_power_on();
void t5s3_epd_power_off();
// Drives all pixels that changed in the rows [y_start, y_end) since the last update. Needs the panel to be powered.
bool t5s3_epd_update(T5s3EpdMode mode, int32_t y_start, int32_t y_end);
void t5s3_epd_fill_white();
// Makes the next update drive every pixel
void t5s3_epd_invalidate();
// Flashes the whole panel to white. Needs the panel to be powered.
bool t5s3_epd_clear();
inline void t5s3_epd_set_pixel(uint8_t* framebuffer, int32_t x, int32_t y, uint8_t level) {
if (x < 0 || x >= T5S3_EPD_WIDTH || y < 0 || y >= T5S3_EPD_HEIGHT) {
return;
}
uint8_t* byte = &framebuffer[y * T5S3_EPD_FB_ROW_BYTES + x / 2];
if (x % 2) {
*byte = (*byte & 0x0F) | static_cast<uint8_t>(level << 4);
} else {
*byte = (*byte & 0xF0) | (level & 0x0F);
}
}

View File

@ -0,0 +1,106 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Conversion of framebuffer rows into the data the source driver of the ED047TC1 panel expects.
// This code has no hardware access.
#pragma once
#include "t5s3_epd_waveform.h"
#include <cstdint>
#ifdef ESP_PLATFORM
#include <esp_attr.h>
#define T5S3_EPD_IRAM IRAM_ATTR
#else
#define T5S3_EPD_IRAM
#endif
constexpr int T5S3_EPD_WIDTH = 960;
constexpr int T5S3_EPD_HEIGHT = 540;
// Framebuffer rows hold 4 bits per pixel with the even pixel of a pair in the low nibble, 0x0 is black and 0xF is white
constexpr int T5S3_EPD_FB_ROW_BYTES = T5S3_EPD_WIDTH / 2;
constexpr int T5S3_EPD_FB_BYTES = T5S3_EPD_FB_ROW_BYTES * T5S3_EPD_HEIGHT;
// Rows on the bus hold 2 bits per pixel with the first of four pixels in the lowest bits
constexpr int T5S3_EPD_BUS_ROW_BYTES = T5S3_EPD_WIDTH / 4;
constexpr uint8_t T5S3_EPD_ACTION_HOLD = 0;
constexpr uint8_t T5S3_EPD_ACTION_DARK = 1;
constexpr uint8_t T5S3_EPD_ACTION_LIGHT = 2;
// Tables map (target level << 4 | source level) to the action for one phase of an update.
// Fast update: pixels that end up black are driven dark and pixels that end up white are driven light
inline void t5s3_epd_build_fast_table(uint8_t* table) {
for (int to = 0; to < 16; to++) {
for (int from = 0; from < 16; from++) {
uint8_t action = T5S3_EPD_ACTION_HOLD;
if (to == 0 && from != 0) {
action = T5S3_EPD_ACTION_DARK;
} else if (to == 15 && from != 15) {
action = T5S3_EPD_ACTION_LIGHT;
}
table[(to << 4) | from] = action;
}
}
}
// The waveform drives white pixels that stay white with a dark and a light pulse, which flashes them.
// Holding them instead leaves the rest of an area alone, so an update of a part of the screen does not leave visible columns.
inline void t5s3_epd_build_quality_table(uint8_t* table, int phase, bool flash_white) {
const uint8_t* packed = T5S3_EPD_QUALITY_LUT[phase];
for (int to = 0; to < 16; to++) {
for (int group = 0; group < 4; group++) {
const uint8_t actions = *packed++;
const int index = (to << 4) | (group * 4);
table[index] = (actions >> 6) & 3;
table[index + 1] = (actions >> 4) & 3;
table[index + 2] = (actions >> 2) & 3;
table[index + 3] = actions & 3;
}
}
if (!flash_white) {
table[(15 << 4) | 15] = T5S3_EPD_ACTION_HOLD;
}
}
// Keeps the pixels of changed columns and holds all others. A framebuffer byte of the changes holds the XOR of two pixels.
inline void t5s3_epd_build_line_mask(uint8_t* mask, const uint8_t* changes) {
for (int i = 0; i < T5S3_EPD_BUS_ROW_BYTES; i++) {
const uint8_t first = changes[2 * i];
const uint8_t second = changes[2 * i + 1];
uint8_t value = 0;
value |= (first & 0x0F) != 0 ? 0x03 : 0x00;
value |= (first & 0xF0) != 0 ? 0x0C : 0x00;
value |= (second & 0x0F) != 0 ? 0x30 : 0x00;
value |= (second & 0xF0) != 0 ? 0xC0 : 0x00;
mask[i] = value;
}
}
// Converts one row of target levels and one row of source levels into bus data.
// All pointers must be 4-byte aligned. Four framebuffer bytes (8 pixels) become two bus bytes per step.
inline void T5S3_EPD_IRAM t5s3_epd_prepare_row(
uint8_t* out,
const uint8_t* to_row,
const uint8_t* from_row,
const uint8_t* table,
const uint8_t* mask
) {
const auto* to_words = reinterpret_cast<const uint32_t*>(to_row);
const auto* from_words = reinterpret_cast<const uint32_t*>(from_row);
const auto* mask_words = reinterpret_cast<const uint16_t*>(mask);
auto* out_words = reinterpret_cast<uint16_t*>(out);
for (int i = 0; i < T5S3_EPD_FB_ROW_BYTES / 4; i++) {
const uint32_t to = to_words[i];
const uint32_t from = from_words[i];
// Table index of a pixel is (target level << 4) | source level, the even pixel of a byte is in its low nibble
const uint32_t even = ((to & 0x0F0F0F0F) << 4) | (from & 0x0F0F0F0F);
const uint32_t odd = (to & 0xF0F0F0F0) | ((from >> 4) & 0x0F0F0F0F);
const uint32_t first = table[even & 0xFF] | (table[odd & 0xFF] << 2) | (table[(even >> 8) & 0xFF] << 4) | (table[(odd >> 8) & 0xFF] << 6);
const uint32_t second = table[(even >> 16) & 0xFF] | (table[(odd >> 16) & 0xFF] << 2) | (table[even >> 24] << 4) | (table[odd >> 24] << 6);
out_words[i] = static_cast<uint16_t>((first | (second << 8)) & mask_words[i]);
}
}

View File

@ -0,0 +1,201 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
//
// Drive data of the ED047TC1 panel for the 16 level update, taken from the epdiy project
// (https://github.com/vroland/epdiy, built-in waveform epdiy_ED047TC1, mode GC16).
// This file is under the LGPL v3.0 or later like epdiy itself.
#pragma once
#include <cstdint>
constexpr int T5S3_EPD_QUALITY_PHASES = 30;
// Hold time of each phase in units of 0.1 microsecond
constexpr uint16_t T5S3_EPD_QUALITY_HOLD[T5S3_EPD_QUALITY_PHASES] = {
30, 30, 20, 20, 30, 30, 30, 40, 40, 50, 50, 50, 100, 200, 300, 10, 10, 8, 8, 8, 8, 8, 10, 10, 15, 15, 20, 20, 100, 300
};
// Per phase: 16 target levels with 4 bytes each. A byte holds the 2 bit actions of 4 source levels,
// the lowest source level in the top bits. Action 0 holds the pixel, 1 drives it dark and 2 light.
constexpr uint8_t T5S3_EPD_QUALITY_LUT[T5S3_EPD_QUALITY_PHASES][64] = {
{
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
},
{
0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05,
0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05,
0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05,
0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05,
},
{
0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15,
0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15,
0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15,
0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15, 0x00, 0x00, 0x00, 0x15,
},
{
0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55,
0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55, 0x00, 0x00, 0x00, 0x55,
},
{
0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55,
0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55,
0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55,
0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55, 0x00, 0x00, 0x01, 0x55,
},
{
0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55,
0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55,
0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55,
0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55, 0x00, 0x00, 0x05, 0x55,
},
{
0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55,
0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55,
0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55,
0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55, 0x00, 0x00, 0x15, 0x55,
},
{
0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55,
0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55,
0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55,
0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55, 0x00, 0x00, 0x55, 0x55,
},
{
0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55,
0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55,
0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55,
0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55, 0x00, 0x01, 0x55, 0x55,
},
{
0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55,
0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55,
0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55,
0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55, 0x00, 0x05, 0x55, 0x55,
},
{
0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55,
0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55,
0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55,
0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55, 0x00, 0x15, 0x55, 0x55,
},
{
0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55,
0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55,
0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55,
0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55, 0x00, 0x55, 0x55, 0x55,
},
{
0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55,
0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55,
0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55,
0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55, 0x01, 0x55, 0x55, 0x55,
},
{
0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55,
0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55,
0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55,
0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55, 0x05, 0x55, 0x55, 0x55,
},
{
0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55,
0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55,
0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55,
0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55, 0x15, 0x55, 0x55, 0x55,
},
{
0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA,
},
{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA,
},
};

View File

@ -5,7 +5,7 @@ if (DEFINED ENV{ESP_IDF_VERSION})
SRC_DIRS "Source/"
INCLUDE_DIRS "Source/"
PRIV_INCLUDE_DIRS "Private/"
REQUIRES lvgl
REQUIRES lvgl lodepng-module
)
else()
@ -25,6 +25,6 @@ else()
)
target_link_libraries(lv_screenshot
PUBLIC lvgl
PUBLIC lvgl lodepng-module
)
endif()

View File

@ -1,11 +1,32 @@
#include "src/libs/lodepng/lodepng.h"
#include "save_png.h"
#include <lodepng/lodepng.h>
#include <lvgl.h>
bool lv_screenshot_save_png_file(const uint8_t* image, uint32_t w, uint32_t h, uint32_t bpp, const char* filename) {
unsigned char* png = NULL;
size_t png_size = 0;
unsigned error;
if (bpp == 32) {
return lodepng_encode32_file(filename, image, w, h) == 0;
error = lodepng_encode32(&png, &png_size, image, w, h);
} else if (bpp == 24) {
return lodepng_encode24_file(filename, image, w, h) == 0;
error = lodepng_encode24(&png, &png_size, image, w, h);
} else {
return false;
}
if (error) {
lodepng_free(png);
return false;
}
// Written through lv_fs, so LVGL drive letter paths ("A:...") work
bool success = false;
lv_fs_file_t file;
if (lv_fs_open(&file, filename, LV_FS_MODE_WR) == LV_FS_RES_OK) {
uint32_t written = 0;
success = lv_fs_write(&file, png, png_size, &written) == LV_FS_RES_OK && written == png_size;
lv_fs_close(&file);
}
lodepng_free(png);
return success;
}

View File

@ -0,0 +1,17 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
list(APPEND REQUIRES_LIST
TactilityKernel
graphics-module
)
tactility_add_module(binfont-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
PRIV_INCLUDE_DIRS private/
REQUIRES ${REQUIRES_LIST}
)

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,14 @@
# binfont-module
Bitmap fonts in the [lv_font_conv](https://github.com/lvgl/lv_font_conv) binary format (`--format bin`), usable with and without LVGL.
- `<binfont/binfont.h>`: load fonts and decode glyphs
- `<binfont/generator.h>`: rasterize a TTF into the binary format on device
- `<binfont/render.h>`: draw glyphs and text into a `PixelBuffer`
For LVGL, see `lvgl_binfont_create()` in lvgl-module's `<lvgl/binfont.h>`.
## License
This module is licensed under the [Apache v2.0](LICENSE-Apache-2.0.md) license.
Uses [stb_truetype](https://github.com/nothings/stb) (public domain).

View File

@ -0,0 +1,118 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* A bitmap font in the lv_font_conv binary format (lv_font_conv --format bin).
* Thread-safe: a font can be used from multiple tasks at the same time.
*/
struct BinFont;
typedef struct BinFontMetrics {
/** Font size in pixels, as passed to the converter */
uint16_t size;
/** Pixels above the baseline */
int16_t ascent;
/** Pixels below the baseline (negative) */
int16_t descent;
/** ascent - descent */
uint16_t line_height;
/** Distance from the bottom of the line to the baseline (-descent) */
int16_t base_line;
int16_t underline_position;
uint16_t underline_thickness;
/** Bits per pixel of the stored bitmaps (1-4) */
uint8_t bpp;
} BinFontMetrics;
typedef struct BinFontGlyph {
uint32_t glyph_id;
/** Horizontal advance in 1/16th pixels */
uint32_t advance_x16;
/** Bounding box left, relative to the pen position */
int16_t x;
/** Bounding box bottom, relative to the baseline (positive is up) */
int16_t y;
uint16_t width;
uint16_t height;
} BinFontGlyph;
/**
* @brief Opens a font file.
* The whole file is loaded into external memory (PSRAM) when available, otherwise into internal memory.
* When that allocation fails, only the lookup tables are loaded and glyph data is read from the file
* on demand: the file is opened and closed for every glyph lookup, unless it's kept open with
* binfont_begin()/binfont_end().
* @param[in] path the file path
* @param[out] out the opened font
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND when the file can't be opened
* @retval ERROR_NOT_SUPPORTED when the file is not a valid or supported font
* @retval ERROR_OUT_OF_MEMORY when memory allocation failed
*/
error_t binfont_open_file(const char* path, struct BinFont** out);
/**
* @brief Opens a font from memory.
* @param[in] data the font file contents, which must stay valid until binfont_close()
* @param[in] size the size of data in bytes
* @param[in] take_ownership when true, data is released with memory_free() by binfont_close() (also on failure)
* @param[out] out the opened font
* @retval ERROR_NONE on success
* @retval ERROR_NOT_SUPPORTED when the data is not a valid or supported font
* @retval ERROR_OUT_OF_MEMORY when memory allocation failed
*/
error_t binfont_open_memory(const void* data, size_t size, bool take_ownership, struct BinFont** out);
/** Releases the font and all its resources. */
void binfont_close(struct BinFont* font);
/**
* @brief Keeps the font file open until the matching binfont_end(), so rendering many glyphs
* doesn't open and close the file for each of them. Calls can be nested and used from multiple tasks.
* Does nothing for fonts that are fully loaded in memory.
* @retval ERROR_NONE on success
* @retval ERROR_NOT_FOUND when the file can't be opened
*/
error_t binfont_begin(struct BinFont* font);
/** Ends a binfont_begin() session. The file is closed when no sessions remain. */
void binfont_end(struct BinFont* font);
void binfont_get_metrics(const struct BinFont* font, BinFontMetrics* out);
/**
* @param[in] font the font
* @param[in] codepoint the Unicode codepoint
* @param[out] out the glyph info
* @return false when the font has no glyph for this codepoint
*/
bool binfont_get_glyph(struct BinFont* font, uint32_t codepoint, BinFontGlyph* out);
/** @return the kerning adjustment for a glyph pair in 1/16th pixels, or 0 when there is none */
int32_t binfont_get_kerning_x16(struct BinFont* font, uint32_t left_glyph_id, uint32_t right_glyph_id);
/**
* @brief Decodes a glyph bitmap as 8-bit alpha (0 = transparent, 255 = opaque).
* @param[in] font the font
* @param[in] glyph a glyph from binfont_get_glyph()
* @param[out] out the destination: glyph->height rows of glyph->width bytes
* @param[in] stride bytes per row in out (at least glyph->width)
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT when stride is too small
* @retval ERROR_RESOURCE when the glyph data can't be read
*/
error_t binfont_get_glyph_bitmap(struct BinFont* font, const BinFontGlyph* glyph, uint8_t* out, size_t stride);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,40 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/error.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
typedef struct BinFontGeneratorConfig {
/** Path to a TrueType (.ttf) file */
const char* ttf_path;
/** Font size in pixels (em height) */
uint16_t size;
/** Bits per pixel: 1, 2, 3 or 4 */
uint8_t bpp;
/** Codepoints to include. Codepoints that the TTF doesn't contain are skipped. */
const uint32_t* codepoints;
size_t codepoint_count;
} BinFontGeneratorConfig;
/**
* @brief Rasterizes a TrueType font into the lv_font_conv binary format.
* @param[in] config the generator configuration
* @param[out] out_data the generated font file contents, to be released with memory_free()
* @param[out] out_size the size of out_data in bytes
* @retval ERROR_NONE on success
* @retval ERROR_INVALID_ARGUMENT when the configuration is invalid
* @retval ERROR_NOT_FOUND when the TTF file can't be read
* @retval ERROR_NOT_SUPPORTED when the TTF file can't be parsed or contains none of the codepoints
* @retval ERROR_OUT_OF_MEMORY when memory allocation failed
*/
error_t binfont_generate(const BinFontGeneratorConfig* config, uint8_t** out_data, size_t* out_size);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module binfont_module;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,57 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <binfont/binfont.h>
#include <graphics/pixel_buffer.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Alpha-blends one glyph over the existing pixels of a PixelBuffer.
* @param[out] buffer destination, in any pixel format
* @param[in] x pen position x
* @param[in] baseline_y pen position y (the baseline)
* @param[in] font the font
* @param[in] codepoint the Unicode codepoint
* @param[in] color glyph colour as RGB565
* @param[in] conversion how colours reduce into the buffer's native format (see PixelBufferConversion)
* @return the horizontal advance in pixels, or 0 when the font has no glyph for the codepoint
*/
int binfont_draw_glyph_rgb565(
struct PixelBuffer* buffer,
int x,
int baseline_y,
struct BinFont* font,
uint32_t codepoint,
uint16_t color,
enum PixelBufferConversion conversion
);
/**
* @brief Alpha-blends a UTF-8 string over the existing pixels of a PixelBuffer, applying kerning.
* @param[out] buffer destination, in any pixel format
* @param[in] x pen position x
* @param[in] baseline_y pen position y (the baseline)
* @param[in] font the font
* @param[in] text UTF-8 encoded, null-terminated text
* @param[in] color glyph colour as RGB565
* @param[in] conversion how colours reduce into the buffer's native format (see PixelBufferConversion)
* @return the total advance in pixels
*/
int binfont_draw_text_rgb565(
struct PixelBuffer* buffer,
int x,
int baseline_y,
struct BinFont* font,
const char* text,
uint16_t color,
enum PixelBufferConversion conversion
);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <binfont/binfont.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* @brief Opens a font file without loading it into memory: only the lookup tables are loaded and
* glyph data is read from the file on demand. binfont_open_file() falls back to this when the file
* doesn't fit in memory.
*/
error_t binfont_open_file_streaming(const char* path, struct BinFont** out);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,803 @@
// SPDX-License-Identifier: Apache-2.0
#include <binfont/binfont.h>
#include <binfont_private.h>
#include <tactility/concurrent/mutex.h>
#include <tactility/memory.h>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <initializer_list>
namespace {
constexpr size_t RECORD_HEADER_SIZE = 8;
constexpr size_t HEAD_MIN_SIZE = 48;
constexpr size_t CMAP_SUBTABLE_HEADER_SIZE = 16;
constexpr size_t GLYPH_HEADER_MAX_BYTES = 16;
enum CmapFormat : uint8_t {
CMAP_FORMAT_0 = 0,
CMAP_FORMAT_SPARSE = 1,
CMAP_FORMAT_0_TINY = 2,
CMAP_FORMAT_SPARSE_TINY = 3,
};
enum Compression : uint8_t {
COMPRESSION_NONE = 0,
COMPRESSION_RLE_PREFILTER = 1,
COMPRESSION_RLE = 2,
};
uint16_t read_u16(const uint8_t* p) {
return static_cast<uint16_t>(p[0] | (p[1] << 8));
}
int16_t read_i16(const uint8_t* p) {
return static_cast<int16_t>(read_u16(p));
}
uint32_t read_u32(const uint8_t* p) {
return static_cast<uint32_t>(p[0]) | (static_cast<uint32_t>(p[1]) << 8) | (static_cast<uint32_t>(p[2]) << 16) | (static_cast<uint32_t>(p[3]) << 24);
}
bool has_marker(const uint8_t* record, const char* marker) {
return memcmp(record + 4, marker, 4) == 0;
}
/** MSB-first bit reader. Bits past the end read as 0. */
class BitReader {
const uint8_t* data;
size_t sizeBits;
size_t position;
public:
BitReader(const uint8_t* data, size_t size, size_t bitOffset = 0) : data(data), sizeBits(size * 8), position(bitOffset) {}
uint32_t read(uint8_t bits) {
uint32_t value = 0;
for (uint8_t i = 0; i < bits; i++) {
value <<= 1;
if (position < sizeBits) {
value |= (data[position >> 3] >> (7 - (position & 7))) & 1U;
}
position++;
}
return value;
}
int32_t readSigned(uint8_t bits) {
if (bits == 0) {
return 0;
}
const uint32_t value = read(bits);
if (value & (1U << (bits - 1))) {
return static_cast<int32_t>(value | (~0U << bits));
}
return static_cast<int32_t>(value);
}
};
/** Decoder for the modified I3BN RLE stream of lv_font_conv. */
class RleReader {
enum class State { Single, Repeated, Counter };
BitReader& bits;
uint8_t bpp;
State state = State::Single;
bool first = true;
uint8_t previous = 0;
uint32_t count = 0;
uint8_t readSingle() {
previous = static_cast<uint8_t>(bits.read(bpp));
state = State::Single;
return previous;
}
public:
RleReader(BitReader& bits, uint8_t bpp) : bits(bits), bpp(bpp) {}
uint8_t next() {
switch (state) {
case State::Single: {
const auto value = static_cast<uint8_t>(bits.read(bpp));
if (!first && value == previous) {
count = 0;
state = State::Repeated;
}
first = false;
previous = value;
return value;
}
case State::Repeated: {
count++;
if (bits.read(1) == 0) {
return readSingle();
}
if (count == 11) {
count = bits.read(6);
if (count != 0) {
state = State::Counter;
} else {
return readSingle();
}
}
return previous;
}
case State::Counter:
count--;
if (count == 0) {
return readSingle();
}
return previous;
}
return 0;
}
};
struct Header {
uint16_t fontSize;
int16_t ascent;
int16_t descent;
uint16_t defaultAdvanceWidth;
uint16_t kerningScale;
uint8_t indexToLocFormat;
uint8_t glyphIdFormat;
uint8_t advanceWidthFormat;
uint8_t bpp;
uint8_t xyBits;
uint8_t whBits;
uint8_t advanceWidthBits;
uint8_t compression;
uint8_t subpixelMode;
int16_t underlinePosition;
uint16_t underlineThickness;
};
} // namespace
struct BinFont {
Header header;
/** The whole file in memory mode, nullptr in stream mode */
const uint8_t* data;
bool ownsData;
/** Stream mode only: the file path, and the file while it's open (guarded by mutex) */
char* path;
FILE* file;
uint32_t sessionCount;
Mutex mutex;
/** Owned copy of the cmap/loca/kern records in stream mode */
uint8_t* tables;
/** Each record pointer includes its 8-byte record header */
const uint8_t* cmap;
size_t cmapSize;
const uint8_t* loca;
size_t locaSize;
uint32_t locaCount;
const uint8_t* kern;
size_t kernSize;
size_t glyfOffset;
size_t glyfSize;
};
namespace {
bool read_at(BinFont* font, size_t offset, uint8_t* out, size_t size) {
if (fseek(font->file, static_cast<long>(offset), SEEK_SET) != 0) {
return false;
}
return fread(out, 1, size, font->file) == size;
}
bool parse_header(const uint8_t* record, size_t size, Header& header) {
if (size < HEAD_MIN_SIZE || !has_marker(record, "head")) {
return false;
}
header.fontSize = read_u16(record + 14);
header.ascent = read_i16(record + 16);
header.descent = read_i16(record + 18);
header.defaultAdvanceWidth = read_u16(record + 30);
header.kerningScale = read_u16(record + 32);
header.indexToLocFormat = record[34];
header.glyphIdFormat = record[35];
header.advanceWidthFormat = record[36];
header.bpp = record[37];
header.xyBits = record[38];
header.whBits = record[39];
header.advanceWidthBits = record[40];
header.compression = record[41];
header.subpixelMode = record[42];
header.underlinePosition = read_i16(record + 44);
header.underlineThickness = read_u16(record + 46);
return header.bpp >= 1 && header.bpp <= 4 &&
header.compression <= COMPRESSION_RLE &&
header.subpixelMode == 0 &&
header.indexToLocFormat <= 1 &&
header.xyBits <= 16 && header.whBits <= 16 && header.advanceWidthBits <= 32;
}
bool validate_cmap(const uint8_t* cmap, size_t size) {
if (size < 12) {
return false;
}
const uint32_t count = read_u32(cmap + 8);
if (count > (size - 12) / CMAP_SUBTABLE_HEADER_SIZE) {
return false;
}
for (uint32_t i = 0; i < count; i++) {
const uint8_t* subtable = cmap + 12 + i * CMAP_SUBTABLE_HEADER_SIZE;
const size_t data_offset = read_u32(subtable);
const size_t range_length = read_u16(subtable + 8);
const size_t entries = read_u16(subtable + 12);
size_t data_size;
switch (subtable[14]) {
case CMAP_FORMAT_0: data_size = range_length; break;
case CMAP_FORMAT_SPARSE: data_size = entries * 4; break;
case CMAP_FORMAT_0_TINY: data_size = 0; break;
case CMAP_FORMAT_SPARSE_TINY: data_size = entries * 2; break;
default: return false;
}
if (data_size > 0 && (data_offset > size || data_size > size - data_offset)) {
return false;
}
}
return true;
}
bool validate_kern(const uint8_t* kern, size_t size, uint8_t glyphIdFormat) {
if (size < 16) {
return false;
}
const uint8_t format = kern[8];
if (format == 0) {
const size_t count = read_u32(kern + 12);
const size_t entry_size = (glyphIdFormat ? 4 : 2) + 1;
return count <= (size - 16) / entry_size;
} else if (format == 3) {
const size_t glyph_count = read_u16(kern + 12);
const size_t left_count = kern[14];
const size_t right_count = kern[15];
return 16 + glyph_count * 2 + left_count * right_count <= size;
}
return false;
}
/**
* Locates all tables. In memory mode the table pointers point into font->data, in stream mode
* the lookup tables are copied into font->tables.
*/
error_t parse_layout(BinFont* font, size_t fileSize) {
uint8_t record_header[RECORD_HEADER_SIZE];
uint8_t head[64];
auto read = [font](size_t offset, uint8_t* out, size_t size) {
if (font->data != nullptr) {
memcpy(out, font->data + offset, size);
return true;
}
return read_at(font, offset, out, size);
};
// head
if (fileSize < RECORD_HEADER_SIZE || !read(0, record_header, RECORD_HEADER_SIZE)) {
return ERROR_NOT_SUPPORTED;
}
const size_t head_size = read_u32(record_header);
if (head_size < HEAD_MIN_SIZE || head_size > fileSize) {
return ERROR_NOT_SUPPORTED;
}
if (!read(0, head, HEAD_MIN_SIZE) || !parse_header(head, HEAD_MIN_SIZE, font->header)) {
return ERROR_NOT_SUPPORTED;
}
// cmap, loca, glyf and optionally kern follow each other
size_t offsets[4] = {};
size_t sizes[4] = {};
const char* markers[4] = { "cmap", "loca", "glyf", "kern" };
size_t offset = head_size;
for (int i = 0; i < 4; i++) {
if (offset > fileSize || fileSize - offset < RECORD_HEADER_SIZE || !read(offset, record_header, RECORD_HEADER_SIZE)) {
if (i == 3) {
break; // kern is optional
}
return ERROR_NOT_SUPPORTED;
}
const size_t size = read_u32(record_header);
if (!has_marker(record_header, markers[i]) || size < RECORD_HEADER_SIZE || size > fileSize - offset) {
if (i == 3) {
break;
}
return ERROR_NOT_SUPPORTED;
}
offsets[i] = offset;
sizes[i] = size;
offset += size;
}
font->glyfOffset = offsets[2];
font->glyfSize = sizes[2];
if (font->data != nullptr) {
font->cmap = font->data + offsets[0];
font->loca = font->data + offsets[1];
font->kern = sizes[3] ? font->data + offsets[3] : nullptr;
} else {
font->tables = static_cast<uint8_t*>(memory_alloc(sizes[0] + sizes[1] + sizes[3]));
if (font->tables == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
uint8_t* cursor = font->tables;
for (int i : { 0, 1, 3 }) {
if (sizes[i] > 0 && !read_at(font, offsets[i], cursor, sizes[i])) {
return ERROR_NOT_SUPPORTED;
}
cursor += sizes[i];
}
font->cmap = font->tables;
font->loca = font->tables + sizes[0];
font->kern = sizes[3] ? font->tables + sizes[0] + sizes[1] : nullptr;
}
font->cmapSize = sizes[0];
font->locaSize = sizes[1];
font->kernSize = sizes[3];
if (!validate_cmap(font->cmap, font->cmapSize)) {
return ERROR_NOT_SUPPORTED;
}
if (font->locaSize < 12) {
return ERROR_NOT_SUPPORTED;
}
font->locaCount = read_u32(font->loca + 8);
const size_t loca_entry_size = font->header.indexToLocFormat ? 4 : 2;
if (font->locaCount > (font->locaSize - 12) / loca_entry_size) {
return ERROR_NOT_SUPPORTED;
}
if (font->kern != nullptr && !validate_kern(font->kern, font->kernSize, font->header.glyphIdFormat)) {
font->kern = nullptr;
font->kernSize = 0;
}
return ERROR_NONE;
}
BinFont* create_font() {
auto* font = static_cast<BinFont*>(memory_calloc(1, sizeof(BinFont)));
if (font != nullptr) {
mutex_construct(&font->mutex);
}
return font;
}
uint32_t find_glyph_id(const BinFont* font, uint32_t codepoint) {
const uint8_t* cmap = font->cmap;
const uint32_t count = read_u32(cmap + 8);
// Find the last subtable that starts at or before the codepoint
uint32_t low = 0;
uint32_t high = count;
while (low < high) {
const uint32_t mid = (low + high) / 2;
if (read_u32(cmap + 12 + mid * CMAP_SUBTABLE_HEADER_SIZE + 4) <= codepoint) {
low = mid + 1;
} else {
high = mid;
}
}
if (low == 0) {
return 0;
}
const uint8_t* subtable = cmap + 12 + (low - 1) * CMAP_SUBTABLE_HEADER_SIZE;
const uint32_t data_offset = read_u32(subtable);
const uint32_t range_start = read_u32(subtable + 4);
const uint16_t range_length = read_u16(subtable + 8);
const uint16_t glyph_id_start = read_u16(subtable + 10);
const uint16_t entries = read_u16(subtable + 12);
const uint32_t relative = codepoint - range_start;
if (relative >= range_length) {
return 0;
}
switch (subtable[14]) {
case CMAP_FORMAT_0:
return glyph_id_start + cmap[data_offset + relative];
case CMAP_FORMAT_0_TINY:
return glyph_id_start + relative;
case CMAP_FORMAT_SPARSE:
case CMAP_FORMAT_SPARSE_TINY: {
const uint8_t* list = cmap + data_offset;
uint32_t list_low = 0;
uint32_t list_high = entries;
while (list_low < list_high) {
const uint32_t mid = (list_low + list_high) / 2;
const uint16_t value = read_u16(list + mid * 2);
if (value == relative) {
if (subtable[14] == CMAP_FORMAT_SPARSE_TINY) {
return glyph_id_start + mid;
}
return glyph_id_start + read_u16(list + entries * 2 + mid * 2);
} else if (value < relative) {
list_low = mid + 1;
} else {
list_high = mid;
}
}
return 0;
}
default:
return 0;
}
}
/** Gets the offset and size of a glyph record, relative to the glyf data (after its record header). */
bool get_glyph_location(const BinFont* font, uint32_t glyphId, size_t& offset, size_t& size) {
if (glyphId == 0 || glyphId >= font->locaCount) {
return false;
}
const uint8_t* entries = font->loca + 12;
auto entry = [&](uint32_t index) -> size_t {
return font->header.indexToLocFormat ? read_u32(entries + index * 4) : read_u16(entries + index * 2);
};
const size_t start = entry(glyphId);
const size_t end = (glyphId + 1 < font->locaCount) ? entry(glyphId + 1) : font->glyfSize;
if (start < RECORD_HEADER_SIZE || end < start || end > font->glyfSize) {
return false;
}
offset = start;
size = end - start;
return true;
}
/** Reads from the font file in stream mode. Outside of a session, the file is opened for this read only. */
bool read_glyph_data(BinFont* font, size_t offset, uint8_t* out, size_t size) {
mutex_lock(&font->mutex);
const bool temporary = font->file == nullptr;
if (temporary) {
font->file = fopen(font->path, "rb");
}
const bool success = font->file != nullptr && read_at(font, offset, out, size);
if (temporary && font->file != nullptr) {
fclose(font->file);
font->file = nullptr;
}
mutex_unlock(&font->mutex);
return success;
}
/**
* Provides the record of a glyph. In memory mode it points into the font data, in stream mode it
* is read into buffer (bufferSize bytes) or, when that is too small, into an allocation returned
* via allocated (to be freed by the caller).
*/
bool get_glyph_record(BinFont* font, uint32_t glyphId, size_t maxSize, uint8_t* buffer, size_t bufferSize, const uint8_t*& record, size_t& size, uint8_t*& allocated) {
size_t offset;
allocated = nullptr;
if (!get_glyph_location(font, glyphId, offset, size)) {
return false;
}
if (font->data != nullptr) {
record = font->data + font->glyfOffset + offset;
return true;
}
if (size > maxSize) {
size = maxSize;
}
uint8_t* target = buffer;
if (size > bufferSize) {
allocated = static_cast<uint8_t*>(memory_alloc(size));
if (allocated == nullptr) {
return false;
}
target = allocated;
}
const bool success = read_glyph_data(font, font->glyfOffset + offset, target, size);
if (!success) {
memory_free(allocated);
allocated = nullptr;
return false;
}
record = target;
return true;
}
void parse_glyph(const Header& header, BitReader& reader, uint32_t glyphId, BinFontGlyph* out) {
uint32_t advance = header.advanceWidthBits ? reader.read(header.advanceWidthBits) : header.defaultAdvanceWidth;
if (header.advanceWidthFormat == 0) {
advance *= 16;
}
out->glyph_id = glyphId;
out->advance_x16 = advance;
out->x = static_cast<int16_t>(reader.readSigned(header.xyBits));
out->y = static_cast<int16_t>(reader.readSigned(header.xyBits));
out->width = static_cast<uint16_t>(reader.read(header.whBits));
out->height = static_cast<uint16_t>(reader.read(header.whBits));
}
/** Opens a file for reading and determines its size */
error_t open_file(const char* path, FILE*& file, size_t& size) {
file = fopen(path, "rb");
if (file == nullptr) {
return ERROR_NOT_FOUND;
}
if (fseek(file, 0, SEEK_END) != 0) {
fclose(file);
return ERROR_NOT_SUPPORTED;
}
const long file_size = ftell(file);
if (file_size <= 0 || fseek(file, 0, SEEK_SET) != 0) {
fclose(file);
return ERROR_NOT_SUPPORTED;
}
size = static_cast<size_t>(file_size);
return ERROR_NONE;
}
/** Loads the lookup tables from an open file, then closes it. Glyph data is read on demand. */
error_t open_streaming(const char* path, FILE* file, size_t size, BinFont** out) {
BinFont* font = create_font();
if (font == nullptr) {
fclose(file);
return ERROR_OUT_OF_MEMORY;
}
font->file = file;
const size_t path_size = strlen(path) + 1;
font->path = static_cast<char*>(memory_alloc(path_size));
if (font->path == nullptr) {
binfont_close(font);
return ERROR_OUT_OF_MEMORY;
}
memcpy(font->path, path, path_size);
const error_t error = parse_layout(font, size);
fclose(font->file);
font->file = nullptr;
if (error != ERROR_NONE) {
binfont_close(font);
return error;
}
*out = font;
return ERROR_NONE;
}
} // namespace
extern "C" {
error_t binfont_open_memory(const void* data, size_t size, bool take_ownership, BinFont** out) {
BinFont* font = create_font();
if (font == nullptr) {
if (take_ownership) {
memory_free(const_cast<void*>(data));
}
return ERROR_OUT_OF_MEMORY;
}
font->data = static_cast<const uint8_t*>(data);
font->ownsData = take_ownership;
const error_t error = parse_layout(font, size);
if (error != ERROR_NONE) {
binfont_close(font);
return error;
}
*out = font;
return ERROR_NONE;
}
error_t binfont_open_file(const char* path, BinFont** out) {
FILE* file;
size_t size;
const error_t open_error = open_file(path, file, size);
if (open_error != ERROR_NONE) {
return open_error;
}
const uint16_t memory_capability = memory_external_total() > 0 ? MEMORY_CAPABILITY_EXTERNAL : MEMORY_CAPABILITY_INTERNAL;
const MemoryPolicy policy = { .required = memory_capability, .desired = 0, .alignment = 0 };
auto* data = static_cast<uint8_t*>(memory_alloc_with_policy(size, &policy));
if (data == nullptr) {
return open_streaming(path, file, size, out);
}
const bool success = fread(data, 1, size, file) == size;
fclose(file);
if (!success) {
memory_free(data);
return ERROR_NOT_SUPPORTED;
}
return binfont_open_memory(data, size, true, out);
}
error_t binfont_open_file_streaming(const char* path, BinFont** out) {
FILE* file;
size_t size;
const error_t open_error = open_file(path, file, size);
if (open_error != ERROR_NONE) {
return open_error;
}
return open_streaming(path, file, size, out);
}
error_t binfont_begin(BinFont* font) {
if (font->data != nullptr) {
return ERROR_NONE;
}
mutex_lock(&font->mutex);
if (font->sessionCount == 0) {
font->file = fopen(font->path, "rb");
if (font->file == nullptr) {
mutex_unlock(&font->mutex);
return ERROR_NOT_FOUND;
}
}
font->sessionCount++;
mutex_unlock(&font->mutex);
return ERROR_NONE;
}
void binfont_end(BinFont* font) {
if (font->data != nullptr) {
return;
}
mutex_lock(&font->mutex);
if (font->sessionCount > 0) {
font->sessionCount--;
if (font->sessionCount == 0) {
fclose(font->file);
font->file = nullptr;
}
}
mutex_unlock(&font->mutex);
}
void binfont_close(BinFont* font) {
if (font->ownsData) {
memory_free(const_cast<uint8_t*>(font->data));
}
if (font->file != nullptr) {
fclose(font->file);
}
memory_free(font->path);
memory_free(font->tables);
mutex_destruct(&font->mutex);
memory_free(font);
}
void binfont_get_metrics(const BinFont* font, BinFontMetrics* out) {
const Header& header = font->header;
out->size = header.fontSize;
out->ascent = header.ascent;
out->descent = header.descent;
out->line_height = static_cast<uint16_t>(header.ascent - header.descent);
out->base_line = static_cast<int16_t>(-header.descent);
out->underline_position = header.underlinePosition;
out->underline_thickness = header.underlineThickness;
out->bpp = header.bpp;
}
bool binfont_get_glyph(BinFont* font, uint32_t codepoint, BinFontGlyph* out) {
const uint32_t glyph_id = find_glyph_id(font, codepoint);
uint8_t buffer[GLYPH_HEADER_MAX_BYTES];
const uint8_t* record;
size_t size;
uint8_t* allocated;
if (!get_glyph_record(font, glyph_id, sizeof(buffer), buffer, sizeof(buffer), record, size, allocated)) {
return false;
}
BitReader reader(record, size);
parse_glyph(font->header, reader, glyph_id, out);
return true;
}
int32_t binfont_get_kerning_x16(BinFont* font, uint32_t left_glyph_id, uint32_t right_glyph_id) {
const uint8_t* kern = font->kern;
if (kern == nullptr) {
return 0;
}
int8_t value = 0;
if (kern[8] == 0) {
const uint32_t count = read_u32(kern + 12);
const uint8_t* pairs = kern + 16;
const bool wide = font->header.glyphIdFormat != 0;
const size_t pair_size = wide ? 4 : 2;
const uint32_t key = (left_glyph_id << 16) | right_glyph_id;
uint32_t low = 0;
uint32_t high = count;
while (low < high) {
const uint32_t mid = (low + high) / 2;
const uint8_t* pair = pairs + mid * pair_size;
const uint32_t left = wide ? read_u16(pair) : pair[0];
const uint32_t right = wide ? read_u16(pair + 2) : pair[1];
const uint32_t mid_key = (left << 16) | right;
if (mid_key == key) {
value = static_cast<int8_t>(pairs[count * pair_size + mid]);
break;
} else if (mid_key < key) {
low = mid + 1;
} else {
high = mid;
}
}
} else {
const uint16_t glyph_count = read_u16(kern + 12);
const uint8_t right_count = kern[15];
if (left_glyph_id >= glyph_count || right_glyph_id >= glyph_count) {
return 0;
}
const uint8_t* left_classes = kern + 16;
const uint8_t* right_classes = left_classes + glyph_count;
const uint8_t* values = right_classes + glyph_count;
const uint8_t left_class = left_classes[left_glyph_id];
const uint8_t right_class = right_classes[right_glyph_id];
if (left_class == 0 || right_class == 0 || left_class > kern[14] || right_class > right_count) {
return 0;
}
value = static_cast<int8_t>(values[(left_class - 1) * right_count + (right_class - 1)]);
}
return (static_cast<int32_t>(value) * font->header.kerningScale) >> 4;
}
error_t binfont_get_glyph_bitmap(BinFont* font, const BinFontGlyph* glyph, uint8_t* out, size_t stride) {
if (stride < glyph->width) {
return ERROR_INVALID_ARGUMENT;
}
if (glyph->width == 0 || glyph->height == 0) {
return ERROR_NONE;
}
const uint8_t* record;
size_t size;
uint8_t* allocated;
if (!get_glyph_record(font, glyph->glyph_id, SIZE_MAX, nullptr, 0, record, size, allocated)) {
return ERROR_RESOURCE;
}
const Header& header = font->header;
const uint8_t bpp = header.bpp;
const size_t header_bits = header.advanceWidthBits + 2U * header.xyBits + 2U * header.whBits;
BitReader reader(record, size, header_bits);
// Raw pixel values first, so the XOR prefilter can work on the previous line
if (header.compression == COMPRESSION_NONE) {
for (uint16_t y = 0; y < glyph->height; y++) {
uint8_t* line = out + y * stride;
for (uint16_t x = 0; x < glyph->width; x++) {
line[x] = static_cast<uint8_t>(reader.read(bpp));
}
}
} else {
RleReader rle(reader, bpp);
const bool prefilter = header.compression == COMPRESSION_RLE_PREFILTER;
for (uint16_t y = 0; y < glyph->height; y++) {
uint8_t* line = out + y * stride;
const uint8_t* previous = line - stride;
for (uint16_t x = 0; x < glyph->width; x++) {
const uint8_t value = rle.next();
line[x] = (prefilter && y > 0) ? (value ^ previous[x]) : value;
}
}
}
const uint32_t max_value = (1U << bpp) - 1;
for (uint16_t y = 0; y < glyph->height; y++) {
uint8_t* line = out + y * stride;
for (uint16_t x = 0; x < glyph->width; x++) {
line[x] = static_cast<uint8_t>(line[x] * 255U / max_value);
}
}
memory_free(allocated);
return ERROR_NONE;
}
}

View File

@ -0,0 +1,443 @@
// SPDX-License-Identifier: Apache-2.0
#include <binfont/generator.h>
#include <tactility/memory.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#define STBTT_STATIC
#define STB_TRUETYPE_IMPLEMENTATION
#define STBTT_malloc(size, user) ((void)(user), memory_alloc(size))
#define STBTT_free(pointer, user) ((void)(user), memory_free(pointer))
#include "stb_truetype.h"
namespace {
constexpr size_t HEAD_SIZE = 48;
constexpr uint8_t CMAP_FORMAT_0_TINY = 2;
constexpr uint8_t CMAP_FORMAT_SPARSE_TINY = 3;
constexpr uint32_t CMAP_MAX_RANGE = 0xFFFF;
constexpr MemoryPolicy EXTERNAL_PREFERRED = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
class ByteBuffer {
uint8_t* data = nullptr;
size_t size = 0;
size_t capacity = 0;
bool failed = false;
bool reserve(size_t required) {
if (failed) {
return false;
}
if (required <= capacity) {
return true;
}
size_t new_capacity = capacity ? capacity * 2 : 4096;
while (new_capacity < required) {
new_capacity *= 2;
}
auto* new_data = static_cast<uint8_t*>(memory_realloc_with_policy(data, new_capacity, &EXTERNAL_PREFERRED));
if (new_data == nullptr) {
failed = true;
return false;
}
data = new_data;
capacity = new_capacity;
return true;
}
public:
~ByteBuffer() { memory_free(data); }
size_t getSize() const { return size; }
bool hasFailed() const { return failed; }
void append(const void* bytes, size_t count) {
if (reserve(size + count)) {
memcpy(data + size, bytes, count);
size += count;
}
}
void appendZeros(size_t count) {
if (reserve(size + count)) {
memset(data + size, 0, count);
size += count;
}
}
void appendU8(uint8_t value) { append(&value, 1); }
void appendU16(uint16_t value) {
const uint8_t bytes[2] = { static_cast<uint8_t>(value), static_cast<uint8_t>(value >> 8) };
append(bytes, 2);
}
void appendU32(uint32_t value) {
const uint8_t bytes[4] = { static_cast<uint8_t>(value), static_cast<uint8_t>(value >> 8), static_cast<uint8_t>(value >> 16), static_cast<uint8_t>(value >> 24) };
append(bytes, 4);
}
void setU32(size_t offset, uint32_t value) {
if (!failed) {
data[offset] = static_cast<uint8_t>(value);
data[offset + 1] = static_cast<uint8_t>(value >> 8);
data[offset + 2] = static_cast<uint8_t>(value >> 16);
data[offset + 3] = static_cast<uint8_t>(value >> 24);
}
}
void alignTo4() { appendZeros((4 - size % 4) % 4); }
/** Starts a record and returns its offset, for endRecord(). */
size_t beginRecord(const char* marker) {
const size_t offset = size;
appendU32(0);
append(marker, 4);
return offset;
}
void endRecord(size_t offset) { setU32(offset, static_cast<uint32_t>(size - offset)); }
uint8_t* release() {
uint8_t* result = data;
data = nullptr;
size = 0;
capacity = 0;
return result;
}
};
/** MSB-first bit writer */
class BitWriter {
ByteBuffer& buffer;
uint8_t current = 0;
uint8_t used = 0;
public:
explicit BitWriter(ByteBuffer& buffer) : buffer(buffer) {}
void write(uint32_t value, uint8_t bits) {
while (bits > 0) {
bits--;
current = static_cast<uint8_t>((current << 1) | ((value >> bits) & 1U));
used++;
if (used == 8) {
buffer.appendU8(current);
current = 0;
used = 0;
}
}
}
void flush() {
if (used > 0) {
buffer.appendU8(static_cast<uint8_t>(current << (8 - used)));
current = 0;
used = 0;
}
}
};
struct GlyphInfo {
uint32_t codepoint;
int ttfIndex;
uint32_t advanceX16;
int x0, y0, x1, y1; // stb_truetype bitmap box: y points down
};
uint8_t bits_for_unsigned(uint32_t value) {
uint8_t bits = 1;
while (bits < 32 && (value >> bits) != 0) {
bits++;
}
return bits;
}
uint8_t bits_for_signed(int32_t min, int32_t max) {
uint8_t bits = 1;
while (bits < 32 && (min < -(1 << (bits - 1)) || max > (1 << (bits - 1)) - 1)) {
bits++;
}
return bits;
}
int16_t scale_metric(int value, float scale) {
return static_cast<int16_t>(std::lround(static_cast<float>(value) * scale));
}
uint8_t* read_file(const char* path, size_t& size) {
FILE* file = fopen(path, "rb");
if (file == nullptr) {
return nullptr;
}
uint8_t* data = nullptr;
long file_size = 0;
if (fseek(file, 0, SEEK_END) == 0 && (file_size = ftell(file)) > 0 && fseek(file, 0, SEEK_SET) == 0) {
data = static_cast<uint8_t*>(memory_alloc_with_policy(static_cast<size_t>(file_size), &EXTERNAL_PREFERRED));
if (data != nullptr && fread(data, 1, static_cast<size_t>(file_size), file) != static_cast<size_t>(file_size)) {
memory_free(data);
data = nullptr;
}
}
fclose(file);
size = static_cast<size_t>(file_size);
return data;
}
void write_cmap(ByteBuffer& out, const GlyphInfo* glyphs, size_t count) {
// Glyph ids are 1..count in codepoint order, so every subtable can be a "tiny" format
struct Range {
size_t first;
size_t count;
bool contiguous;
};
auto next_range = [&](size_t first) {
Range range = { first, 1, true };
while (first + range.count < count && range.count < CMAP_MAX_RANGE) {
const GlyphInfo& candidate = glyphs[first + range.count];
if (candidate.codepoint - glyphs[first].codepoint >= CMAP_MAX_RANGE) {
break;
}
if (candidate.codepoint != glyphs[first].codepoint + range.count) {
range.contiguous = false;
}
range.count++;
}
return range;
};
uint32_t range_count = 0;
for (size_t i = 0; i < count; i += next_range(i).count) {
range_count++;
}
const size_t record = out.beginRecord("cmap");
out.appendU32(range_count);
size_t data_offset = 12 + range_count * 16;
for (size_t i = 0; i < count;) {
const Range range = next_range(i);
const uint32_t range_start = glyphs[i].codepoint;
const uint32_t range_length = glyphs[i + range.count - 1].codepoint - range_start + 1;
out.appendU32(range.contiguous ? 0 : static_cast<uint32_t>(data_offset));
out.appendU32(range_start);
out.appendU16(static_cast<uint16_t>(range_length));
out.appendU16(static_cast<uint16_t>(i + 1));
out.appendU16(range.contiguous ? 0 : static_cast<uint16_t>(range.count));
out.appendU8(range.contiguous ? CMAP_FORMAT_0_TINY : CMAP_FORMAT_SPARSE_TINY);
out.appendU8(0);
if (!range.contiguous) {
data_offset += (range.count * 2 + 3) & ~static_cast<size_t>(3);
}
i += range.count;
}
for (size_t i = 0; i < count;) {
const Range range = next_range(i);
if (!range.contiguous) {
for (size_t j = 0; j < range.count; j++) {
out.appendU16(static_cast<uint16_t>(glyphs[i + j].codepoint - glyphs[i].codepoint));
}
out.alignTo4();
}
i += range.count;
}
out.endRecord(record);
}
error_t generate(const BinFontGeneratorConfig* config, const stbtt_fontinfo& info, GlyphInfo* glyphs, ByteBuffer& out) {
const float scale = stbtt_ScaleForMappingEmToPixels(&info, config->size);
// Collect glyphs, sorted by codepoint and without duplicates or missing glyphs
size_t count = 0;
for (size_t i = 0; i < config->codepoint_count; i++) {
const int index = stbtt_FindGlyphIndex(&info, static_cast<int>(config->codepoints[i]));
if (index != 0) {
glyphs[count].codepoint = config->codepoints[i];
glyphs[count].ttfIndex = index;
count++;
}
}
std::sort(glyphs, glyphs + count, [](const GlyphInfo& a, const GlyphInfo& b) { return a.codepoint < b.codepoint; });
count = std::unique(glyphs, glyphs + count, [](const GlyphInfo& a, const GlyphInfo& b) { return a.codepoint == b.codepoint; }) - glyphs;
if (count == 0 || count >= 0xFFFF) {
return ERROR_NOT_SUPPORTED;
}
int32_t min_xy = 0;
int32_t max_xy = 0;
uint32_t max_wh = 0;
uint32_t max_advance = 0;
// Like lv_font_conv, ascent and descent are the extents of the included glyphs
int32_t min_y = INT32_MAX;
int32_t max_y = INT32_MIN;
for (size_t i = 0; i < count; i++) {
GlyphInfo& glyph = glyphs[i];
int advance;
int left_side_bearing;
stbtt_GetGlyphHMetrics(&info, glyph.ttfIndex, &advance, &left_side_bearing);
glyph.advanceX16 = static_cast<uint32_t>(std::lround(static_cast<float>(advance) * scale * 16.0f));
stbtt_GetGlyphBitmapBox(&info, glyph.ttfIndex, scale, scale, &glyph.x0, &glyph.y0, &glyph.x1, &glyph.y1);
const int32_t bottom = -glyph.y1;
min_xy = std::min({ min_xy, static_cast<int32_t>(glyph.x0), bottom });
max_xy = std::max({ max_xy, static_cast<int32_t>(glyph.x0), bottom });
max_wh = std::max({ max_wh, static_cast<uint32_t>(glyph.x1 - glyph.x0), static_cast<uint32_t>(glyph.y1 - glyph.y0) });
max_advance = std::max(max_advance, glyph.advanceX16);
min_y = std::min(min_y, bottom);
max_y = std::max(max_y, static_cast<int32_t>(-glyph.y0));
}
const uint8_t xy_bits = bits_for_signed(min_xy, max_xy);
const uint8_t wh_bits = bits_for_unsigned(max_wh);
const uint8_t advance_bits = bits_for_unsigned(max_advance);
const uint32_t glyph_count = static_cast<uint32_t>(count) + 1;
int typo_ascent = 0;
int typo_descent = 0;
int typo_line_gap = 0;
stbtt_GetFontVMetricsOS2(&info, &typo_ascent, &typo_descent, &typo_line_gap);
int underline_position = 0;
int underline_thickness = 0;
const stbtt_uint32 post = stbtt__find_table(info.data, info.fontstart, "post");
if (post != 0) {
underline_position = ttSHORT(info.data + post + 8);
underline_thickness = ttSHORT(info.data + post + 10);
}
// head
const size_t head = out.beginRecord("head");
out.appendU32(1); // version
out.appendU16(3); // additional tables: cmap, loca, glyf
out.appendU16(config->size);
out.appendU16(static_cast<uint16_t>(max_y));
out.appendU16(static_cast<uint16_t>(min_y));
out.appendU16(static_cast<uint16_t>(scale_metric(typo_ascent, scale)));
out.appendU16(static_cast<uint16_t>(scale_metric(typo_descent, scale)));
out.appendU16(static_cast<uint16_t>(scale_metric(typo_line_gap, scale)));
out.appendU16(static_cast<uint16_t>(min_y));
out.appendU16(static_cast<uint16_t>(max_y));
out.appendU16(0); // default advance width
out.appendU16(16); // kerning scale 1.0 in FP12.4
out.appendU8(1); // loca: Offset32
out.appendU8(glyph_count > 256 ? 1 : 0);
out.appendU8(1); // advance width with 4 fractional bits
out.appendU8(config->bpp);
out.appendU8(xy_bits);
out.appendU8(wh_bits);
out.appendU8(advance_bits);
out.appendU8(0); // no compression
out.appendU8(0); // no subpixel rendering
out.appendU8(0);
out.appendU16(static_cast<uint16_t>(scale_metric(underline_position, scale)));
out.appendU16(static_cast<uint16_t>(scale_metric(underline_thickness, scale)));
out.endRecord(head);
write_cmap(out, glyphs, count);
const size_t loca = out.beginRecord("loca");
out.appendU32(glyph_count);
const size_t loca_entries = out.getSize();
out.appendZeros(glyph_count * 4);
out.endRecord(loca);
const size_t glyf = out.beginRecord("glyf");
uint8_t* bitmap = nullptr;
size_t bitmap_capacity = 0;
const uint32_t max_value = (1U << config->bpp) - 1;
for (uint32_t id = 0; id < glyph_count && !out.hasFailed(); id++) {
out.setU32(loca_entries + id * 4, static_cast<uint32_t>(out.getSize() - glyf));
BitWriter writer(out);
if (id == 0) {
writer.write(0, advance_bits);
writer.write(0, xy_bits);
writer.write(0, xy_bits);
writer.write(0, wh_bits);
writer.write(0, wh_bits);
writer.flush();
continue;
}
const GlyphInfo& glyph = glyphs[id - 1];
const int width = glyph.x1 - glyph.x0;
const int height = glyph.y1 - glyph.y0;
writer.write(glyph.advanceX16, advance_bits);
writer.write(static_cast<uint32_t>(glyph.x0), xy_bits);
writer.write(static_cast<uint32_t>(-glyph.y1), xy_bits);
writer.write(static_cast<uint32_t>(width), wh_bits);
writer.write(static_cast<uint32_t>(height), wh_bits);
const size_t pixel_count = static_cast<size_t>(width) * height;
if (pixel_count > 0) {
if (pixel_count > bitmap_capacity) {
memory_free(bitmap);
bitmap = static_cast<uint8_t*>(memory_alloc_with_policy(pixel_count, &EXTERNAL_PREFERRED));
if (bitmap == nullptr) {
return ERROR_OUT_OF_MEMORY;
}
bitmap_capacity = pixel_count;
}
stbtt_MakeGlyphBitmap(&info, bitmap, width, height, width, scale, scale, glyph.ttfIndex);
for (size_t i = 0; i < pixel_count; i++) {
writer.write((bitmap[i] * max_value + 127) / 255, config->bpp);
}
}
writer.flush();
}
memory_free(bitmap);
out.alignTo4();
out.endRecord(glyf);
return out.hasFailed() ? ERROR_OUT_OF_MEMORY : ERROR_NONE;
}
} // namespace
extern "C" {
error_t binfont_generate(const BinFontGeneratorConfig* config, uint8_t** out_data, size_t* out_size) {
if (config->ttf_path == nullptr || config->size == 0 || config->bpp < 1 || config->bpp > 4 || config->codepoint_count == 0) {
return ERROR_INVALID_ARGUMENT;
}
size_t ttf_size = 0;
uint8_t* ttf = read_file(config->ttf_path, ttf_size);
if (ttf == nullptr) {
return ERROR_NOT_FOUND;
}
stbtt_fontinfo info;
const int font_offset = stbtt_GetFontOffsetForIndex(ttf, 0);
if (font_offset < 0 || stbtt_InitFont(&info, ttf, font_offset) == 0) {
memory_free(ttf);
return ERROR_NOT_SUPPORTED;
}
auto* glyphs = static_cast<GlyphInfo*>(memory_alloc(config->codepoint_count * sizeof(GlyphInfo)));
if (glyphs == nullptr) {
memory_free(ttf);
return ERROR_OUT_OF_MEMORY;
}
ByteBuffer out;
const error_t error = generate(config, info, glyphs, out);
memory_free(glyphs);
memory_free(ttf);
if (error != ERROR_NONE) {
return error;
}
*out_size = out.getSize();
*out_data = out.release();
return ERROR_NONE;
}
}

View File

@ -0,0 +1,33 @@
// SPDX-License-Identifier: Apache-2.0
#include <binfont/binfont.h>
#include <binfont/generator.h>
#include <binfont/module.h>
#include <binfont/render.h>
static const struct ModuleSymbol SYMBOLS[] = {
// binfont
DEFINE_MODULE_SYMBOL(binfont_open_file),
DEFINE_MODULE_SYMBOL(binfont_open_memory),
DEFINE_MODULE_SYMBOL(binfont_close),
DEFINE_MODULE_SYMBOL(binfont_begin),
DEFINE_MODULE_SYMBOL(binfont_end),
DEFINE_MODULE_SYMBOL(binfont_get_metrics),
DEFINE_MODULE_SYMBOL(binfont_get_glyph),
DEFINE_MODULE_SYMBOL(binfont_get_kerning_x16),
DEFINE_MODULE_SYMBOL(binfont_get_glyph_bitmap),
// generator
DEFINE_MODULE_SYMBOL(binfont_generate),
// render
DEFINE_MODULE_SYMBOL(binfont_draw_glyph_rgb565),
DEFINE_MODULE_SYMBOL(binfont_draw_text_rgb565),
MODULE_SYMBOL_TERMINATOR,
};
struct Module binfont_module = {
.name = "binfont",
.start = NULL,
.stop = NULL,
.drivers = NULL,
.symbols = SYMBOLS,
.internal = NULL,
};

View File

@ -0,0 +1,126 @@
// SPDX-License-Identifier: Apache-2.0
#include <binfont/render.h>
#include <tactility/memory.h>
namespace {
constexpr size_t STACK_BITMAP_SIZE = 1024;
uint16_t blend_rgb565(uint16_t foreground, uint16_t background, uint8_t alpha) {
const uint32_t inverse = 255U - alpha;
const uint32_t r = (((foreground >> 11) & 0x1FU) * alpha + ((background >> 11) & 0x1FU) * inverse + 127U) / 255U;
const uint32_t g = (((foreground >> 5) & 0x3FU) * alpha + ((background >> 5) & 0x3FU) * inverse + 127U) / 255U;
const uint32_t b = ((foreground & 0x1FU) * alpha + (background & 0x1FU) * inverse + 127U) / 255U;
return static_cast<uint16_t>((r << 11) | (g << 5) | b);
}
void draw_glyph(PixelBuffer* buffer, int x, int baselineY, BinFont* font, const BinFontGlyph& glyph, uint16_t color, PixelBufferConversion conversion) {
const size_t pixel_count = static_cast<size_t>(glyph.width) * glyph.height;
if (pixel_count == 0) {
return;
}
uint8_t stack_bitmap[STACK_BITMAP_SIZE];
uint8_t* allocated = nullptr;
uint8_t* bitmap = stack_bitmap;
if (pixel_count > sizeof(stack_bitmap)) {
allocated = static_cast<uint8_t*>(memory_alloc(pixel_count));
if (allocated == nullptr) {
return;
}
bitmap = allocated;
}
if (binfont_get_glyph_bitmap(font, &glyph, bitmap, glyph.width) == ERROR_NONE) {
const int left = x + glyph.x;
const int top = baselineY - glyph.y - glyph.height;
for (int row = 0; row < glyph.height; row++) {
const uint8_t* line = bitmap + row * glyph.width;
for (int column = 0; column < glyph.width; column++) {
const uint8_t alpha = line[column];
if (alpha == 0) {
continue;
}
const int pixel_x = left + column;
const int pixel_y = top + row;
const uint16_t pixel = (alpha == 255) ? color : blend_rgb565(color, pixel_buffer_get_pixel_rgb565(buffer, pixel_x, pixel_y), alpha);
pixel_buffer_set_pixel_rgb565(buffer, pixel_x, pixel_y, pixel, conversion);
}
}
}
memory_free(allocated);
}
/** Decodes one UTF-8 codepoint and advances text. Invalid sequences decode as U+FFFD. */
uint32_t next_codepoint(const char*& text) {
const auto* bytes = reinterpret_cast<const uint8_t*>(text);
const uint8_t lead = bytes[0];
int length;
uint32_t codepoint;
if (lead < 0x80) {
length = 1;
codepoint = lead;
} else if ((lead & 0xE0) == 0xC0) {
length = 2;
codepoint = lead & 0x1FU;
} else if ((lead & 0xF0) == 0xE0) {
length = 3;
codepoint = lead & 0x0FU;
} else if ((lead & 0xF8) == 0xF0) {
length = 4;
codepoint = lead & 0x07U;
} else {
text++;
return 0xFFFD;
}
for (int i = 1; i < length; i++) {
if ((bytes[i] & 0xC0) != 0x80) {
text += i;
return 0xFFFD;
}
codepoint = (codepoint << 6) | (bytes[i] & 0x3FU);
}
text += length;
return codepoint;
}
} // namespace
extern "C" {
int binfont_draw_glyph_rgb565(PixelBuffer* buffer, int x, int baseline_y, BinFont* font, uint32_t codepoint, uint16_t color, PixelBufferConversion conversion) {
BinFontGlyph glyph;
if (!binfont_get_glyph(font, codepoint, &glyph)) {
return 0;
}
draw_glyph(buffer, x, baseline_y, font, glyph, color, conversion);
return static_cast<int>((glyph.advance_x16 + 8) >> 4);
}
int binfont_draw_text_rgb565(PixelBuffer* buffer, int x, int baseline_y, BinFont* font, const char* text, uint16_t color, PixelBufferConversion conversion) {
// When the file can't be kept open, every glyph lookup opens it by itself
const bool session = binfont_begin(font) == ERROR_NONE;
int32_t pen_x16 = 0;
uint32_t previous_glyph_id = 0;
while (*text != '\0') {
BinFontGlyph glyph;
if (!binfont_get_glyph(font, next_codepoint(text), &glyph)) {
previous_glyph_id = 0;
continue;
}
if (previous_glyph_id != 0) {
pen_x16 += binfont_get_kerning_x16(font, previous_glyph_id, glyph.glyph_id);
}
draw_glyph(buffer, x + ((pen_x16 + 8) >> 4), baseline_y, font, glyph, color, conversion);
pen_x16 += static_cast<int32_t>(glyph.advance_x16);
previous_glyph_id = glyph.glyph_id;
}
if (session) {
binfont_end(font);
}
return (pen_x16 + 8) >> 4;
}
}

View File

@ -0,0 +1,23 @@
project(BinFontModuleTests)
enable_language(C CXX ASM)
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(BinFontModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
target_include_directories(BinFontModuleTests PRIVATE ${DOCTESTINC} ${PROJECT_SOURCE_DIR}/../private)
target_compile_definitions(BinFontModuleTests PRIVATE
"BINFONT_TEST_FIXTURES_DIR=\"${PROJECT_SOURCE_DIR}/fixtures\""
"BINFONT_TEST_TEMP_DIR=\"${CMAKE_CURRENT_BINARY_DIR}\""
"BINFONT_TEST_ICON_TTF=\"${CMAKE_SOURCE_DIR}/Data/system/fonts/MaterialSymbolsRounded.ttf\""
)
add_test(NAME BinFontModuleTests COMMAND BinFontModuleTests)
target_link_libraries(BinFontModuleTests PUBLIC
binfont-module
graphics-module
TactilityKernel
freertos_kernel
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,340 @@
#include "doctest.h"
#include <binfont/binfont.h>
#include <binfont_private.h>
#include <binfont/generator.h>
#include <binfont/render.h>
#include <graphics/pixel_buffer.h>
#include <tactility/memory.h>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
namespace {
constexpr uint32_t ICON_CODEPOINTS[] = { 0xE5C3, 0xE8B8, 0xE2C7, 0xE63E, 0xE1A7 };
std::string fixture(const char* name) {
return std::string(BINFONT_TEST_FIXTURES_DIR) + "/" + name;
}
/** Loads a file into memory allocated with memory_alloc() */
uint8_t* load_file(const std::string& path, size_t& size) {
FILE* file = fopen(path.c_str(), "rb");
REQUIRE(file != nullptr);
fseek(file, 0, SEEK_END);
size = static_cast<size_t>(ftell(file));
fseek(file, 0, SEEK_SET);
auto* data = static_cast<uint8_t*>(memory_alloc(size));
REQUIRE(fread(data, 1, size, file) == size);
fclose(file);
return data;
}
BinFont* open_fixture_in_memory(const char* name) {
size_t size;
uint8_t* data = load_file(fixture(name), size);
BinFont* font = nullptr;
REQUIRE(binfont_open_memory(data, size, true, &font) == ERROR_NONE);
return font;
}
std::vector<uint8_t> decode(BinFont* font, const BinFontGlyph& glyph) {
std::vector<uint8_t> bitmap(static_cast<size_t>(glyph.width) * glyph.height);
REQUIRE(binfont_get_glyph_bitmap(font, &glyph, bitmap.data(), glyph.width) == ERROR_NONE);
return bitmap;
}
uint32_t sum(const std::vector<uint8_t>& bitmap) {
uint32_t total = 0;
for (uint8_t value : bitmap) {
total += value;
}
return total;
}
void check_same_glyph(const BinFontGlyph& a, const BinFontGlyph& b) {
CHECK_EQ(a.glyph_id, b.glyph_id);
CHECK_EQ(a.advance_x16, b.advance_x16);
CHECK_EQ(a.x, b.x);
CHECK_EQ(a.y, b.y);
CHECK_EQ(a.width, b.width);
CHECK_EQ(a.height, b.height);
}
} // namespace
TEST_CASE("binfont_open_file returns ERROR_NOT_FOUND for a missing file") {
BinFont* font = nullptr;
CHECK_EQ(binfont_open_file(fixture("missing.bin").c_str(), &font), ERROR_NOT_FOUND);
}
TEST_CASE("binfont_open_memory rejects data that is not a font") {
const uint8_t data[64] = { 1, 2, 3, 4 };
BinFont* font = nullptr;
CHECK_EQ(binfont_open_memory(data, sizeof(data), false, &font), ERROR_NOT_SUPPORTED);
}
TEST_CASE("binfont reads metrics, glyphs and bitmaps that match lv_font_conv's C output") {
BinFont* font = open_fixture_in_memory("text_kern_pairs.bin");
BinFontMetrics metrics;
binfont_get_metrics(font, &metrics);
CHECK_EQ(metrics.size, 20);
CHECK_EQ(metrics.line_height, 20);
CHECK_EQ(metrics.base_line, 5);
CHECK_EQ(metrics.ascent, 15);
CHECK_EQ(metrics.descent, -5);
CHECK_EQ(metrics.underline_position, 1);
CHECK_EQ(metrics.underline_thickness, 0);
CHECK_EQ(metrics.bpp, 4);
BinFontGlyph glyph;
REQUIRE(binfont_get_glyph(font, 'A', &glyph));
CHECK_EQ(glyph.glyph_id, 1);
CHECK_EQ(glyph.advance_x16, 195);
CHECK_EQ(glyph.x, 0);
CHECK_EQ(glyph.y, 0);
CHECK_EQ(glyph.width, 13);
CHECK_EQ(glyph.height, 14);
const auto bitmap = decode(font, glyph);
const std::vector<uint8_t> first_row = { 0, 0, 0, 0, 0, 238, 255, 17, 0, 0, 0, 0, 0 };
const std::vector<uint8_t> last_row = { 221, 255, 0, 0, 0, 0, 0, 0, 0, 0, 238, 238, 0 };
CHECK(std::vector<uint8_t>(bitmap.begin(), bitmap.begin() + 13) == first_row);
CHECK(std::vector<uint8_t>(bitmap.end() - 13, bitmap.end()) == last_row);
CHECK_EQ(sum(bitmap), 13974);
REQUIRE(binfont_get_glyph(font, 'V', &glyph));
CHECK_EQ(glyph.glyph_id, 22);
REQUIRE(binfont_get_glyph(font, 'z', &glyph));
CHECK_EQ(glyph.glyph_id, 52);
CHECK_FALSE(binfont_get_glyph(font, '0', &glyph));
CHECK_FALSE(binfont_get_glyph(font, 0x5B, &glyph));
CHECK_FALSE(binfont_get_glyph(font, 0x10FFFF, &glyph));
binfont_close(font);
}
TEST_CASE("binfont kerning is the same for the pairs and the classes format") {
BinFont* pairs = open_fixture_in_memory("text_kern_pairs.bin");
BinFont* classes = open_fixture_in_memory("text_kern_classes.bin");
CHECK_EQ(binfont_get_kerning_x16(pairs, 1, 22), -10);
CHECK_EQ(binfont_get_kerning_x16(classes, 1, 22), -10);
int non_zero = 0;
for (uint32_t left = 1; left <= 52; left++) {
for (uint32_t right = 1; right <= 52; right++) {
const int32_t value = binfont_get_kerning_x16(pairs, left, right);
CHECK_EQ(value, binfont_get_kerning_x16(classes, left, right));
if (value != 0) {
non_zero++;
}
}
}
CHECK(non_zero > 0);
CHECK_EQ(binfont_get_kerning_x16(pairs, 1, 1000), 0);
CHECK_EQ(binfont_get_kerning_x16(classes, 1, 1000), 0);
binfont_close(pairs);
binfont_close(classes);
}
TEST_CASE("binfont decodes identical bitmaps for raw, RLE and RLE with prefilter") {
for (const char* bpp : { "2", "4" }) {
CAPTURE(bpp);
BinFont* raw = open_fixture_in_memory((std::string("icons_raw_") + bpp + ".bin").c_str());
BinFont* rle = open_fixture_in_memory((std::string("icons_rle_") + bpp + ".bin").c_str());
BinFont* prefiltered = open_fixture_in_memory((std::string("icons_rlepf_") + bpp + ".bin").c_str());
for (uint32_t codepoint : ICON_CODEPOINTS) {
CAPTURE(codepoint);
BinFontGlyph raw_glyph, rle_glyph, prefiltered_glyph;
REQUIRE(binfont_get_glyph(raw, codepoint, &raw_glyph));
REQUIRE(binfont_get_glyph(rle, codepoint, &rle_glyph));
REQUIRE(binfont_get_glyph(prefiltered, codepoint, &prefiltered_glyph));
check_same_glyph(raw_glyph, rle_glyph);
check_same_glyph(raw_glyph, prefiltered_glyph);
const auto raw_bitmap = decode(raw, raw_glyph);
CHECK(sum(raw_bitmap) > 0);
CHECK(decode(rle, rle_glyph) == raw_bitmap);
CHECK(decode(prefiltered, prefiltered_glyph) == raw_bitmap);
}
binfont_close(raw);
binfont_close(rle);
binfont_close(prefiltered);
}
}
TEST_CASE("binfont gives the same results when streaming from a file") {
BinFont* memory_font = open_fixture_in_memory("icons_rlepf_4.bin");
BinFont* file_font = nullptr;
REQUIRE(binfont_open_file_streaming(fixture("icons_rlepf_4.bin").c_str(), &file_font) == ERROR_NONE);
for (uint32_t codepoint : ICON_CODEPOINTS) {
BinFontGlyph memory_glyph, file_glyph;
REQUIRE(binfont_get_glyph(memory_font, codepoint, &memory_glyph));
REQUIRE(binfont_get_glyph(file_font, codepoint, &file_glyph));
check_same_glyph(memory_glyph, file_glyph);
CHECK(decode(memory_font, memory_glyph) == decode(file_font, file_glyph));
}
binfont_close(memory_font);
binfont_close(file_font);
}
TEST_CASE("binfont_open_file loads the font into memory") {
const std::string path = std::string(BINFONT_TEST_TEMP_DIR) + "/binfont_memory_test.bin";
size_t size;
uint8_t* data = load_file(fixture("icons_rlepf_4.bin"), size);
FILE* copy = fopen(path.c_str(), "wb");
REQUIRE(copy != nullptr);
REQUIRE(fwrite(data, 1, size, copy) == size);
fclose(copy);
memory_free(data);
BinFont* font = nullptr;
REQUIRE(binfont_open_file(path.c_str(), &font) == ERROR_NONE);
REQUIRE(remove(path.c_str()) == 0);
BinFontGlyph glyph;
CHECK(binfont_get_glyph(font, ICON_CODEPOINTS[0], &glyph));
CHECK(sum(decode(font, glyph)) > 0);
binfont_close(font);
}
TEST_CASE("binfont only keeps a streamed font file open during a session") {
// An open file stays readable after it's deleted, so deleting it shows whether it's kept open
const std::string path = std::string(BINFONT_TEST_TEMP_DIR) + "/binfont_session_test.bin";
size_t size;
uint8_t* data = load_file(fixture("icons_rlepf_4.bin"), size);
FILE* copy = fopen(path.c_str(), "wb");
REQUIRE(copy != nullptr);
REQUIRE(fwrite(data, 1, size, copy) == size);
fclose(copy);
memory_free(data);
BinFont* font = nullptr;
REQUIRE(binfont_open_file_streaming(path.c_str(), &font) == ERROR_NONE);
BinFontGlyph glyph;
REQUIRE(binfont_get_glyph(font, ICON_CODEPOINTS[0], &glyph));
REQUIRE(binfont_begin(font) == ERROR_NONE);
REQUIRE(binfont_begin(font) == ERROR_NONE);
REQUIRE(remove(path.c_str()) == 0);
binfont_end(font);
// Still open: one session remains
CHECK(binfont_get_glyph(font, ICON_CODEPOINTS[1], &glyph));
CHECK_FALSE(decode(font, glyph).empty());
binfont_end(font);
// Closed: the deleted file can't be opened again
CHECK_FALSE(binfont_get_glyph(font, ICON_CODEPOINTS[2], &glyph));
CHECK_EQ(binfont_begin(font), ERROR_NOT_FOUND);
binfont_close(font);
}
TEST_CASE("binfont_get_glyph_bitmap rejects a stride smaller than the glyph width") {
BinFont* font = open_fixture_in_memory("icons_raw_2.bin");
BinFontGlyph glyph;
REQUIRE(binfont_get_glyph(font, ICON_CODEPOINTS[0], &glyph));
std::vector<uint8_t> bitmap(static_cast<size_t>(glyph.width) * glyph.height);
CHECK_EQ(binfont_get_glyph_bitmap(font, &glyph, bitmap.data(), glyph.width - 1), ERROR_INVALID_ARGUMENT);
binfont_close(font);
}
TEST_CASE("binfont_generate rasterizes a TTF close to lv_font_conv") {
uint32_t codepoints[] = { 0xE8B8, 0xE5C3, 0x41, 0xE1A7, 0xE2C7, 0xE63E, 0xE5C3 }; // unsorted, duplicate and missing entries
const BinFontGeneratorConfig config = {
.ttf_path = BINFONT_TEST_ICON_TTF,
.size = 24,
.bpp = 4,
.codepoints = codepoints,
.codepoint_count = sizeof(codepoints) / sizeof(codepoints[0]),
};
uint8_t* data = nullptr;
size_t size = 0;
REQUIRE(binfont_generate(&config, &data, &size) == ERROR_NONE);
BinFont* generated = nullptr;
REQUIRE(binfont_open_memory(data, size, true, &generated) == ERROR_NONE);
BinFont* reference = open_fixture_in_memory("icons_raw_4.bin");
BinFontMetrics generated_metrics, reference_metrics;
binfont_get_metrics(generated, &generated_metrics);
binfont_get_metrics(reference, &reference_metrics);
CHECK_EQ(generated_metrics.size, 24);
CHECK_EQ(generated_metrics.bpp, 4);
CHECK_EQ(generated_metrics.ascent, reference_metrics.ascent);
CHECK_EQ(generated_metrics.descent, reference_metrics.descent);
BinFontGlyph glyph;
CHECK_FALSE(binfont_get_glyph(generated, 0x41, &glyph));
for (uint32_t codepoint : ICON_CODEPOINTS) {
CAPTURE(codepoint);
BinFontGlyph generated_glyph, reference_glyph;
REQUIRE(binfont_get_glyph(generated, codepoint, &generated_glyph));
REQUIRE(binfont_get_glyph(reference, codepoint, &reference_glyph));
CHECK(std::abs(static_cast<int>(generated_glyph.advance_x16) - static_cast<int>(reference_glyph.advance_x16)) <= 16);
CHECK(std::abs(generated_glyph.x - reference_glyph.x) <= 1);
CHECK(std::abs(generated_glyph.y - reference_glyph.y) <= 1);
CHECK(std::abs(generated_glyph.width - reference_glyph.width) <= 1);
CHECK(std::abs(generated_glyph.height - reference_glyph.height) <= 1);
const auto generated_sum = static_cast<double>(sum(decode(generated, generated_glyph)));
const auto reference_sum = static_cast<double>(sum(decode(reference, reference_glyph)));
CHECK(generated_sum > reference_sum * 0.9);
CHECK(generated_sum < reference_sum * 1.1);
}
binfont_close(generated);
binfont_close(reference);
}
TEST_CASE("binfont_generate fails for a missing TTF") {
const uint32_t codepoint = 0xE5C3;
const BinFontGeneratorConfig config = {
.ttf_path = BINFONT_TEST_FIXTURES_DIR "/missing.ttf",
.size = 24,
.bpp = 2,
.codepoints = &codepoint,
.codepoint_count = 1,
};
uint8_t* data = nullptr;
size_t size = 0;
CHECK_EQ(binfont_generate(&config, &data, &size), ERROR_NOT_FOUND);
}
TEST_CASE("binfont draws glyphs and text into a PixelBuffer") {
BinFont* font = open_fixture_in_memory("text_kern_pairs.bin");
PixelBuffer* buffer = pixel_buffer_create(DISPLAY_COLOR_FORMAT_RGB565, 64, 24);
REQUIRE(buffer != nullptr);
pixel_buffer_clear(buffer);
const int advance = binfont_draw_glyph_rgb565(buffer, 0, 18, font, 'A', 0xFFFF, PIXEL_BUFFER_CONVERSION_EXACT_BLACK);
CHECK_EQ(advance, (195 + 8) >> 4);
// Top of 'A' (row 0 of the glyph, column 6 is fully opaque) sits 14 rows above the baseline
CHECK_EQ(pixel_buffer_get_pixel_rgb565(buffer, 6, 18 - 14), 0xFFFF);
CHECK_EQ(pixel_buffer_get_pixel_rgb565(buffer, 0, 0), 0x0000);
CHECK_EQ(binfont_draw_glyph_rgb565(buffer, 0, 18, font, '0', 0xFFFF, PIXEL_BUFFER_CONVERSION_EXACT_BLACK), 0);
// "AV" is kerned: 195 - 10 + V's advance
BinFontGlyph v;
REQUIRE(binfont_get_glyph(font, 'V', &v));
pixel_buffer_clear(buffer);
const int width = binfont_draw_text_rgb565(buffer, 0, 18, font, "AV", 0xFFFF, PIXEL_BUFFER_CONVERSION_EXACT_BLACK);
CHECK_EQ(width, static_cast<int>((195 - 10 + v.advance_x16 + 8) >> 4));
pixel_buffer_free(buffer);
binfont_close(font);
}

View File

@ -0,0 +1,64 @@
#define DOCTEST_CONFIG_IMPLEMENT
#include "doctest.h"
#include <cstdio>
#include <cstdlib>
#include "FreeRTOS.h"
#include "task.h"
typedef struct {
int argc;
char** argv;
int result;
} TestTaskData;
void test_task(void* parameter) {
auto* data = (TestTaskData*)parameter;
doctest::Context context;
context.applyCommandLine(data->argc, data->argv);
// overrides
context.setOption("no-breaks", true); // don't break in the debugger when assertions fail
data->result = context.run();
vTaskEndScheduler();
vTaskDelete(nullptr);
}
int main(int argc, char** argv) {
TestTaskData data = {
.argc = argc,
.argv = argv,
.result = 0
};
BaseType_t task_result = xTaskCreate(
test_task,
"test_task",
8192,
&data,
1,
nullptr
);
if (task_result != pdPASS) {
return 1;
}
vTaskStartScheduler();
return data.result;
}
// NOTE: This is normally provided by the platform kernel module, but that's not loaded for binfont-module
extern "C" {
// Required for FreeRTOS
void vAssertCalled(unsigned long line, const char* const file) {
std::fprintf(stderr, "assert failed at %s:%lu\n", file, line);
std::abort();
}
}

View File

@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.20)
include("${CMAKE_CURRENT_LIST_DIR}/../../Buildscripts/module.cmake")
file(GLOB_RECURSE SOURCE_FILES "source/*.c*")
list(APPEND REQUIRES_LIST
TactilityKernel
)
tactility_add_module(lodepng-module
SRCS ${SOURCE_FILES}
INCLUDE_DIRS include/
REQUIRES ${REQUIRES_LIST}
)

View File

@ -0,0 +1,195 @@
Apache License
==============
_Version 2.0, January 2004_
_&lt;<http://www.apache.org/licenses/>&gt;_
### Terms and Conditions for use, reproduction, and distribution
#### 1. Definitions
“License” shall mean the terms and conditions for use, reproduction, and
distribution as defined by Sections 1 through 9 of this document.
“Licensor” shall mean the copyright owner or entity authorized by the copyright
owner that is granting the License.
“Legal Entity” shall mean the union of the acting entity and all other entities
that control, are controlled by, or are under common control with that entity.
For the purposes of this definition, “control” means **(i)** the power, direct or
indirect, to cause the direction or management of such entity, whether by
contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
outstanding shares, or **(iii)** beneficial ownership of such entity.
“You” (or “Your”) shall mean an individual or Legal Entity exercising
permissions granted by this License.
“Source” form shall mean the preferred form for making modifications, including
but not limited to software source code, documentation source, and configuration
files.
“Object” form shall mean any form resulting from mechanical transformation or
translation of a Source form, including but not limited to compiled object code,
generated documentation, and conversions to other media types.
“Work” shall mean the work of authorship, whether in Source or Object form, made
available under the License, as indicated by a copyright notice that is included
in or attached to the work (an example is provided in the Appendix below).
“Derivative Works” shall mean any work, whether in Source or Object form, that
is based on (or derived from) the Work and for which the editorial revisions,
annotations, elaborations, or other modifications represent, as a whole, an
original work of authorship. For the purposes of this License, Derivative Works
shall not include works that remain separable from, or merely link (or bind by
name) to the interfaces of, the Work and Derivative Works thereof.
“Contribution” shall mean any work of authorship, including the original version
of the Work and any modifications or additions to that Work or Derivative Works
thereof, that is intentionally submitted to Licensor for inclusion in the Work
by the copyright owner or by an individual or Legal Entity authorized to submit
on behalf of the copyright owner. For the purposes of this definition,
“submitted” means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems, and
issue tracking systems that are managed by, or on behalf of, the Licensor for
the purpose of discussing and improving the Work, but excluding communication
that is conspicuously marked or otherwise designated in writing by the copyright
owner as “Not a Contribution.”
“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
of whom a Contribution has been received by Licensor and subsequently
incorporated within the Work.
#### 2. Grant of Copyright License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the Work and such
Derivative Works in Source or Object form.
#### 3. Grant of Patent License
Subject to the terms and conditions of this License, each Contributor hereby
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
irrevocable (except as stated in this section) patent license to make, have
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
such license applies only to those patent claims licensable by such Contributor
that are necessarily infringed by their Contribution(s) alone or by combination
of their Contribution(s) with the Work to which such Contribution(s) was
submitted. If You institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
Contribution incorporated within the Work constitutes direct or contributory
patent infringement, then any patent licenses granted to You under this License
for that Work shall terminate as of the date such litigation is filed.
#### 4. Redistribution
You may reproduce and distribute copies of the Work or Derivative Works thereof
in any medium, with or without modifications, and in Source or Object form,
provided that You meet the following conditions:
* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
this License; and
* **(b)** You must cause any modified files to carry prominent notices stating that You
changed the files; and
* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
all copyright, patent, trademark, and attribution notices from the Source form
of the Work, excluding those notices that do not pertain to any part of the
Derivative Works; and
* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
Derivative Works that You distribute must include a readable copy of the
attribution notices contained within such NOTICE file, excluding those notices
that do not pertain to any part of the Derivative Works, in at least one of the
following places: within a NOTICE text file distributed as part of the
Derivative Works; within the Source form or documentation, if provided along
with the Derivative Works; or, within a display generated by the Derivative
Works, if and wherever such third-party notices normally appear. The contents of
the NOTICE file are for informational purposes only and do not modify the
License. You may add Your own attribution notices within Derivative Works that
You distribute, alongside or as an addendum to the NOTICE text from the Work,
provided that such additional attribution notices cannot be construed as
modifying the License.
You may add Your own copyright statement to Your modifications and may provide
additional or different license terms and conditions for use, reproduction, or
distribution of Your modifications, or for any such Derivative Works as a whole,
provided Your use, reproduction, and distribution of the Work otherwise complies
with the conditions stated in this License.
#### 5. Submission of Contributions
Unless You explicitly state otherwise, any Contribution intentionally submitted
for inclusion in the Work by You to the Licensor shall be under the terms and
conditions of this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify the terms of
any separate license agreement you may have executed with Licensor regarding
such Contributions.
#### 6. Trademarks
This License does not grant permission to use the trade names, trademarks,
service marks, or product names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the NOTICE file.
#### 7. Disclaimer of Warranty
Unless required by applicable law or agreed to in writing, Licensor provides the
Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
including, without limitation, any warranties or conditions of TITLE,
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
solely responsible for determining the appropriateness of using or
redistributing the Work and assume any risks associated with Your exercise of
permissions under this License.
#### 8. Limitation of Liability
In no event and under no legal theory, whether in tort (including negligence),
contract, or otherwise, unless required by applicable law (such as deliberate
and grossly negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special, incidental,
or consequential damages of any character arising as a result of this License or
out of the use or inability to use the Work (including but not limited to
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
any and all other commercial damages or losses), even if such Contributor has
been advised of the possibility of such damages.
#### 9. Accepting Warranty or Additional Liability
While redistributing the Work or Derivative Works thereof, You may choose to
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
other liability obligations and/or rights consistent with this License. However,
in accepting such obligations, You may act only on Your own behalf and on Your
sole responsibility, not on behalf of any other Contributor, and only if You
agree to indemnify, defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason of your
accepting any such warranty or additional liability.
_END OF TERMS AND CONDITIONS_
### APPENDIX: How to apply the Apache License to your work
To apply the Apache License to your work, attach the following boilerplate
notice, with the fields enclosed by brackets `[]` replaced with your own
identifying information. (Don't include the brackets!) The text should be
enclosed in the appropriate comment syntax for the file format. We also
recommend that a file or class name and description of purpose be included on
the same “printed page” as the copyright notice for easier identification within
third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,25 @@
/*
LodePNG version 20261001
Copyright (c) 2005-2026 Lode Vandevenne
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source
distribution.
*/

View File

@ -0,0 +1,10 @@
# lodepng-module
PNG decoding and encoding with [LodePNG](https://github.com/lvandeve/lodepng), usable with and without LVGL.
Include `<lodepng/lodepng.h>` and release decoded or encoded data with `lodepng_free()`.
## License
This module is licensed under the [Apache v2.0](LICENSE-Apache-2.0.md) license.
LodePNG is licensed under the [zlib](LICENSE-lodepng.txt) license.

View File

@ -0,0 +1,25 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
// The C++ wrapper is not compiled: callers use the C API from C and C++ alike
#define LODEPNG_NO_COMPILE_CPP
// Allocations go through the kernel's memory API (see lodepng_malloc() below)
#define LODEPNG_NO_COMPILE_ALLOCATORS
#ifdef __cplusplus
extern "C" {
#endif
#include <lodepng/lodepng_upstream.h>
/** Allocates memory, preferring external memory (PSRAM) */
void* lodepng_malloc(size_t size);
void* lodepng_realloc(void* ptr, size_t new_size);
/** Releases memory from lodepng_malloc(), including images returned by the decode and encode functions */
void lodepng_free(void* ptr);
#ifdef __cplusplus
}
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,14 @@
// SPDX-License-Identifier: Apache-2.0
#pragma once
#include <tactility/module.h>
#ifdef __cplusplus
extern "C" {
#endif
extern struct Module lodepng_module;
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,22 @@
// SPDX-License-Identifier: Apache-2.0
#include <lodepng/lodepng.h>
#include <tactility/memory.h>
static const struct MemoryPolicy EXTERNAL_PREFERRED = {
.required = 0,
.desired = MEMORY_CAPABILITY_EXTERNAL,
.alignment = 0,
};
void* lodepng_malloc(size_t size) {
return memory_alloc_with_policy(size, &EXTERNAL_PREFERRED);
}
void* lodepng_realloc(void* ptr, size_t new_size) {
return memory_realloc_with_policy(ptr, new_size, &EXTERNAL_PREFERRED);
}
void lodepng_free(void* ptr) {
memory_free(ptr);
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,37 @@
// SPDX-License-Identifier: Apache-2.0
#include <lodepng/lodepng.h>
#include <lodepng/module.h>
static const struct ModuleSymbol SYMBOLS[] = {
// decode
DEFINE_MODULE_SYMBOL(lodepng_decode_memory),
DEFINE_MODULE_SYMBOL(lodepng_decode32),
DEFINE_MODULE_SYMBOL(lodepng_decode24),
DEFINE_MODULE_SYMBOL(lodepng_decode_file),
DEFINE_MODULE_SYMBOL(lodepng_decode32_file),
DEFINE_MODULE_SYMBOL(lodepng_decode24_file),
// encode
DEFINE_MODULE_SYMBOL(lodepng_encode_memory),
DEFINE_MODULE_SYMBOL(lodepng_encode32),
DEFINE_MODULE_SYMBOL(lodepng_encode24),
DEFINE_MODULE_SYMBOL(lodepng_encode_file),
DEFINE_MODULE_SYMBOL(lodepng_encode32_file),
DEFINE_MODULE_SYMBOL(lodepng_encode24_file),
// utilities
DEFINE_MODULE_SYMBOL(lodepng_error_text),
DEFINE_MODULE_SYMBOL(lodepng_load_file),
DEFINE_MODULE_SYMBOL(lodepng_save_file),
DEFINE_MODULE_SYMBOL(lodepng_malloc),
DEFINE_MODULE_SYMBOL(lodepng_realloc),
DEFINE_MODULE_SYMBOL(lodepng_free),
MODULE_SYMBOL_TERMINATOR,
};
struct Module lodepng_module = {
.name = "lodepng",
.start = NULL,
.stop = NULL,
.drivers = NULL,
.symbols = SYMBOLS,
.internal = NULL,
};

View File

@ -0,0 +1,20 @@
project(LodePngModuleTests)
enable_language(C CXX ASM)
file(GLOB_RECURSE TEST_SOURCES CONFIGURE_DEPENDS ${PROJECT_SOURCE_DIR}/source/*.cpp)
add_executable(LodePngModuleTests EXCLUDE_FROM_ALL ${TEST_SOURCES})
target_include_directories(LodePngModuleTests PRIVATE ${DOCTESTINC})
target_compile_definitions(LodePngModuleTests PRIVATE
"LODEPNG_TEST_LOGO=\"${CMAKE_SOURCE_DIR}/Data/system/app/Boot/assets/logo.png\""
)
add_test(NAME LodePngModuleTests COMMAND LodePngModuleTests)
target_link_libraries(LodePngModuleTests PUBLIC
lodepng-module
TactilityKernel
freertos_kernel
)

View File

@ -0,0 +1,47 @@
#include "doctest.h"
#include <lodepng/lodepng.h>
#include <cstring>
TEST_CASE("lodepng decodes a PNG file") {
unsigned char* image = nullptr;
unsigned width = 0;
unsigned height = 0;
REQUIRE_EQ(lodepng_decode32_file(&image, &width, &height, LODEPNG_TEST_LOGO), 0);
CHECK_EQ(width, 80);
CHECK_EQ(height, 103);
lodepng_free(image);
}
TEST_CASE("lodepng returns an error for a missing file") {
unsigned char* image = nullptr;
unsigned width = 0;
unsigned height = 0;
CHECK_NE(lodepng_decode32_file(&image, &width, &height, "missing.png"), 0);
lodepng_free(image);
}
TEST_CASE("lodepng encodes and decodes an image without changes") {
constexpr unsigned width = 3;
constexpr unsigned height = 2;
const unsigned char pixels[width * height * 4] = {
255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255,
0, 0, 0, 0, 255, 255, 255, 128, 10, 20, 30, 40,
};
unsigned char* png = nullptr;
size_t png_size = 0;
REQUIRE_EQ(lodepng_encode32(&png, &png_size, pixels, width, height), 0);
unsigned char* decoded = nullptr;
unsigned decoded_width = 0;
unsigned decoded_height = 0;
REQUIRE_EQ(lodepng_decode32(&decoded, &decoded_width, &decoded_height, png, png_size), 0);
CHECK_EQ(decoded_width, width);
CHECK_EQ(decoded_height, height);
CHECK_EQ(memcmp(decoded, pixels, sizeof(pixels)), 0);
lodepng_free(png);
lodepng_free(decoded);
}

Some files were not shown because too many files have changed in this diff Show More