From ddd047b2f250752cb92e0a084bb4c60124623486 Mon Sep 17 00:00:00 2001 From: Eric <0015@users.noreply.github.com> Date: Thu, 11 Sep 2025 21:38:59 -0700 Subject: [PATCH] feat: Release version 1.2.0 with new features --- README.md | 4 +- idf_component.yml | 2 +- script/README.md | 49 ++++++++ script/lvgl_map_tile_converter.py | 178 ++++++++++++++++++++++++++++++ 4 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 script/README.md create mode 100644 script/lvgl_map_tile_converter.py diff --git a/README.md b/README.md index d19e7eb..c39017c 100755 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ You can easily add this component to your project using the idf.py command or by From your project's root directory, simply run the following command in your terminal: ```bash -idf.py add-dependency "0015/map_tiles^1.1.0" +idf.py add-dependency "0015/map_tiles^1.2.0" ``` This command will automatically add the component to your idf_component.yml file and download the required files the next time you build your project. @@ -43,7 +43,7 @@ Add to your project's `main/idf_component.yml`: dependencies: map_tiles: git: "https://github.com/0015/map_tiles.git" - version: "^1.1.0" + version: "^1.2.0" ``` ### Manual Installation diff --git a/idf_component.yml b/idf_component.yml index 64bb74e..2609a7d 100755 --- a/idf_component.yml +++ b/idf_component.yml @@ -1,6 +1,6 @@ namespace: "0015" name: "map_tiles" -version: "1.1.0" +version: "1.2.0" maintainers: - Eric Nam description: "Map tiles component for LVGL 9.x - Load and display map tiles with GPS coordinate conversion" diff --git a/script/README.md b/script/README.md new file mode 100644 index 0000000..939291b --- /dev/null +++ b/script/README.md @@ -0,0 +1,49 @@ +# **LVGL Map Tile Converter** + +This Python script is designed to convert a directory of PNG map tiles into a format compatible with LVGL (Light and Versatile Graphics Library) version 9\. The output is a series of binary files (.bin) with a header and pixel data in the RGB565 format, optimized for direct use with LVGL's image APIs. + +The script is multithreaded and can significantly speed up the conversion process for large tile sets. + +## **Features** + +* **PNG to RGB565 Conversion**: Converts standard 24-bit PNG images into 16-bit RGB565. +* **LVGL v9 Compatibility**: Generates a .bin file with the correct LVGL v9 header format. +* **Multithreaded Conversion**: Utilizes a ThreadPoolExecutor to process tiles in parallel, configurable with the \--jobs flag. +* **Directory Traversal**: Automatically finds and converts all .png files within a specified input directory structure. +* **Skip Existing Files**: Skips conversion for tiles that already exist in the output directory unless the \--force flag is used. + +## **Requirements** + +The script requires the Pillow library to handle image processing. You can install it using pip: +```bash +pip install Pillow +``` + +## **Usage** + +The script is a command-line tool. You can run it from your terminal with the following arguments: +```bash +python lvgl_map_tile_converter.py --input --output [options] +``` + +### **Arguments** + +* \-i, \--input: **Required**. The root folder containing your map tiles. The script expects a tile structure like zoom/x/y.png. +* \-o, \--output: **Required**. The root folder where the converted .bin tiles will be saved. The output structure will mirror the input: zoom/x/y.bin. +* \-j, \--jobs: **Optional**. The number of worker threads to use for the conversion. Defaults to the number of CPU cores on your system. Using more jobs can speed up the process. +* \-f, \--force: **Optional**. If this flag is set, the script will re-convert all tiles, even if the output .bin files already exist. + +### **Examples** + +**1\. Basic conversion with default settings:** +```bash +python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 +``` +**2\. Conversion using a specific number of threads (e.g., 8):** +```bash +python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 --jobs 8 +``` +**3\. Forcing a full re-conversion:** +```bash +python lvgl_map_tile_converter.py --input ./map_tiles --output ./tiles1 --force +``` \ No newline at end of file diff --git a/script/lvgl_map_tile_converter.py b/script/lvgl_map_tile_converter.py new file mode 100644 index 0000000..43d4014 --- /dev/null +++ b/script/lvgl_map_tile_converter.py @@ -0,0 +1,178 @@ +import os +import struct +import argparse +import threading +from concurrent.futures import ThreadPoolExecutor, as_completed +from PIL import Image # pip install pillow + +# No implicit defaults; these are set from CLI in main() +INPUT_ROOT = None +OUTPUT_ROOT = None + + +# Convert RGB to 16-bit RGB565 +def to_rgb565(r, g, b): + return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3) + + +# Strip .png or .bin extensions +def clean_tile_name(filename): + name = filename + while True: + name, ext = os.path.splitext(name) + if ext.lower() not in [".png", ".bin", ".jpg", ".jpeg"]: + break + filename = name + return filename + + +# Create LVGL v9-compatible .bin image +def make_lvgl_bin(png_path, bin_path): + im = Image.open(png_path).convert("RGB") + w, h = im.size + pixels = im.load() + + stride = (w * 16 + 7) // 8 # bytes per row (RGB565 = 16 bpp) + flags = 0x00 # no compression, no premult + color_format = 0x12 # RGB565 + magic = 0x19 + + header = bytearray() + header += struct.pack("=1) + - force: if True, re-generate even if output exists + """ + if not os.path.isdir(INPUT_ROOT): + print(f"[ERROR] '{INPUT_ROOT}' not found.") + return + + # Build task list (skip existing unless --force) + tasks = [] + for input_path, output_path in _iter_tile_paths(): + if not force and os.path.isfile(output_path): + print(f"[Skip] {output_path}") + continue + tasks.append((input_path, output_path)) + + if not tasks: + print("[INFO] Nothing to do.") + return + + print(f"[INFO] Converting {len(tasks)} tiles with {jobs} thread(s)...") + + if jobs <= 1: + # Serial path + for inp, outp in tasks: + try: + make_lvgl_bin(inp, outp) + except Exception as e: + print(f"[Error] Failed to convert {inp} → {e}") + return + + # Threaded path + print_lock = threading.Lock() + with ThreadPoolExecutor(max_workers=jobs) as ex: + future_map = {ex.submit(make_lvgl_bin, inp, outp): (inp, outp) for inp, outp in tasks} + for fut in as_completed(future_map): + inp, outp = future_map[fut] + try: + fut.result() + except Exception as e: + with print_lock: + print(f"[Error] Failed to convert {inp} → {e}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Convert OSM PNG tiles into LVGL-friendly .bin files (RGB565).", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + parser.add_argument( + "-i", "--input", + required=True, + default=argparse.SUPPRESS, # hide '(default: None)' + help="Input root folder containing tiles in zoom/x/y.png structure", + ) + parser.add_argument( + "-o", "--output", + required=True, + default=argparse.SUPPRESS, # hide '(default: None)' + help="Output root folder where .bin tiles will be written", + ) + parser.add_argument( + "-j", "--jobs", + type=int, + default=os.cpu_count(), + help="Number of worker threads", + ) + parser.add_argument( + "-f", "--force", + action="store_true", + help="Rebuild even if output file already exists", + ) + + args = parser.parse_args() + + # Basic checks + if not os.path.isdir(args.input): + parser.error(f"Input folder not found or not a directory: {args.input}") + os.makedirs(args.output, exist_ok=True) + + # Apply CLI values + INPUT_ROOT = args.input + OUTPUT_ROOT = args.output + + convert_all_tiles(jobs=max(1, args.jobs), force=args.force)