Skip to main content

RawImage

Displays pixel frames pushed from Python at full bandwidth.

Unlike Image, whose src travels through the regular Flet protocol on every update, RawImage streams frames over a dedicated DataChannel: bytes skip MsgPack encode/decode entirely and, on local transports (desktop app, flet run, Pyodide), are displayed from raw RGBA pixels without any image encoding or decoding. This makes it suitable for animations, generated graphics, camera frames and Pillow output at interactive frame rates.

Frames are pushed with the awaitable render, render_rgba and render_encoded methods. Each call resolves when the client has actually displayed the frame, so a plain loop self-paces to display speed:

raw_image = ft.RawImage(width=400, height=300)
page.add(raw_image)

while True:
await raw_image.render(produce_pil_image())

Transport behavior is selected automatically:

  • Local transports (local_data_transport connections): frames are sent as uncompressed premultiplied RGBA8888 and uploaded straight to a GPU texture on the client.
  • Remote transports (flet-web over WebSocket): Pillow and NumPy frames are PNG-encoded off the event loop to save bandwidth; raw pixel frames for which no straight-alpha source is available are sent uncompressed.

Premultiplied alpha. The raw-pixel path uploads frames directly as GPU textures in Flutter's rgba8888 format, which expects RGB values already multiplied by alpha ("premultiplied"). Pillow, numpy code and image files normally produce straight alpha instead, where color and opacity are independent: a half-transparent white pixel is (255, 255, 255, 128) straight but (128, 128, 128, 128) premultiplied (each color channel becomes value * alpha // 255). The premultiplied argument of the render methods says whether that conversion is already done:

  • premultiplied=False — pixels carry straight alpha; RGB is multiplied by alpha before sending. The conversion runs in fast C loops and is skipped when the frame turns out to be fully opaque.
  • premultiplied=True — skip the conversion. Use it when pixels are genuinely premultiplied or, the common case, when the frame is fully opaque: with every alpha at 255 both forms are identical, and declaring it saves an alpha scan per frame in streaming loops.

Passing straight-alpha pixels with premultiplied=True makes semi-transparent areas render too bright; for fully opaque frames the flag cannot be wrong either way.

The last frame is retained and replayed automatically when the client widget remounts (page rebuild, route navigation), mirroring how Image.src persists.

RawImage
RawImage

Inherits: LayoutControl

Properties

  • ack_timeout - Seconds a render call waits for the client's frame-applied acknowledgment before raising TimeoutError.
  • filter_quality - The rendering quality of the displayed frame.
  • fit - Defines how to inscribe the current frame into the space allocated during layout.
  • ready_timeout - Seconds the render methods wait for the client widget to attach its data channel before raising TimeoutError.
  • scale - How many physical frame pixels correspond to one logical pixel.

Events

Methods

  • clear - Clear the displayed frame.
  • render - Display a Pillow image or a NumPy-style array and wait until the client has shown it.
  • render_encoded - Display an encoded image (PNG, JPEG, WebP, ...) and wait until the client has shown it.
  • render_rgba - Display a raw RGBA8888 frame and wait until the client has shown it.

RawImage vs Image

Use Image for pictures that come from a file, URL, asset or a one-off byte string. Use RawImage when your Python code produces pixels — Pillow drawings, NumPy arrays, camera frames, plots, procedural animations — and you want to push them to the screen repeatedly and fast.

Every update of Image.src travels through the regular Flet protocol and is decoded from scratch on the client. RawImage instead streams frames over a dedicated data channel: bytes skip the protocol entirely and, when the client runs on the same machine (desktop app, flet run, Pyodide), frames are transferred as raw RGBA pixels and uploaded straight to a GPU texture — no image encoding or decoding on either side. Remote web clients automatically receive compact PNG frames instead.

The render methods are awaitable and resolve when the client has displayed the frame, so a plain loop self-paces to display speed:

raw_image = ft.RawImage(expand=True)
page.add(raw_image)

while True:
await raw_image.render(produce_pil_image())

Examples

Photo viewer

Regular PNG/JPEG/WebP bytes — downloaded, read from a file or pulled from a database — are displayed with render_encoded; the client decodes them with its image codecs.

Photo viewer

play_arrowTry Online
import asyncio
import urllib.request

import flet as ft

PHOTOS = [
f"https://picsum.photos/seed/{seed}/800/500.jpg"
for seed in ("flet", "raw", "image", "viewer", "photo")
]


async def main(page: ft.Page):
page.title = "RawImage photo viewer"

raw_image = ft.RawImage(expand=True, fit=ft.BoxFit.CONTAIN)
status_text = ft.Text("", size=12)

index = 0
cache: dict[str, bytes] = {}

async def fetch(url: str) -> bytes:
if url not in cache:
cache[url] = await asyncio.to_thread(
lambda: urllib.request.urlopen(url, timeout=20).read()
)
return cache[url]

async def show(i: int):
nonlocal index
index = i % len(PHOTOS)
status_text.value = f"loading {index + 1}/{len(PHOTOS)}…"
page.update()
data = await fetch(PHOTOS[index])
# render_encoded displays PNG/JPEG/WebP bytes from anywhere: an
# HTTP response like here, a file (Path("photo.jpg").read_bytes()),
# a database blob. The client decodes them with its image codecs.
await raw_image.render_encoded(data)
status_text.value = f"{index + 1}/{len(PHOTOS)}{len(data) // 1024} KB JPEG"
page.update()

def go(delta: int):
asyncio.create_task(show(index + delta))

page.add(
ft.Row(
[
ft.IconButton(ft.Icons.CHEVRON_LEFT, on_click=lambda: go(-1)),
ft.IconButton(ft.Icons.CHEVRON_RIGHT, on_click=lambda: go(1)),
status_text,
],
spacing=10,
),
raw_image,
)
await show(0)


if __name__ == "__main__":
ft.run(main)

Plasma animation

Streams a procedurally generated plasma effect with a live FPS counter and a render-resolution slider.

Plasma animation

play_arrowTry Online
import asyncio
import time
from collections import deque

import numpy as np

import flet as ft

FPS_WINDOW_SECONDS = 2.0


def plasma_frame(width: int, height: int, t: float) -> np.ndarray:
"""
Renders a classic demoscene plasma: several drifting sine fields summed
and mapped through a rotating RGB palette. Fully vectorized in numpy.
"""
x = np.linspace(0.0, 3.0 * np.pi, width, dtype=np.float32)[None, :]
y = np.linspace(0.0, 3.0 * np.pi, height, dtype=np.float32)[:, None]
cx = x - 1.5 * np.pi + np.sin(t / 3.0) * np.pi
cy = y - 1.5 * np.pi + np.cos(t / 2.0) * np.pi
v = (
np.sin(x + t)
+ np.sin((y + t) / 2.0)
+ np.sin((x + y + t) / 2.0)
+ np.sin(np.sqrt(cx * cx + cy * cy + 1.0) + t)
) / 4.0
frame = np.empty((height, width, 4), dtype=np.uint8)
frame[..., 0] = np.sin(v * np.pi) * 127 + 128
frame[..., 1] = np.sin(v * np.pi + 2.0 * np.pi / 3.0) * 127 + 128
frame[..., 2] = np.sin(v * np.pi + 4.0 * np.pi / 3.0) * 127 + 128
frame[..., 3] = 255
return frame


async def main(page: ft.Page):
page.title = "RawImage plasma"
page.padding = 0

raw_image = ft.RawImage(expand=True, fit=ft.BoxFit.FILL)

fps_text = ft.Text("fps: —", size=12)
resolution_text = ft.Text("res: —", size=12)
detail_slider = ft.Slider(
min=1,
max=8,
divisions=7,
value=2,
width=200,
label="downscale 1/{value}",
)

status_bar = ft.Container(
content=ft.Row(
[fps_text, resolution_text, ft.Text("detail:", size=12), detail_slider],
spacing=20,
),
padding=ft.Padding.symmetric(horizontal=12, vertical=2),
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGH,
)

page.add(
ft.SafeArea(
content=ft.Column([raw_image, status_bar], expand=True, spacing=0),
expand=True,
)
)

frame_times: deque[float] = deque()
frame_size = (0, 0)

async def animate():
nonlocal frame_size
started = time.monotonic()
while True:
downscale = int(detail_slider.value)
width = max(2, int(page.width or 800) // downscale)
height = max(2, (int(page.height or 600) - 50) // downscale)
frame_size = (width, height)
frame = plasma_frame(width, height, time.monotonic() - started)
try:
# The frame is opaque, so it is premultiplied by definition —
# saying so skips an alpha scan per frame. The await resolves
# when the frame is on screen, pacing the loop to display
# speed.
await raw_image.render(frame, premultiplied=True)
except (RuntimeError, TimeoutError):
return # window closed — session destroyed
now = time.monotonic()
frame_times.append(now)
while frame_times and frame_times[0] < now - FPS_WINDOW_SECONDS:
frame_times.popleft()

async def refresh_stats():
# Refresh labels at ~4 Hz instead of once per frame, which would
# thrash the layout.
while True:
now = time.monotonic()
while frame_times and frame_times[0] < now - FPS_WINDOW_SECONDS:
frame_times.popleft()
fps_text.value = f"fps: {len(frame_times) / FPS_WINDOW_SECONDS:.1f}"
resolution_text.value = f"res: {frame_size[0]}x{frame_size[1]}"
try:
page.update()
except RuntimeError:
return
await asyncio.sleep(0.25)

asyncio.create_task(animate())
asyncio.create_task(refresh_stats())


if __name__ == "__main__":
ft.run(main)

Pillow paint

An interactive paint app: pan gestures draw brush strokes onto a Pillow image that is streamed to the screen through a dirty-flag render loop.

Pillow paint

play_arrowTry Online
import asyncio

from PIL import Image, ImageDraw

import flet as ft

CANVAS_WIDTH = 700
CANVAS_HEIGHT = 450
# ImageDraw has no anti-aliasing: lines and ellipses get hard, jagged
# edges. Painting on a canvas this many times larger and letting the
# client downscale it into the control's box averages each screen pixel
# from SUPERSAMPLE^2 canvas pixels — cheap, GPU-side anti-aliasing.
SUPERSAMPLE = 2
PALETTE = [
"#1d1d21",
"#e53935",
"#fb8c00",
"#fdd835",
"#43a047",
"#1e88e5",
"#8e24aa",
]


async def main(page: ft.Page):
page.title = "RawImage paint"

image = Image.new(
"RGBA", (CANVAS_WIDTH * SUPERSAMPLE, CANVAS_HEIGHT * SUPERSAMPLE), "white"
)
draw = ImageDraw.Draw(image)

# Frames arrive at SUPERSAMPLE times the control's size; fit=FILL
# scales them down and filter_quality blends the extra pixels away.
raw_image = ft.RawImage(
width=CANVAS_WIDTH,
height=CANVAS_HEIGHT,
fit=ft.BoxFit.FILL,
filter_quality=ft.FilterQuality.MEDIUM,
)

brush_color = PALETTE[0]
last_point: tuple[float, float] | None = None # in canvas pixels

# Pan events arrive faster than frames can be displayed, so handlers
# only mutate the Pillow image and flag it dirty; a single loop below
# streams the latest state as fast as the display confirms frames.
dirty = asyncio.Event()

def stroke_to(point: ft.Offset):
nonlocal last_point
# Pointer coordinates are in control (logical) pixels; the canvas
# lives at SUPERSAMPLE resolution.
x = point.x * SUPERSAMPLE
y = point.y * SUPERSAMPLE
radius = brush_size_slider.value / 2 * SUPERSAMPLE
if last_point is not None:
draw.line(
(last_point[0], last_point[1], x, y),
fill=brush_color,
width=int(radius * 2),
)
# Round cap: a line alone leaves flat, jagged joints.
draw.ellipse(
(x - radius, y - radius, x + radius, y + radius),
fill=brush_color,
)
last_point = (x, y)
dirty.set()

def pan_start(e: ft.DragStartEvent):
nonlocal last_point
last_point = None
stroke_to(e.local_position)

def pan_update(e: ft.DragUpdateEvent):
stroke_to(e.local_position)

def select_color(color: str):
nonlocal brush_color
brush_color = color
for dot in palette_row.controls:
dot.border = (
ft.Border.all(3, ft.Colors.BLUE_GREY_200) if dot.data == color else None
)
palette_row.update()

def clear_canvas():
draw.rectangle(
(0, 0, CANVAS_WIDTH * SUPERSAMPLE, CANVAS_HEIGHT * SUPERSAMPLE),
fill="white",
)
dirty.set()

palette_row = ft.Row(
[
ft.Container(
width=28,
height=28,
border_radius=14,
bgcolor=color,
data=color,
on_click=lambda e: select_color(e.control.data),
)
for color in PALETTE
],
spacing=8,
)
brush_size_slider = ft.Slider(min=2, max=40, value=8, width=150)

page.add(
ft.Row(
[
palette_row,
ft.Text("size:"),
brush_size_slider,
ft.OutlinedButton("Clear", on_click=clear_canvas),
],
spacing=10,
),
ft.Container(
content=ft.GestureDetector(
content=raw_image,
on_pan_start=pan_start,
on_pan_update=pan_update,
drag_interval=10,
),
border=ft.Border.all(1, ft.Colors.BLUE_GREY_200),
width=CANVAS_WIDTH,
height=CANVAS_HEIGHT,
),
)
select_color(brush_color)

async def render_loop():
while True:
await dirty.wait()
dirty.clear()
try:
# The canvas is opaque, so it is premultiplied by definition.
# Awaiting the render paces this loop to display speed: any
# strokes drawn meanwhile coalesce into the next frame.
await raw_image.render(image, premultiplied=True)
except (RuntimeError, TimeoutError):
return # window closed — session destroyed

dirty.set() # show the blank canvas
asyncio.create_task(render_loop())


if __name__ == "__main__":
ft.run(main)

Mandelbrot explorer

Click to zoom into the Mandelbrot set — every zoom is a burst of NumPy-rendered frames computed in a background thread.

Mandelbrot explorer

play_arrowTry Online
import asyncio
import time
from collections import deque

import numpy as np

import flet as ft

WIDTH = 640
HEIGHT = 480
MAX_ITER = 96
ZOOM_PER_CLICK = 8.0
ZOOM_FRAMES = 30
HOME = (-0.65, 0.0, 1.6) # center x, center y, half-width of the view
FPS_WINDOW_SECONDS = 2.0


def mandelbrot_frame(center_x: float, center_y: float, scale: float) -> np.ndarray:
"""
Escape-time Mandelbrot render, fully vectorized: points that stay
bounded for MAX_ITER iterations are painted black, escaped points get
a sine palette keyed on the escape iteration.
"""
aspect = HEIGHT / WIDTH
x = np.linspace(center_x - scale, center_x + scale, WIDTH)
y = np.linspace(center_y - scale * aspect, center_y + scale * aspect, HEIGHT)
c = x[None, :] + 1j * y[:, None]
z = np.zeros_like(c)
escape_iter = np.full(c.shape, MAX_ITER, dtype=np.int32)
alive = np.ones(c.shape, dtype=bool)
for i in range(MAX_ITER):
z[alive] = z[alive] ** 2 + c[alive]
escaped = alive & (np.abs(z) > 2.0)
escape_iter[escaped] = i
alive &= ~escaped
t = escape_iter / MAX_ITER
frame = np.zeros((HEIGHT, WIDTH, 4), dtype=np.uint8)
outside = ~alive
frame[outside, 0] = np.sin(t[outside] * 9.0) * 127 + 128
frame[outside, 1] = np.sin(t[outside] * 9.0 + 1.2) * 127 + 128
frame[outside, 2] = np.sin(t[outside] * 9.0 + 2.4) * 127 + 128
frame[..., 3] = 255
return frame


async def main(page: ft.Page):
page.title = "RawImage Mandelbrot"

# No fixed size: the control fills the window (expand=True) and CONTAIN
# scales the 640x480 frame into whatever box the layout provides, so the
# image grows and shrinks with the window.
raw_image = ft.RawImage(expand=True, fit=ft.BoxFit.CONTAIN)
fps_text = ft.Text("fps: —", size=12)
zoom_text = ft.Text("zoom: 1x", size=12)
hint_text = ft.Text("click to zoom in", size=12, italic=True)

view = HOME
frame_times: deque[float] = deque()
zoom_task: asyncio.Task | None = None
box_size = (float(WIDTH), float(HEIGHT)) # laid-out size of the control

def track_size(e: ft.LayoutSizeChangeEvent):
nonlocal box_size
box_size = (e.width, e.height)

raw_image.on_size_change = track_size

async def show(center_x: float, center_y: float, scale: float):
nonlocal view
view = (center_x, center_y, scale)
# The render itself is CPU-heavy — keep it off the event loop.
frame = await asyncio.to_thread(mandelbrot_frame, center_x, center_y, scale)
await raw_image.render(frame, premultiplied=True)
now = time.monotonic()
frame_times.append(now)
while frame_times and frame_times[0] < now - FPS_WINDOW_SECONDS:
frame_times.popleft()
fps_text.value = f"fps: {len(frame_times) / FPS_WINDOW_SECONDS:.1f}"
zoom_text.value = f"zoom: {HOME[2] / scale:,.0f}x"
page.update()

async def zoom_to(target_x: float, target_y: float, factor: float):
# A burst of ZOOM_FRAMES awaited renders: exponential interpolation
# of the view towards the target, each frame displayed as soon as
# the client acks the previous one.
start_x, start_y, start_scale = view
for i in range(1, ZOOM_FRAMES + 1):
k = i / ZOOM_FRAMES
f = factor**k
scale = start_scale / f
x = target_x + (start_x - target_x) / f
y = target_y + (start_y - target_y) / f
try:
await show(x, y, scale)
except (RuntimeError, TimeoutError):
return

def start_zoom(target_x: float, target_y: float, factor: float):
nonlocal zoom_task
if zoom_task is not None and not zoom_task.done():
return # ignore clicks while a zoom animation is running
zoom_task = asyncio.create_task(zoom_to(target_x, target_y, factor))

def tap(e: ft.TapEvent):
if e.local_position is None:
return
# With CONTAIN the frame is centered in the box with letterbox
# bands; undo that mapping to get frame-pixel coordinates.
box_width, box_height = box_size
display_scale = min(box_width / WIDTH, box_height / HEIGHT)
offset_x = (box_width - WIDTH * display_scale) / 2
offset_y = (box_height - HEIGHT * display_scale) / 2
frame_x = (e.local_position.x - offset_x) / display_scale
frame_y = (e.local_position.y - offset_y) / display_scale
if not (0 <= frame_x < WIDTH and 0 <= frame_y < HEIGHT):
return # tap landed on a letterbox band
center_x, center_y, scale = view
aspect = HEIGHT / WIDTH
x = center_x + (frame_x / WIDTH * 2 - 1) * scale
y = center_y + (frame_y / HEIGHT * 2 - 1) * scale * aspect
start_zoom(x, y, ZOOM_PER_CLICK)

def reset():
nonlocal zoom_task
if zoom_task is not None:
zoom_task.cancel()
zoom_task = None
asyncio.create_task(show(*HOME))

page.add(
ft.Row(
[
ft.OutlinedButton("Reset", on_click=reset),
zoom_text,
fps_text,
hint_text,
],
spacing=20,
),
ft.GestureDetector(content=raw_image, on_tap_down=tap, expand=True),
)

await show(*HOME)


if __name__ == "__main__":
ft.run(main)

Game of Life

Conway's Game of Life on a tiny grid upscaled with crisp nearest-neighbor filtering; draw cells with the pointer while the simulation runs.

Game of Life

play_arrowTry Online
import asyncio
import time
from collections import deque

import numpy as np

import flet as ft

GRID_WIDTH = 160
GRID_HEIGHT = 100
CELL_SIZE = 5 # logical pixels per cell on screen
FPS_WINDOW_SECONDS = 2.0

ALIVE_COLOR = (80, 250, 123)
DEAD_COLOR = (30, 32, 44)


def step(world: np.ndarray) -> np.ndarray:
"""One Game of Life generation on a wrapping (toroidal) grid."""
neighbors = sum(
np.roll(np.roll(world, dy, axis=0), dx, axis=1)
for dy in (-1, 0, 1)
for dx in (-1, 0, 1)
if (dy, dx) != (0, 0)
)
return (neighbors == 3) | (world & (neighbors == 2))


def world_frame(world: np.ndarray) -> np.ndarray:
frame = np.empty((GRID_HEIGHT, GRID_WIDTH, 4), dtype=np.uint8)
frame[..., :3] = np.where(world[..., None], ALIVE_COLOR, DEAD_COLOR)
frame[..., 3] = 255
return frame


async def main(page: ft.Page):
page.title = "RawImage Game of Life"

rng = np.random.default_rng(42)
world = rng.random((GRID_HEIGHT, GRID_WIDTH)) < 0.2

# One frame pixel per cell: the client upscales with nearest-neighbor
# (filter_quality=NONE), so each cell stays a crisp square and frames
# remain tiny (grid-sized) regardless of the on-screen size.
raw_image = ft.RawImage(
width=GRID_WIDTH * CELL_SIZE,
height=GRID_HEIGHT * CELL_SIZE,
fit=ft.BoxFit.FILL,
filter_quality=ft.FilterQuality.NONE,
)

playing = True
dirty = asyncio.Event()
frame_times: deque[float] = deque()

def toggle_playing():
nonlocal playing
playing = not playing
play_button.icon = ft.Icons.PAUSE if playing else ft.Icons.PLAY_ARROW
play_button.update()
dirty.set()

def randomize():
nonlocal world
world = rng.random((GRID_HEIGHT, GRID_WIDTH)) < 0.2
dirty.set()

def clear():
nonlocal world
world = np.zeros((GRID_HEIGHT, GRID_WIDTH), dtype=bool)
dirty.set()

def paint_cell(position: ft.Offset):
# Draw with the pointer, also while the simulation is running.
x = int(position.x // CELL_SIZE)
y = int(position.y // CELL_SIZE)
if 0 <= x < GRID_WIDTH and 0 <= y < GRID_HEIGHT:
world[y, x] = True
dirty.set()

play_button = ft.IconButton(ft.Icons.PAUSE, on_click=toggle_playing)
speed_slider = ft.Slider(min=1, max=60, value=20, width=150)
fps_text = ft.Text("fps: —", size=12)

page.add(
ft.Row(
[
play_button,
ft.Text("speed:"),
speed_slider,
ft.OutlinedButton("Randomize", on_click=randomize),
ft.OutlinedButton("Clear", on_click=clear),
fps_text,
],
spacing=10,
),
ft.GestureDetector(
content=raw_image,
on_tap_down=lambda e: paint_cell(e.local_position),
on_pan_update=lambda e: paint_cell(e.local_position),
drag_interval=10,
),
)

async def run_loop():
nonlocal world
while True:
if playing:
world = step(world)
dirty.clear()
else:
# Paused: wait for pointer edits or the play button.
await dirty.wait()
dirty.clear()
try:
await raw_image.render(world_frame(world), premultiplied=True)
except (RuntimeError, TimeoutError):
return # window closed — session destroyed
now = time.monotonic()
frame_times.append(now)
while frame_times and frame_times[0] < now - FPS_WINDOW_SECONDS:
frame_times.popleft()
fps_text.value = f"fps: {len(frame_times) / FPS_WINDOW_SECONDS:.1f}"
try:
page.update()
except RuntimeError:
return
if playing:
await asyncio.sleep(1 / speed_slider.value)

asyncio.create_task(run_loop())


if __name__ == "__main__":
ft.run(main)

Properties

ack_timeoutclass-attributeinstance-attribute

ack_timeout: Number | None = 10.0

Seconds a render call waits for the client's frame-applied acknowledgment before raising TimeoutError.

The ack normally arrives within milliseconds of the frame being displayed. The timeout is a liveness guard: transports drop frames sent while the client is disconnected (closed browser tab, network loss), and without it a while True: await raw_image.render(...) loop would wait for the lost ack forever. Raise it when streaming very large frames to slow remote clients, or set to None to wait indefinitely.

filter_qualityclass-attributeinstance-attribute

filter_quality: FilterQuality = FilterQuality.LOW

The rendering quality of the displayed frame.

Defaults to LOW (bilinear), a good trade-off for streamed frames. Use NONE for crisp nearest-neighbor scaling of pixel-art-style content.

fitclass-attributeinstance-attribute

fit: BoxFit | None = None

Defines how to inscribe the current frame into the space allocated during layout.

ready_timeoutclass-attributeinstance-attribute

ready_timeout: Number | None = 5.0

Seconds the render methods wait for the client widget to attach its data channel before raising TimeoutError.

Set to None to wait indefinitely.

scaleclass-attributeinstance-attribute

scale: Number = 1.0

How many physical frame pixels correspond to one logical pixel.

Only affects the intrinsic size of this control when it is laid out without tight constraints: a 800x600 frame with scale=2 measures 400x300 logical pixels. Set it to the device pixel ratio when frames are rendered at physical resolution.

Events

on_data_channel_openclass-attributeinstance-attribute

on_data_channel_open: (
    EventHandler[DataChannelOpenEvent] | None
) = None

Framework hook — Dart fires this when it opens the data channel on mount. The default handler captures the channel used by the render methods; override only to do something extra at attach-time.

Methods

clearasync

clear() -> None

Clear the displayed frame.

renderasync

render(image: Any, premultiplied: bool = False) -> None

Display a Pillow image or a NumPy-style array and wait until the client has shown it.

Parameters:

  • image (Any) - A PIL.Image.Image (any mode; converted to RGBA as needed), or an object exposing __array_interface__ (e.g. a NumPy array) of shape (height, width, 4) or (height, width, 3) with uint8 values.
  • premultiplied (bool, default: False) - Set to True if RGB values are already premultiplied by alpha to skip the premultiplication step.

Raises:

  • TypeError - If image is neither a Pillow image nor an array-like object.
  • TimeoutError - If the client widget doesn't attach within ready_timeout.

render_encodedasync

render_encoded(data: bytes) -> None

Display an encoded image (PNG, JPEG, WebP, ...) and wait until the client has shown it.

The bytes still travel over the data channel (skipping the Flet protocol), but the client decodes them with its image codecs instead of a raw pixel upload.

Parameters:

  • data (bytes) - Encoded image bytes.

Raises:

  • TimeoutError - If the client widget doesn't attach within ready_timeout.

render_rgbaasync

render_rgba(
    width: int,
    height: int,
    pixels: bytes,
    premultiplied: bool = True,
) -> None

Display a raw RGBA8888 frame and wait until the client has shown it.

Parameters:

  • width (int) - Frame width in physical pixels.
  • height (int) - Frame height in physical pixels.
  • pixels (bytes) - Tightly packed RGBA8888 bytes, width * height * 4 long. RGB values must be premultiplied by alpha (fully opaque frames are premultiplied by definition).
  • premultiplied (bool, default: True) - Set to False if RGB values are straight (non-premultiplied); Pillow is then required to premultiply them.

Raises:

  • ValueError - If pixels has the wrong length.
  • TimeoutError - If the client widget doesn't attach within ready_timeout.