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. Такой подход делает код короче, надёжнее и согласованным с тем, что библиотека реально поддерживает сейчас.