A JSON Domain Language to draw on E-Ink displays by receiving a JSON payload from any endpoint. Built on top of FastEPD and packaged as an ESP-IDF component targeting the ESP32 family (primary targets: ESP32-C5, S3, P4).
If you would like a good start to make a web-interface for a basic Canvas draw tool, you can check out: FastJsonRenderer Using Symfony as a PHP backend and Rest to make the dynamic canvas drawing tool. Is very basic but it can be a great starting point to understand the JSON format and draw some shapes. Soon it will have a Bluetooth send to device feature! To try it online please go to draw.fasani.de and be aware that to save or to use the import PNG/SVG feature into G5 image you need to login with your Github account.
- Parses a JSON layout description and issues the corresponding drawing
commands to a
FASTEPDdisplay instance. - Configurable bits-per-pixel mode (
display_bppin JSON, or viasetDefaultBpp()). - Named-font registry — map any string to a FastEPD font data blob.
- In-order item rendering: items in the
"items"array are drawn sequentially, so layering (e.g. black bar behind white text) works exactly as written. - Human-readable error reporting via
getLastError(). - DEFLATE decompression —
renderDeflatedJson()accepts a raw DEFLATE-compressed payload (RFC 1951, no zlib/gzip wrapper) and decompresses it on-device before rendering. Requires theminizESP-IDF component.
To run the demos easily please clone this repository using:
git clone --recursive https://github.com/martinberlin/FastJsonDL.git
This json code:
{
"display_bpp": 4,
"clear": true,
"items": [
{ "type": "fillRect", "x": 0, "y": 0, "w": 540, "h": 120, "c": 14 },
{ "type": "fillRect", "x": 20, "y": 150, "w": 670, "h": 220, "c": 12 },
{ "type": "drawRect", "x": 20, "y": 150, "w": 670, "h": 220, "c": 4 },
{ "type": "drawLine", "x1": 20, "y1": 150, "x2": 520, "y2": 370, "c": 6 },
{ "type": "drawLine", "x1": 520, "y1": 150, "x2": 20, "y2": 370, "c": 6 },
{ "type": "fillCircle", "x": 90, "y": 520, "r": 60, "c": 10 },
{ "type": "drawCircle", "x": 90, "y": 520, "r": 60, "c": 2 },
{ "type": "fillCircle", "x": 270, "y": 520, "r": 60, "c": 7 },
{ "type": "drawCircle", "x": 270, "y": 520, "r": 60, "c": 2 },
{ "type": "fillCircle", "x": 450, "y": 520, "r": 60, "c": 3 },
{ "type": "drawCircle", "x": 450, "y": 520, "r": 60, "c": 2 },
{
"type": "drawString",
"font": "Ubuntu40",
"string": "FastJsonDL",
"x": 30,
"y": 70,
"c": 0
},
{
"type": "drawString",
"font": "Ubuntu30",
"string": "Grayscale: c0 c7 c14",
"x": 30,
"y": 240,
"c": 7
},
{
"type": "drawString",
"font": "Ubuntu40",
"string": "Now with working",
"x": 30,
"y": 330,
"c": 14
},
{
"type": "drawString",
"font": "Ubuntu40",
"string": "shades",
"x": 690,
"y": 330,
"c": 9
},
{
"type": "drawString",
"font": "Ubuntu30",
"string": "0",
"x": 80,
"y": 530,
"c": 15
}
]
}Will generate the following drawing:

| Field | Type | Default | Description |
|---|---|---|---|
display_bpp |
integer | current EPD mode (usually 1) |
Bits per pixel: 1, 2, or 4. If omitted, FastJsonDL keeps the mode active on the FASTEPD instance when it was constructed. |
rotation |
integer | — | Display rotation in degrees (0, 90, 180, 270). When present, setRotation() is called and the logical display dimensions are refreshed so that portrait-mode items near the right/bottom edge render correctly. |
clear |
bool | false |
When true, fills the framebuffer with white before rendering any items. Use this to avoid uninitialised pixel data appearing as vertical stripes on the display. |
"type" |
Required fields | Optional |
|---|---|---|
drawString |
string, x, y |
font, c |
fillRect |
x, y, w, h |
c |
drawRect |
x, y, w, h |
c |
drawLine |
x1, y1, x2, y2 |
c |
fillCircle |
x, y, r |
c |
drawCircle |
x, y, r |
c |
p |
x, y |
c |
loadG5Image |
data, x, y, w, h |
fg, bg |
c is the colour value and depends on display_bpp:
- 1BPP:
0..1(0= black,1= white) - 2BPP:
0..3(0= black,3= white) - 4BPP:
0..15(0= black,15= white)
When omitted, c defaults to black (0).
For loadG5Image, data must be a byte array and supports:
- decimal byte values (
191, 187, 90, ...) - hex strings with or without the
0xprefix ("bf","0xbf","13","0x13")
drawString— y is the text baseline FastEPD BB-format fonts (produced byfontconvert) treatyas the text baseline, not the top-left corner of the glyph. For a 40 pt font the ascender height is approximately 50 px, so the top of the rendered characters sits aty − 50. Setting"y": 10with such a font places the glyphs almost entirely above the top edge of the screen (invisible, no error). Always sety≥ the font's ascender height — for Ubuntu40 use"y": 50or greater.
{
"display_bpp": 4,
"clear": true,
"items": [
{
"type": "drawString",
"font": "Ubuntu40",
"string": "Hello from FastJsonDL!",
"x": 10, "y": 70,
"c": 0
}
]
}// 1. Initialise the EPD panel (FastEPD)
FASTEPD epd;
epd.initPanel(BB_PANEL_M5PAPERS3);
// 2. Create the renderer
FastJsonDL dl(epd);
// 3. (Optional) Register named fonts
static const FastJsonDLFont fonts[] = {
{ "Ubuntu40", Ubuntu40 },
};
dl.setFontRegistry(fonts, 1);
// 4. (Optional) Override display dimensions or BPP default
dl.setDisplaySize(540, 960);
dl.setDefaultBpp(1);
// 5a. Render a plain JSON layout
if (!dl.renderJsonString(myJson)) {
printf("Error: %s\n", dl.getLastError());
}
// 5b. Render a raw DEFLATE-compressed JSON layout (type 0x0002)
// Requires lbernstone__miniz component.
if (!dl.renderDeflatedJson(compressedBuf, compressedLen)) {
printf("Error: %s\n", dl.getLastError());
}
// 6. Push to the physical display
epd.fullUpdate();Add to your project's idf_component.yml:
dependencies:
martinberlin__FastJsonDL:
git: "https://github.com/martinberlin/FastJsonDL.git"FastEPD is intentionally not declared as a managed dependency by this
component. Add your own FastEPD source (for example a submodule or a custom
branch checkout) in your project and wire it as a normal ESP-IDF component
named FastEPD.
Clone into your project's components/ directory:
cd components
git clone https://github.com/martinberlin/FastJsonDL.git FastJsonDLThen add your own FastEPD component (for example as a submodule pointing to
your preferred branch) under components/FastEPD.
Then add FastJsonDL to the REQUIRES list in your app component's
CMakeLists.txt.
| Dependency | Source |
|---|---|
| FastEPD | User-provided ESP-IDF component (e.g. local submodule/custom branch) |
| cJSON | Ships with ESP-IDF (json component) |
| lbernstone__miniz | ESP-IDF component manager (lbernstone/miniz); required for renderDeflatedJson() |
renderDeflatedJson() requires the
lbernstone/miniz ESP-IDF component.
Add it to your project's idf_component.yml:
dependencies:
lbernstone__miniz:
version: ">=0.0.1"If the component is absent the library still compiles and all other methods
work normally; renderDeflatedJson() will return false and set an error
message explaining the missing dependency.
| Example | Description |
|---|---|
examples/basic |
Renders a static JSON layout at boot — good starting point. |
examples/ble_receive |
BLE GATT server that receives a JSON payload over Bluetooth and renders it on the display. Supports both plain JSON (type 0x0001) and raw DEFLATE-compressed JSON (type 0x0002). Compatible with the FastJsonRenderer client. |
The 8-byte binary header prepended by the client:
| Bytes | Field | Value |
|---|---|---|
| 0–1 | type | 0x0001 — plain JSON | 0x0002 — raw DEFLATE compressed JSON |
| 2–7 | length | payload byte count (little-endian uint48; bytes 6–7 are always 0x00) |
Type 0x0002 payloads are raw DEFLATE streams as produced by
pako.deflateRaw() in JavaScript or zlib.compress(data)[2:-4] in Python.
The firmware decompresses the payload on-device using tinfl_decompress_mem_to_heap
from the miniz library before parsing the JSON.
Do not send zlib-wrapped data (deflate); use raw DEFLATE only
(deflate-raw / deflateRaw) for compressed BLE frames.
See LICENSE.