2025, Nov 27 03:01
Как вывести PNG 16×16 в окне X11 на Python без X.Image
Показываем, как отрисовать PNG 16×16 в окне X11 на Python: вместо несуществующего X.Image используем python-xlib и Pillow, метод put_pil_image. Пример кода.
Отрисовка маленьких иконок в окне в прототипе на Python под X11 кажется простой задачей, пока не столкнёшься с тем, что конкретная привязка реально предоставляет. Если вы пытались напрямую собрать XImage и передать сырые байты через python-xlib, то, скорее всего, получили исключение вида «module 'Xlib.X' has no attribute 'Image'». Ниже — краткое объяснение, почему так происходит, и рабочий путь с использованием Pillow и python-xlib.
Постановка задачи
Цель — показать PNG 16×16 внутри окна X11, созданного на Python 3 с помощью Pillow и python-xlib. Прямолинейный подход: открыть PNG в PIL, преобразовать в RGB-байты, собрать XImage и отправить его в окно через put_image. На этом месте всё и ломается.
Неудачный пример
Следующая минимальная программа инициализирует дисплей, создаёт окно, преобразует PNG в сырые байты и пытается создать X.Image для вывода. При первом запуске она также автоматически генерирует красную тестовую картинку 16×16.
from Xlib import display, X # Xutil здесь не требуется
from PIL import Image, ImageDraw
import os
def draw_icon(img_path, win_w, win_h, pos_x=50, pos_y=50):
pil_img = Image.open(img_path)
if pil_img.mode != 'RGB':
pil_img = pil_img.convert('RGB')
rgb_buf = pil_img.tobytes("raw", "RGB")
dpy = display.Display()
scr = dpy.screen()
root_win = scr.root
vis = scr.root_visual # ранее пробовал "default_visual"
depth_bits = scr.root_depth
wnd = root_win.create_window(
pos_x, pos_y,
win_w, win_h,
1,
depth_bits,
X.InputOutput,
vis,
background_pixel=scr.white_pixel,
event_mask=X.ExposureMask | X.KeyPressMask | X.StructureNotifyMask
)
wnd.set_wm_name("PNG image")
wnd.map()
gctx = wnd.create_gc(
foreground=scr.black_pixel,
background=scr.white_pixel
)
# Здесь всё и падает: X.Image не существует в python-xlib
ximg = X.Image(
data=rgb_buf,
width=16,
height=16,
depth=depth_bits,
bitmap_unit=8,
bitmap_pad=32,
byte_order=X.LSBFirst,
bitmap_bit_order=X.LSBFirst,
format=X.ZPixmap,
bytes_per_line=16 * 3
)
while True:
evt = dpy.next_event()
if evt.type == X.Expose:
if evt.count == 0:
wnd.put_image(gctx, ximg, 0, 0, 0, 0, 16, 16)
dpy.flush()
elif evt.type == X.KeyPress:
break
elif evt.type == X.ConfigureNotify:
pass
wnd.destroy()
dpy.close()
if __name__ == '__main__':
sample = "dummy.png"
if not os.path.exists(sample):
tmp = Image.new('RGB', (16, 16), color='red')
painter = ImageDraw.Draw(tmp)
painter.text((1, 1), "abc", fill=(255, 255, 255))
tmp.save(sample)
draw_icon(sample, 200, 200, 100, 100)
При запуске вы получите:
AttributeError: module 'Xlib.X' has no attribute 'Image'
Что на самом деле не так
Проблем две. Во‑первых, можно искать на объекте экрана поле default_visual — его там нет, зато есть root_visual. Во‑вторых, X.Image не предоставляется python-xlib. Вместо ручного создания XImage есть встроенный помощник, который принимает PIL.Image напрямую и берёт на себя передачу данных.
Исправление
Используйте screen.root_visual вместо отсутствующего default_visual и замените ручной путь через XImage на метод окна put_pil_image. Тогда не придётся конвертировать изображение в сырые байты, и вы вовсе не столкнётесь с отсутствующим X.Image.
from Xlib import display, X
from PIL import Image, ImageDraw
import os
def blit_png_to_window(img_path, win_w, win_h, pos_x=50, pos_y=50):
pil_img = Image.open(img_path)
if pil_img.mode != 'RGB':
pil_img = pil_img.convert('RGB')
dpy = display.Display()
scr = dpy.screen()
root_win = scr.root
vis = scr.root_visual
depth_bits = scr.root_depth
wnd = root_win.create_window(
pos_x, pos_y,
win_w, win_h,
1,
depth_bits,
X.InputOutput,
vis,
background_pixel=scr.white_pixel,
event_mask=X.ExposureMask | X.KeyPressMask | X.StructureNotifyMask
)
wnd.set_wm_name("PNG preview")
wnd.map()
gctx = wnd.create_gc(
foreground=scr.black_pixel,
background=scr.white_pixel
)
while True:
evt = dpy.next_event()
if evt.type == X.Expose:
if evt.count == 0:
wnd.put_pil_image(gctx, 0, 0, pil_img)
dpy.flush()
elif evt.type == X.KeyPress:
break
elif evt.type == X.ConfigureNotify:
pass
wnd.destroy()
dpy.close()
if __name__ == '__main__':
sample = "dummy.png"
if not os.path.exists(sample):
tmp = Image.new('RGB', (16, 16), color='red')
painter = ImageDraw.Draw(tmp)
painter.text((1, 1), "abc", fill=(255, 255, 255))
tmp.save(sample)
blit_png_to_window(sample, 200, 200, 100, 100)
Почему это важно
Исследуя низкоуровневый API через Python-обёртку, важно помнить: не каждое нативное понятие отзеркалено один к одному. Попытка протолкнуть недоступный путь через X.Image ведёт в тупик, тогда как готовый помощник put_pil_image даёт рабочий конвейер с меньшим объёмом кода и меньшим числом ловушек. Если вы экспериментируете с оконным менеджером на Python, знание того, какие примитивы реально доступны в привязке, сэкономит часы проб и ошибок. В экосистеме Python есть и альтернативные подходы — например, один оконный менеджер использует xcffib поверх XCB.
Практические советы
Если не уверены, есть ли у объекта python-xlib нужное поле, помогает интроспекция. Посмотрите атрибуты через dir(...) или проверьте доступные ключи во внутренних данных объекта — так легко обнаружить root_visual вместо ожидаемого default_visual. А для передачи пикселей предпочтительнее использовать прямую интеграцию привязки с Pillow, а не собирать структуры XImage вручную, которых в библиотеке нет.
Итоги
Чтобы выводить иконки PNG 16×16 в окне X11 через python-xlib, не используйте несуществующие конструкции вроде X.Image и полагайтесь на window.put_pil_image с PIL.Image. Применяйте screen.root_visual там, где это нужно, и держите цикл обработки событий минимальным: обновляйте по Expose и выходите по KeyPress. Такой подход делает код короче, надёжнее и согласованным с тем, что библиотека реально поддерживает сейчас.