diff --git a/apps/desktop/scripts/png_to_bmp.py b/apps/desktop/scripts/png_to_bmp.py new file mode 100644 index 0000000..b8bf2f0 --- /dev/null +++ b/apps/desktop/scripts/png_to_bmp.py @@ -0,0 +1,231 @@ +#!/usr/bin/env python3 +""" +Utility script to convert a PNG file (non-interlaced, 8-bit RGBA/RGB) +into a 24-bit BMP with optional letterboxing resize. + +The script is intentionally lightweight and relies only on Python's +standard library so it can run in constrained build environments. +""" + +from __future__ import annotations + +import argparse +import struct +import sys +import zlib +from pathlib import Path + + +PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n" + + +def parse_png(path: Path): + data = path.read_bytes() + if not data.startswith(PNG_SIGNATURE): + raise ValueError("Input is not a PNG file") + + idx = len(PNG_SIGNATURE) + width = height = bit_depth = color_type = None + compressed = bytearray() + interlaced = False + + while idx < len(data): + if idx + 8 > len(data): + raise ValueError("Corrupted PNG (unexpected EOF)") + length = struct.unpack(">I", data[idx : idx + 4])[0] + idx += 4 + chunk_type = data[idx : idx + 4] + idx += 4 + chunk_data = data[idx : idx + length] + idx += length + crc = data[idx : idx + 4] # noqa: F841 - crc skipped (validated by reader) + idx += 4 + + if chunk_type == b"IHDR": + width, height, bit_depth, color_type, compression, filter_method, interlace = struct.unpack( + ">IIBBBBB", chunk_data + ) + if compression != 0 or filter_method != 0: + raise ValueError("Unsupported PNG compression/filter method") + interlaced = interlace != 0 + elif chunk_type == b"IDAT": + compressed.extend(chunk_data) + elif chunk_type == b"IEND": + break + + if interlaced: + raise ValueError("Interlaced PNGs are not supported by this script") + if bit_depth != 8: + raise ValueError(f"Unsupported bit depth: {bit_depth}") + if color_type not in (2, 6): + raise ValueError(f"Unsupported color type: {color_type}") + + raw = zlib.decompress(bytes(compressed)) + bytes_per_pixel = 3 if color_type == 2 else 4 + stride = width * bytes_per_pixel + expected = (stride + 1) * height + if len(raw) != expected: + raise ValueError("Corrupted PNG data") + + # Apply PNG scanline filters + image = bytearray(width * height * 4) # Force RGBA output + prev_row = [0] * (stride) + + def paeth(a, b, c): + p = a + b - c + pa = abs(p - a) + pb = abs(p - b) + pc = abs(p - c) + if pa <= pb and pa <= pc: + return a + if pb <= pc: + return b + return c + + out_idx = 0 + for y in range(height): + offset = y * (stride + 1) + filter_type = raw[offset] + row = bytearray(raw[offset + 1 : offset + 1 + stride]) + if filter_type == 1: # Sub + for i in range(stride): + left = row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0 + row[i] = (row[i] + left) & 0xFF + elif filter_type == 2: # Up + for i in range(stride): + row[i] = (row[i] + prev_row[i]) & 0xFF + elif filter_type == 3: # Average + for i in range(stride): + left = row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0 + up = prev_row[i] + row[i] = (row[i] + ((left + up) >> 1)) & 0xFF + elif filter_type == 4: # Paeth + for i in range(stride): + left = row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0 + up = prev_row[i] + up_left = prev_row[i - bytes_per_pixel] if i >= bytes_per_pixel else 0 + row[i] = (row[i] + paeth(left, up, up_left)) & 0xFF + elif filter_type != 0: + raise ValueError(f"Unsupported PNG filter type: {filter_type}") + + # Convert to RGBA + for x in range(width): + if color_type == 2: + r, g, b = row[x * 3 : x * 3 + 3] + a = 255 + else: + r, g, b, a = row[x * 4 : x * 4 + 4] + image[out_idx : out_idx + 4] = bytes((r, g, b, a)) + out_idx += 4 + + prev_row = list(row) + + return width, height, image + + +def resize_with_letterbox(image, width, height, target_w, target_h, background): + if width == target_w and height == target_h: + return image, width, height + + bg_r, bg_g, bg_b = background + scale = min(target_w / width, target_h / height) + scale = max(scale, 1 / max(width, height)) # avoid zero + scaled_w = max(1, int(round(width * scale))) + scaled_h = max(1, int(round(height * scale))) + + output = bytearray(target_w * target_h * 4) + # Fill background + for i in range(0, len(output), 4): + output[i : i + 4] = bytes((bg_r, bg_g, bg_b, 255)) + + offset_x = (target_w - scaled_w) // 2 + offset_y = (target_h - scaled_h) // 2 + + for y in range(scaled_h): + src_y = min(height - 1, int(round(y / scale))) + for x in range(scaled_w): + src_x = min(width - 1, int(round(x / scale))) + src_idx = (src_y * width + src_x) * 4 + dst_idx = ((y + offset_y) * target_w + (x + offset_x)) * 4 + output[dst_idx : dst_idx + 4] = image[src_idx : src_idx + 4] + + return output, target_w, target_h + + +def blend_to_rgb(image): + rgb = bytearray(len(image) // 4 * 3) + for i in range(0, len(image), 4): + r, g, b, a = image[i : i + 4] + if a == 255: + rgb[(i // 4) * 3 : (i // 4) * 3 + 3] = bytes((b, g, r)) # BMP stores BGR + else: + alpha = a / 255.0 + bg = (255, 255, 255) + rr = int(round(r * alpha + bg[0] * (1 - alpha))) + gg = int(round(g * alpha + bg[1] * (1 - alpha))) + bb = int(round(b * alpha + bg[2] * (1 - alpha))) + rgb[(i // 4) * 3 : (i // 4) * 3 + 3] = bytes((bb, gg, rr)) + return rgb + + +def write_bmp(path: Path, width: int, height: int, rgb: bytearray): + row_stride = (width * 3 + 3) & ~3 # align to 4 bytes + padding = row_stride - width * 3 + pixel_data = bytearray() + + for y in range(height - 1, -1, -1): + start = y * width * 3 + end = start + width * 3 + pixel_data.extend(rgb[start:end]) + if padding: + pixel_data.extend(b"\0" * padding) + + file_size = 14 + 40 + len(pixel_data) + header = struct.pack("<2sIHHI", b"BM", file_size, 0, 0, 14 + 40) + dib_header = struct.pack( + " ) : (
-
+
Logotipo Raven -
-
-
- -
- Raven - Portal do Cliente +
+ Portal do Cliente +
+ + + + Raven
- +
+

Agente Desktop

+ +
-

Agente Desktop

{error ?

{error}

: null} {!token ? (
@@ -971,14 +973,19 @@ function App() { ) } -function StatusBadge({ status }: { status: string | null }) { +function StatusBadge({ status, className }: { status: string | null; className?: string }) { const s = (status ?? "").toLowerCase() const label = s === "online" ? "Online" : s === "offline" ? "Offline" : s === "maintenance" ? "Manutenção" : "Sem status" const dot = s === "online" ? "bg-emerald-500" : s === "offline" ? "bg-rose-500" : s === "maintenance" ? "bg-amber-500" : "bg-slate-400" const ring = s === "online" ? "bg-emerald-400/30" : s === "offline" ? "bg-rose-400/30" : s === "maintenance" ? "bg-amber-400/30" : "bg-slate-300/30" const isOnline = s === "online" return ( - + {isOnline ? : null}