Pillow всемогущий

Oct 20, 2016 16:07 · 1076 words · 6 minute read python tutorial

Рано или поздно, но всем нам приходится работать с графикой. Заказчики просят добавить ватермарки, сделать превьюшки картинок, построить графики… В мире python для этого есть библиотека Pillow, которая действительно умеет если не всё, то очень многое.

Установка pillow

Pillow далеко не самостоятельная библиотека, ей необходима поддержка форматов сжатия файлов. Для ubuntu как правило хватает установки следующих пакетов:

$ sudo apt-get install libjpeg-dev libfreetype6-dev zlib1g-dev libpng12-dev

То есть работы с jpg, png, ttf (шрифты) и gzip (нужен для некоторых форматов). Самое главное тут разделять сам объект Image (холст), который представляет собой абстрактную картинку, от его представления на диске в виде файла. Попробуем поиграться:

Пример

from PIL import Image, ImageDraw, ImageFont

def sample():
    image = Image.new('RGBA', (300, 300))
    draw = ImageDraw.Draw(image)
    draw.line(((0, 0), (150, 300)), fill=(255, 0, 0), width=20)  # красная линия
    draw.arc(((150, 150), (250, 250)), 45, 210, fill=(0, 255, 0))  # зелёная дуга
    draw.rectangle(((50, 50), (150, 150)), fill=(0, 0, 255))  # синий квадрат

    font = ImageFont.truetype("data/kaligrafica_allfont_ru.ttf", 32)
    draw.text((200, 200), 'Hello world!', (255, 255, 0), font=font)  # жёлтый текст
    image = image.rotate(90)  # поворот на 90 градусов
    image.save('sample.png')

if __name__ == "__main__":
    sample()

Ничего сложного - рисование примитивов и текста. Такое в жизни вряд ли пригодится, так что этот пример исключительно для ознакомления с базовыми возможностями.

Создание превью изображения

Существует куча разных библиотек для создания и хранения превьюшек картинок. В своих проектах на Django я использую sorl-thumbnail. В принципе она всем устраивает, большой плюс что она хранит ссылки на все сгенерированные изображения. Таким образом при смене основного изображения превьюшки пересоздаются заново. До недавнего времени там был критичный баг, но после небольших уговоров автор его закрыл.

Но не всегда разумно тянуть в проект такую махину. Порой хочется чего-нибудь лёгкого и простого. Например, как-то так:

im = Image.open('input.jpg')
im.thumbnail((64, 64))
im.save("thumbnail.jpg", "JPEG")

Что может быть проще! :) У Image ещё есть метод resize, но, в отличие от thumbnail, он не сохраняет соотношение сторон (aspect ratio). Т.е. в примере выше картинка будет ужата до 64 пикселей по большей стороне.

Наложение ватермарка

Как всегда для Django есть уже готовая библиотека, но нам же интересно самим всё сделать, правда ведь? :) В качестве водяного знака возьмём вот этого котика, повёрнутого на 45 градусов. Нууу… все же любят котиков…

Watermark

mark = Image.open('cat_PNG132.png')
mark = mark.resize(source.size)
source.paste(mark, (0, 0), mark)
source.save('wm.png')

Таким вот образом можно наложить изображения друг на друга, но на водяной знак это смахивает лишь очень отдалённо. Хм, opacity 0.1, rotate 30. Кстати, тут мы работаем с прозрачными изображениями, так что вместо обычного метода Image.paste надо использовать Image.alpha_composite - он сохраняет альфа-каналы.

Watermark result

mark = Image.open('cat_PNG132.png')
mark = mark.resize(source.size)
alpha = ImageEnhance.Brightness(mark.split()[3]).enhance(0.1)
mark.putalpha(alpha)
mark = mark.rotate(-30, Image.BICUBIC)
Image.alpha_composite(source, mark).save("test3.png")

Склейка изображений

Ещё один пример работы с изображениями - компоновка нескольких в одно. Как пример это может быть массив спрайтов (для web) или фреймов для анимации какой-нибудь 2D игрушки (Да-да, я знаю, что велосипедов и так уже полно, и некоторые даже работают, но у них есть фатальный недостаток :) ). Мне же понадобилось из картинок-элементов собрать таблицу Менделеева. Так что код из рабочего проекта:

def concat(images):
    width, height = images[0][0].size  # size of element
    total_width = width * len(images[0])
    max_height = height * len(images)
    result = Image.new('RGBA', (total_width, max_height))  # common canvas

    y_offset = 0
    for line in images:
        x_offset = 0
        for element in line:
            result.paste(element, (x_offset, y_offset))
            x_offset += element.size[0]
        y_offset += line[0].size[1]
    return result

Склейка изображения

В images передаётся матрица из объектов типа Image. В первом блоке вычисляются размеры результирующего холста, а во втором вставляются картинки согласно элементам матрицы. Если будет интересно, то в комментариях могу выложить полный код по склейке таблицы Менделеева из готовых png и саму таблицу для печати.

Генерация gif

Переходим к генерации gif из получившихся картинок. Вот здесь всё несколько неочевидно. Беглое гугление говорит, что есть специальная библиотечка images2gif (только python2), однако сам автор просит её не использовать. Некоторые предлагают вызывать convert -delay 20 -loop 0 *jpg animated.gif, но что-то всё это костылями попахивает, неужели сам Pillow не умеет склеивать png в gif?! Конечно же умеет! Причём сам, без библиотеки pillow-images2gif. Пусть у нас в каталоге data/png находятся 10 файлов вида 000.png, 001.png, 002.png… Тогда склеить их в один gif можно таким кодом:

SIZE = (300, 300)  # размер gif
WHITE = (255, 255, 255)  # фон каждого кадра gif

def prepare(img):  # resize to 300x300 and remove opacity
    bg = Image.new("RGB", SIZE, WHITE)
    img = img.resize(SIZE, Image.ANTIALIAS)
    bg.paste(img, img)
    return bg.convert('P', palette=Image.ADAPTIVE, dither=1)

images = list(map(prepare, map(Image.open, ('data/png/{0:03d}.png'.format(x) for x in range(10)))))
gif = Image.new('RGB', SIZE, WHITE)
gif.save('temp.gif', 'GIF', save_all=True, append_images=images, loop=0)  # loop=0 зацикливает воспроизведение

Это код для Python3, так что map пришлось распаковать в список. Ух, прям обожаю функциональщину - так много делается всего в одной строчке :) С помощью внутреннего map создаётся итератор по списку Image для каждого файла, а потом к каждому Image применяется функция prepare. Её назначение заключается лишь в подготовке кадра - привести к одному размеру и сделать фон белым вместо прозрачного. К сожалению, иного способа кроме как положить png (переменная img) на белый холст (переменная bg) для удаления прозрачности я не нашёл. Согласен, как-то некрасиво получается… Зато потом одной командой save оно само как-то всё склеивается. Посмотреть можно здесь.

А теперь наоборот

Зачастую надо не собрать gif из картинок, а наоборот разбить (а потом, возможно, сохранить в новый gif). Pillow умеет и это:

im = Image.open("in.gif")
frames = ImageSequence.Iterator(im)

def resize(frames):
    for frame in frames:
        image = frame.copy()
        yield image.crop((0, 0, image.width, image.height - 15))

frames = resize(frames)

om = next(frames)
om.info = im.info
om.save("out.gif", save_all=True, append_images=list(frames))

К сожалению, здесь может быть не так просто из-за формата gif. В нём каждый кадр может содержать свою собственную палитру. Это помогает в уменьшении размеров файла, но накладывает дополнительные трудности при работе с ним. Причём палитра может вычисляться как дифф от предыдущего кадра с одним “прозрачным” цветом. Нельзя просто так взять и сохранить gif по кадрам То есть, надо накапливать разницу цветов, учитывая ту же прозрачность, но поля этого блога слишком малы, чтобы привести решение :)

Вместо послесловия

Работа с графикой таким образом иногда напоминает магию, особенно когда действий несколько. Например, загрузить файл по http, преобразовать в поток байт, открыть по нему Image, повернуть/отразить/уменьшить и записать на диск. Да, это, конечно же, всего десяток строк, но провозиться с этим можно долго. Мне помогает отлаживать программы метод Image.show(), который показывает текущую картинку на экране. Можно сказать, что это своего рода println для pillow :)

Как всегда код на GitHub. В файле sample.py примеры к статье, в файле main.py - генерация таблицы Менделеева. Картинки, к сожалению, в открытый доступ загрузить не смогу, т.к. они отрисованы вручную дизайнером. Если интересуют именно они, пишите в личку в Telegram (@TyVik).