feat: standalone LvScreenshot library v1.0.0
Promoted from DIYStickyNotes/lib/LvScreenshot (byte-identical src) into a reusable Gitea library. Adds LICENSE, .gitignore, library.properties, keywords.txt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
.pio/
|
||||
.vscode/
|
||||
.idea/
|
||||
__pycache__/
|
||||
*.o
|
||||
*.d
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 xPablo.cz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,72 @@
|
||||
# LvScreenshot
|
||||
|
||||
Board-independent **LVGL 9 screen capture** as a reusable PlatformIO library: grab the current screen
|
||||
into a tight **RGB565** buffer via `lv_snapshot_take()`, optionally serve it over HTTP, and save it as
|
||||
a PNG on your PC. Because it re-renders the LVGL object tree (not the panel framebuffer), it works with
|
||||
**any** display driver — partial-flush (LovyanGFX), full-frame (Arduino_GFX), or RGB panel.
|
||||
|
||||
## Requirements
|
||||
|
||||
- LVGL 9 with **`LV_USE_SNAPSHOT 1`** in your `lv_conf.h` (the core warns at compile time otherwise).
|
||||
- **PSRAM must be enabled/available on ESP32** — see "Memory / PSRAM" below. This is essential.
|
||||
- The optional HTTP helper needs `ESPAsyncWebServer`; it self-disables (no-op) when that is absent.
|
||||
|
||||
## Memory / PSRAM (important)
|
||||
|
||||
A full-frame snapshot is large — e.g. 320×480 RGB565 = **~300 KB**. LVGL's stock `lv_snapshot_take()`
|
||||
allocates that from LVGL's own heap, which with **`LV_USE_STDLIB_MALLOC = LV_STDLIB_BUILTIN`** (the
|
||||
default) is a small fixed pool (`LV_MEM_SIZE`). It cannot fit ~300 KB, so `lv_snapshot_take()` simply
|
||||
**returns null and the capture fails** — the symptom that must be worked around whenever capture goes
|
||||
through LVGL.
|
||||
|
||||
This library avoids that: instead of letting LVGL allocate, it allocates the draw buffer itself from
|
||||
**PSRAM** (`heap_caps_malloc(MALLOC_CAP_SPIRAM)` on ESP32, plain `malloc` elsewhere) and renders into
|
||||
it via `lv_snapshot_take_to_draw_buf()`. Consequences for integrators:
|
||||
|
||||
- **Enable PSRAM** in your build (e.g. `-DBOARD_HAS_PSRAM`, `board_build.arduino.memory_type` set to a
|
||||
`*_opi`/`*_qspi` PSRAM variant). Without a working PSRAM heap the two ~300 KB allocations (draw buffer
|
||||
+ tight-packed copy) will fail and capture returns `false`.
|
||||
- You do **not** need to enlarge `LV_MEM_SIZE` or switch `LV_USE_STDLIB_MALLOC` — the library
|
||||
deliberately bypasses LVGL's allocator for the snapshot buffer for exactly this reason.
|
||||
- Peak transient usage is ~2× the frame (~600 KB) during a capture; both buffers are freed right after
|
||||
(the HTTP helper keeps one until the next request so it can stream it).
|
||||
|
||||
## Core API (`LvScreenshot.hpp`, LVGL-only)
|
||||
|
||||
```cpp
|
||||
LvScreenshot shot;
|
||||
if (lvgl_port_lock(-1)) { // the caller must hold the LVGL lock
|
||||
bool ok = lvScreenshotCaptureTop(&shot); // topmost overlay, else the active screen
|
||||
lvgl_port_unlock();
|
||||
if (ok) { /* shot.data = RGB565, shot.width/height/length */ lvScreenshotFree(&shot); }
|
||||
}
|
||||
```
|
||||
`lvScreenshotCapture(obj, &shot)` captures a specific object. Buffers use PSRAM on ESP32.
|
||||
|
||||
## Optional HTTP endpoint (`LvScreenshotHttp.hpp`, needs ESPAsyncWebServer)
|
||||
|
||||
```cpp
|
||||
#include <LvScreenshotHttp.hpp>
|
||||
// in your web-server setup:
|
||||
lvScreenshotAttachHttp(www, lvgl_port_lock, lvgl_port_unlock); // registers GET /screenshot
|
||||
```
|
||||
The route returns the raw RGB565 body plus `X-Width` / `X-Height` / `X-Format` headers.
|
||||
|
||||
## PC tool
|
||||
|
||||
```bash
|
||||
uv run tools/screenshot.py --host diystickynotes.local --out shot.png
|
||||
# --swap-rb if colours look inverted (BGR panel)
|
||||
# --byteswap if the image is garbled (big-endian RGB565)
|
||||
```
|
||||
Converts the RGB565 response to a PNG (numpy + Pillow; deps auto-installed via uv / PEP 723).
|
||||
|
||||
## Gating it to dev builds (recommended)
|
||||
|
||||
Keep production lean: flip `LV_USE_SNAPSHOT` on only under a build flag, and only call
|
||||
`lvScreenshotAttachHttp` under the same flag, so normal builds are byte-identical and expose no
|
||||
endpoint. See the DIYStickyNotes `[env:screenshot_*]` envs for an example.
|
||||
|
||||
## License
|
||||
|
||||
MIT (code). Screenshots you capture are your own content.
|
||||
@@ -0,0 +1,3 @@
|
||||
LvScreenshot KEYWORD1
|
||||
lvScreenshotTake KEYWORD2
|
||||
lvScreenshotAttachHttp KEYWORD2
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "LvScreenshot",
|
||||
"version": "1.0.0",
|
||||
"description": "Board-independent LVGL 9 screen capture (lv_snapshot_take -> tight RGB565) plus an optional AsyncWebServer /screenshot endpoint and a PC tool that saves a PNG.",
|
||||
"keywords": "lvgl, screenshot, snapshot, capture, png, esp32",
|
||||
"frameworks": "arduino",
|
||||
"platforms": "espressif32",
|
||||
"headers": "LvScreenshot.hpp",
|
||||
"license": "MIT",
|
||||
"build": { "srcDir": "src" }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
name=LvScreenshot
|
||||
version=1.0.0
|
||||
author=xPablo.cz
|
||||
maintainer=xPablo.cz <https://git.xpablo.cz/xPablo.cz>
|
||||
sentence=Board-independent LVGL 9 screen capture with an optional /screenshot web endpoint.
|
||||
paragraph=lv_snapshot_take -> tight RGB565, an optional AsyncWebServer GET /screenshot endpoint, and a PC tool that saves a PNG.
|
||||
category=Display
|
||||
url=https://git.xpablo.cz/xPablo.cz/LvScreenshot
|
||||
architectures=esp32
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "LvScreenshot.hpp"
|
||||
#include <cstring>
|
||||
|
||||
// Prefer PSRAM on ESP32 (a full-frame RGB565 buffer is ~300 KB and won't fit in internal RAM); fall
|
||||
// back to standard malloc on other platforms so the core stays portable.
|
||||
#if defined(ESP_PLATFORM)
|
||||
#include <esp_heap_caps.h>
|
||||
static void * ssMalloc(size_t n) {
|
||||
void * p = heap_caps_malloc(n, MALLOC_CAP_SPIRAM);
|
||||
return p ? p : heap_caps_malloc(n, MALLOC_CAP_8BIT);
|
||||
}
|
||||
static void ssFree(void * p) { heap_caps_free(p); }
|
||||
#else
|
||||
#include <cstdlib>
|
||||
static void * ssMalloc(size_t n) { return malloc(n); }
|
||||
static void ssFree(void * p) { free(p); }
|
||||
#endif
|
||||
|
||||
bool lvScreenshotCapture(lv_obj_t * obj, LvScreenshot * out) {
|
||||
if (out) { out->data = nullptr; out->width = 0; out->height = 0; out->length = 0; }
|
||||
if (!obj || !out) return false;
|
||||
#if LV_USE_SNAPSHOT
|
||||
// Render into a PSRAM-backed draw buffer supplied by us, rather than lv_snapshot_take()'s own
|
||||
// allocation: LVGL's builtin malloc pool (LV_STDLIB_BUILTIN) is far too small for a full-frame
|
||||
// (~300 KB) buffer, so lv_snapshot_take() would fail. lv_snapshot_take_to_draw_buf() reshapes and
|
||||
// fills a buffer we own. Size it exactly as LVGL does (obj size; +slack rows to cover any
|
||||
// ext_draw_size), same stride formula LVGL will pick for LV_STRIDE_AUTO.
|
||||
const int32_t ow = lv_obj_get_width(obj);
|
||||
const int32_t oh = lv_obj_get_height(obj);
|
||||
if (ow <= 0 || oh <= 0) return false;
|
||||
const uint32_t stride = lv_draw_buf_width_to_stride((uint32_t) ow, LV_COLOR_FORMAT_RGB565);
|
||||
const uint32_t hAlloc = (uint32_t) oh + 16; // slack so a small ext_draw_size still fits
|
||||
const size_t dataSz = (size_t) stride * hAlloc;
|
||||
|
||||
uint8_t * raw = static_cast<uint8_t *>(ssMalloc(dataSz + LV_DRAW_BUF_ALIGN));
|
||||
if (!raw) return false;
|
||||
void * data = lv_draw_buf_align(raw, LV_COLOR_FORMAT_RGB565);
|
||||
|
||||
lv_draw_buf_t db;
|
||||
if (lv_draw_buf_init(&db, (uint32_t) ow, (uint32_t) oh, LV_COLOR_FORMAT_RGB565, stride,
|
||||
data, dataSz) != LV_RESULT_OK) { ssFree(raw); return false; }
|
||||
if (lv_snapshot_take_to_draw_buf(obj, LV_COLOR_FORMAT_RGB565, &db) != LV_RESULT_OK) {
|
||||
ssFree(raw); return false;
|
||||
}
|
||||
|
||||
const uint32_t w = db.header.w; // reshape may have grown these by ext_draw_size
|
||||
const uint32_t h = db.header.h;
|
||||
const uint32_t rstride = db.header.stride;
|
||||
const size_t len = (size_t) w * h * 2;
|
||||
|
||||
uint8_t * outbuf = static_cast<uint8_t *>(ssMalloc(len));
|
||||
if (!outbuf) { ssFree(raw); return false; }
|
||||
const uint8_t * src = static_cast<const uint8_t *>(db.data);
|
||||
for (uint32_t y = 0; y < h; ++y) // tight-pack: drop any stride padding
|
||||
std::memcpy(outbuf + (size_t) y * w * 2, src + (size_t) y * rstride, (size_t) w * 2);
|
||||
|
||||
ssFree(raw);
|
||||
out->data = outbuf; out->width = w; out->height = h; out->length = len;
|
||||
return true;
|
||||
#else
|
||||
#warning "LvScreenshot: LV_USE_SNAPSHOT is 0 -- capture always fails. Enable LV_USE_SNAPSHOT in lv_conf.h."
|
||||
(void) obj;
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool lvScreenshotCaptureTop(LvScreenshot * out) {
|
||||
lv_obj_t * top = lv_layer_top();
|
||||
lv_obj_t * obj = (top && lv_obj_get_child_count(top) > 0) ? top : lv_screen_active();
|
||||
return lvScreenshotCapture(obj, out);
|
||||
}
|
||||
|
||||
void lvScreenshotFree(LvScreenshot * shot) {
|
||||
if (!shot) return;
|
||||
if (shot->data) { ssFree(shot->data); shot->data = nullptr; }
|
||||
shot->width = 0; shot->height = 0; shot->length = 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @file LvScreenshot.hpp
|
||||
* @brief Board-independent LVGL screen capture. Renders any lv_obj (typically the active screen)
|
||||
* into a tightly-packed RGB565 buffer via lv_snapshot_take(). Works with ANY LVGL 9 display
|
||||
* driver -- partial-flush, full-frame, RGB panel -- because it re-renders the object tree
|
||||
* rather than reading a panel framebuffer. Requires LV_USE_SNAPSHOT=1 in your lv_conf.h.
|
||||
*/
|
||||
#pragma once
|
||||
#include <lvgl.h>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
/** A captured image: tightly-packed RGB565 (2 bytes/pixel, row-major, no stride padding). */
|
||||
struct LvScreenshot {
|
||||
uint8_t * data; ///< RGB565 pixels; nullptr if capture failed. Free with lvScreenshotFree().
|
||||
uint32_t width; ///< pixels
|
||||
uint32_t height; ///< pixels
|
||||
size_t length; ///< bytes (== width * height * 2)
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Capture @p obj and its subtree as tight RGB565 into @p out.
|
||||
* @warning The caller MUST hold the LVGL lock (LVGL is single-threaded) for the whole call.
|
||||
* @return true on success (then free @p out->data with lvScreenshotFree); false on failure (@p out
|
||||
* is zeroed). Fails and warns at compile time if LV_USE_SNAPSHOT is disabled.
|
||||
*/
|
||||
bool lvScreenshotCapture(lv_obj_t * obj, LvScreenshot * out);
|
||||
|
||||
/**
|
||||
* @brief Capture whatever is visible on top: lv_layer_top() when it has children (an active overlay),
|
||||
* otherwise lv_screen_active(). The caller MUST hold the LVGL lock.
|
||||
*/
|
||||
bool lvScreenshotCaptureTop(LvScreenshot * out);
|
||||
|
||||
/** @brief Free a buffer from a successful capture and zero the struct. Safe on a zeroed/failed one. */
|
||||
void lvScreenshotFree(LvScreenshot * shot);
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "LvScreenshotHttp.hpp"
|
||||
|
||||
// Self-disable if the project has no ESPAsyncWebServer, so the core library stays dependency-free.
|
||||
#if defined(__has_include)
|
||||
# if __has_include(<ESPAsyncWebServer.h>)
|
||||
# define LVSS_HAVE_ASYNC_WEB 1
|
||||
# endif
|
||||
#endif
|
||||
|
||||
#if defined(LVSS_HAVE_ASYNC_WEB)
|
||||
#include <ESPAsyncWebServer.h>
|
||||
#include "LvScreenshot.hpp"
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
// One shared capture buffer: the async response streams from it over ~1 s, so it must outlive the
|
||||
// handler. It is freed and re-taken on the next request. This assumes a single sequential client
|
||||
// (the intended tools/screenshot.py workflow); two truly concurrent requests could tear the image
|
||||
// -- just retry. A busy flag was intentionally omitted (an aborted transfer would wedge it).
|
||||
LvScreenshot g_shot = {nullptr, 0, 0, 0};
|
||||
LvScreenshotLockFn g_lock = nullptr;
|
||||
LvScreenshotUnlockFn g_unlock = nullptr;
|
||||
}
|
||||
|
||||
void lvScreenshotAttachHttp(AsyncWebServer * www, LvScreenshotLockFn lock,
|
||||
LvScreenshotUnlockFn unlock, const char * path) {
|
||||
if (!www) return;
|
||||
g_lock = lock;
|
||||
g_unlock = unlock;
|
||||
www->on(path ? path : "/screenshot", HTTP_GET, [](AsyncWebServerRequest * req) {
|
||||
lvScreenshotFree(&g_shot); // release any previous capture
|
||||
|
||||
bool ok;
|
||||
if (g_lock && g_lock(-1)) { // capture under the LVGL lock (not mid-flush)
|
||||
ok = lvScreenshotCaptureTop(&g_shot);
|
||||
if (g_unlock) g_unlock();
|
||||
} else {
|
||||
ok = lvScreenshotCaptureTop(&g_shot); // no lock hook provided: best-effort
|
||||
}
|
||||
if (!ok || !g_shot.data) {
|
||||
req->send(500, "text/plain", "screenshot: capture failed");
|
||||
return;
|
||||
}
|
||||
|
||||
// Stream the buffer in chunks; g_shot stays valid until the next request.
|
||||
AsyncWebServerResponse * resp = req->beginResponse(
|
||||
"application/octet-stream", g_shot.length,
|
||||
[](uint8_t * out, size_t maxLen, size_t index) -> size_t {
|
||||
const size_t remaining = g_shot.length - index;
|
||||
const size_t n = remaining < maxLen ? remaining : maxLen;
|
||||
std::memcpy(out, g_shot.data + index, n);
|
||||
return n;
|
||||
});
|
||||
resp->addHeader("X-Width", String(g_shot.width));
|
||||
resp->addHeader("X-Height", String(g_shot.height));
|
||||
resp->addHeader("X-Format", "RGB565");
|
||||
req->send(resp);
|
||||
});
|
||||
}
|
||||
#else
|
||||
// ESPAsyncWebServer not available in this project: no-op so the core LvScreenshot still builds.
|
||||
void lvScreenshotAttachHttp(AsyncWebServer *, LvScreenshotLockFn, LvScreenshotUnlockFn, const char *) {}
|
||||
#endif
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @file LvScreenshotHttp.hpp
|
||||
* @brief OPTIONAL AsyncWebServer glue for LvScreenshot: registers a GET route that returns the
|
||||
* current LVGL screen as raw RGB565 with X-Width/X-Height/X-Format headers. Kept separate from
|
||||
* the core so a project that does not use ESPAsyncWebServer can ignore it; the implementation
|
||||
* compiles to a no-op when <ESPAsyncWebServer.h> is unavailable.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
class AsyncWebServer;
|
||||
|
||||
/** LVGL-lock hooks (e.g. lvgl_port_lock / lvgl_port_unlock); the capture runs while locked. */
|
||||
typedef bool (*LvScreenshotLockFn)(int timeout_ms);
|
||||
typedef bool (*LvScreenshotUnlockFn)(void);
|
||||
|
||||
/**
|
||||
* @brief Register @p path (default "/screenshot") on @p www: on GET, capture the topmost LVGL layer
|
||||
* and stream it as raw RGB565 with X-Width/X-Height/X-Format headers.
|
||||
* @param www the AsyncWebServer to attach to.
|
||||
* @param lock LVGL-lock acquire hook (held across the capture); may be nullptr for best-effort.
|
||||
* @param unlock LVGL-lock release hook; may be nullptr.
|
||||
* @param path route path (default "/screenshot").
|
||||
*
|
||||
* Pair with tools/screenshot.py to save a PNG. No-op if ESPAsyncWebServer is not available.
|
||||
*/
|
||||
void lvScreenshotAttachHttp(AsyncWebServer * www, LvScreenshotLockFn lock,
|
||||
LvScreenshotUnlockFn unlock, const char * path = "/screenshot");
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env -S uv run --script
|
||||
# /// script
|
||||
# requires-python = ">=3.9"
|
||||
# dependencies = [
|
||||
# "requests",
|
||||
# "numpy",
|
||||
# "pillow",
|
||||
# ]
|
||||
# ///
|
||||
"""Fetch an LVGL device's screen via the LvScreenshot HTTP endpoint and save it as a PNG.
|
||||
|
||||
The firmware must expose the endpoint (register it with lvScreenshotAttachHttp, e.g. behind a
|
||||
build flag). The device returns the raw RGB565 framebuffer plus X-Width / X-Height / X-Format
|
||||
headers; this script converts it to RGB888 and writes a PNG.
|
||||
|
||||
Dependencies are declared inline (PEP 723) and installed automatically by uv -- no manual setup.
|
||||
|
||||
Usage (via uv):
|
||||
uv run screenshot.py [--host diystickynotes.local] [--out shot.png] [--swap-rb] [--byteswap]
|
||||
# the shebang also lets you run it directly: ./screenshot.py ...
|
||||
|
||||
--swap-rb : swap red/blue if the panel uses BGR order (colours look inverted).
|
||||
--byteswap : byte-swap each 16-bit pixel if the panel stores RGB565 big-endian (garbled image).
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""Fetch the screen, convert RGB565 -> RGB888, and write a PNG. Returns a process exit code."""
|
||||
ap = argparse.ArgumentParser(description="Capture an LVGL device screen as a PNG.")
|
||||
ap.add_argument("--host", default="diystickynotes.local", help="device hostname or IP")
|
||||
ap.add_argument("--out", default="shot.png", help="output PNG path")
|
||||
ap.add_argument("--path", default="/screenshot", help="endpoint path")
|
||||
ap.add_argument("--swap-rb", action="store_true", help="swap R/B (BGR panel)")
|
||||
ap.add_argument("--byteswap", action="store_true", help="byte-swap 16-bit pixels")
|
||||
ap.add_argument("--timeout", type=float, default=15.0, help="HTTP timeout seconds")
|
||||
args = ap.parse_args()
|
||||
|
||||
url = f"http://{args.host}{args.path}"
|
||||
try:
|
||||
r = requests.get(url, timeout=args.timeout)
|
||||
except requests.RequestException as e:
|
||||
print(f"error: request to {url} failed: {e}", file=sys.stderr)
|
||||
return 1
|
||||
if r.status_code != 200:
|
||||
print(f"error: HTTP {r.status_code}: {r.text[:200]}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
try:
|
||||
w = int(r.headers["X-Width"])
|
||||
h = int(r.headers["X-Height"])
|
||||
fmt = r.headers.get("X-Format", "RGB565")
|
||||
except (KeyError, ValueError) as e:
|
||||
print(f"error: missing/invalid dimension headers: {e}", file=sys.stderr)
|
||||
return 1
|
||||
if fmt != "RGB565":
|
||||
print(f"error: unsupported X-Format '{fmt}' (expected RGB565)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
data = r.content
|
||||
expect = w * h * 2
|
||||
if len(data) != expect:
|
||||
print(f"error: body is {len(data)} bytes, expected {expect} ({w}x{h} RGB565)", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
dtype = ">u2" if args.byteswap else "<u2"
|
||||
px = np.frombuffer(data, dtype=dtype).reshape((h, w)).astype(np.uint32)
|
||||
r5 = (px >> 11) & 0x1F
|
||||
g6 = (px >> 5) & 0x3F
|
||||
b5 = px & 0x1F
|
||||
# Expand to 8-bit with bit replication so full white maps to 255.
|
||||
r8 = ((r5 << 3) | (r5 >> 2)).astype(np.uint8)
|
||||
g8 = ((g6 << 2) | (g6 >> 4)).astype(np.uint8)
|
||||
b8 = ((b5 << 3) | (b5 >> 2)).astype(np.uint8)
|
||||
if args.swap_rb:
|
||||
r8, b8 = b8, r8
|
||||
rgb = np.dstack([r8, g8, b8])
|
||||
Image.fromarray(rgb, "RGB").save(args.out)
|
||||
print(f"saved {args.out} ({w}x{h})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user