Improve desktop branding and NSIS assets

This commit is contained in:
Esdras Renan 2025-10-18 23:31:10 -03:00
parent 9439890488
commit 78030dbcdb
5 changed files with 253 additions and 12 deletions

View file

@ -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(
"<IIIHHIIIIII",
40, # header size
width,
height,
1, # planes
24, # bits per pixel
0, # compression
len(pixel_data),
2835, # horizontal resolution (px/m ~72dpi)
2835, # vertical resolution
0,
0,
)
path.write_bytes(header + dib_header + pixel_data)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("input", type=Path)
parser.add_argument("output", type=Path)
parser.add_argument("--width", type=int, help="Target width (px)")
parser.add_argument("--height", type=int, help="Target height (px)")
parser.add_argument(
"--background",
type=str,
default="FFFFFF",
help="Background hex color used for transparent pixels (default: FFFFFF)",
)
args = parser.parse_args()
try:
width, height, image = parse_png(args.input)
if args.width and args.height:
bg = tuple(int(args.background[i : i + 2], 16) for i in (0, 2, 4))
image, width, height = resize_with_letterbox(image, width, height, args.width, args.height, bg)
rgb = blend_to_rgb(image)
write_bmp(args.output, width, height, rgb)
except Exception as exc: # noqa: BLE001
print(f"Error: {exc}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

View file

@ -53,6 +53,9 @@
"nsis": { "nsis": {
"displayLanguageSelector": true, "displayLanguageSelector": true,
"installerIcon": "icons/icon.ico", "installerIcon": "icons/icon.ico",
"uninstallerIcon": "icons/icon.ico",
"headerImage": "icons/nsis-header.bmp",
"sidebarImage": "icons/nsis-sidebar.bmp",
"installMode": "perMachine", "installMode": "perMachine",
"languages": ["PortugueseBR"] "languages": ["PortugueseBR"]
} }

View file

@ -751,7 +751,7 @@ function App() {
<DeactivationScreen companyName={companyName} /> <DeactivationScreen companyName={companyName} />
) : ( ) : (
<div className="w-full max-w-[720px] rounded-2xl border border-slate-200 bg-white p-6 shadow-sm"> <div className="w-full max-w-[720px] rounded-2xl border border-slate-200 bg-white p-6 shadow-sm">
<div className="mb-4 flex justify-center"> <div className="mb-6 flex flex-col items-center gap-4 text-center">
<img <img
src={logoSrc} src={logoSrc}
alt="Logotipo Raven" alt="Logotipo Raven"
@ -764,18 +764,20 @@ function App() {
setLogoSrc(`${appUrl}/raven.png`) setLogoSrc(`${appUrl}/raven.png`)
}} }}
/> />
</div> <div className="flex flex-col items-center gap-2">
<div className="mb-2 flex items-center justify-between gap-3"> <span className="text-xs uppercase tracking-[0.18em] text-neutral-500">Portal do Cliente</span>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span className="flex size-8 items-center justify-center rounded-lg bg-neutral-900 text-white"><GalleryVerticalEnd className="size-4" /></span> <span className="flex size-10 items-center justify-center rounded-xl bg-neutral-900 text-white shadow-sm">
<div className="flex flex-col leading-tight"> <GalleryVerticalEnd className="size-5" />
<span className="text-base font-semibold text-neutral-900">Raven</span> </span>
<span className="text-xs font-medium text-neutral-500">Portal do Cliente</span> <span className="text-2xl font-semibold text-neutral-900">Raven</span>
</div> </div>
</div> </div>
<StatusBadge status={status} /> <div className="flex flex-wrap items-center justify-center gap-3">
<h1 className="text-lg font-semibold text-slate-800">Agente Desktop</h1>
<StatusBadge status={status} />
</div>
</div> </div>
<h1 className="text-base font-semibold text-slate-800">Agente Desktop</h1>
{error ? <p className="mt-3 rounded-md bg-rose-50 p-2 text-sm text-rose-700">{error}</p> : null} {error ? <p className="mt-3 rounded-md bg-rose-50 p-2 text-sm text-rose-700">{error}</p> : null}
{!token ? ( {!token ? (
<div className="mt-4 space-y-3"> <div className="mt-4 space-y-3">
@ -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 s = (status ?? "").toLowerCase()
const label = s === "online" ? "Online" : s === "offline" ? "Offline" : s === "maintenance" ? "Manutenção" : "Sem status" 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 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 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" const isOnline = s === "online"
return ( return (
<span className="inline-flex h-8 items-center gap-4 rounded-full border border-slate-200 px-3 text-sm font-semibold"> <span
className={cn(
"inline-flex h-8 items-center gap-3 rounded-full border border-slate-200 bg-white/80 px-3 text-sm font-medium text-neutral-600 shadow-sm",
className
)}
>
<span className="relative inline-flex items-center"> <span className="relative inline-flex items-center">
<span className={cn("size-2 rounded-full", dot)} /> <span className={cn("size-2 rounded-full", dot)} />
{isOnline ? <span className={cn("absolute left-1/2 top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full animate-ping [animation-duration:2s]", ring)} /> : null} {isOnline ? <span className={cn("absolute left-1/2 top-1/2 size-4 -translate-x-1/2 -translate-y-1/2 rounded-full animate-ping [animation-duration:2s]", ring)} /> : null}