Так получилось, что мне давно понравилась выдача картинок Вконтакте, но я долгое время не обращал на них внимания, считая чем-то рядовым. Настолько они были гармоничными и приятные глазу, что казались чем-то естественным и простым.

Решено было повторить генерацию коллажа для картинок товаров интернет-магазина. Оказалось, что существует множество решений на JQuery, но загружать клиентскую машину лишней работой уж больно не хотелось. Было желание произвести подготовку коллажа на стороне сервера. Причём, не с фиксированной высотой строки как у Яндекс.картинок, а именно с полной динамикой как по вертикали, так и по горизонтали.
Поиски огорчили, готового решения на python нет. Но есть библиотека Photocollage, работающая лишь на gui и, по-моему, не на всех даже платформах. Не долго думав, мной был заимствован единственный его ключевой модуль collage.py), содержащий все объекты, всю математику расчёта нужного мне коллажа и невообразимо удобный API с доступом ко всем параметрам каждого элемента сгенерированного коллажа. Для его работы требуется лишь установленный PIL. И нужен он только для снятия ширины и высоты исходных изображений при создании объекта Photo.
На основании класса UserCollage, передав ему просто список полных адресов картинок на сервере, мы получаем объект-прототип коллажа new_collage.
И всё бы просто, надо лишь вывести этот коллаж в html, но я быстро столкнулся с парой проблем:
• Оказалось, что коллаж генерируется под горизонтальный рост. Т.е. каждая 3я картинка генерировала дополнительный столбец. Что не может устраивать, если ширина коллажа строго ограничена и его рост должен идти вниз.
• Объект new_collage просто не предусматривает отступы между картинками. Исходный модуль делает это при отрисовке.
Первая проблема решилась быстро, переписав расчёт в UserCollage и поделив коллажи на горизонтальные и вертикальные. Вызывая метод make_page, назначаем критичную ширину коллажа, получаем готовый объект new_collage.page. С ним и работаем.
Вторую проблему пришлось решать при составлении html коллажа. Решено было назначить каждой ячейке style=”position:absolute”, передать ей координаты left и top, значение border отнять от её ширины и высоты, кроме последней. Также не забываем, в случае, если картинка больше ячейки, отцентровать её по сердцевине. Итог получился таким:
Данный способ создания коллажа имеет недостатки:
• В нём присутствует рандом. Т.е. невозможно точно предугадать какой коллаж мы получим на выходе. Готового шаблона нет.
• Иногда не получаются коллажи идеально ровной формы.
• Объединение по ширине одной ячейки максимум двух колонок.
Итоговый код:
Пример здесь. Можно обновлять страницу и посмотреть что предложит код. Если коллаж не впишется в target_w, будет выдана старая версия превью картинок.
Спасибо за внимание!

Решено было повторить генерацию коллажа для картинок товаров интернет-магазина. Оказалось, что существует множество решений на JQuery, но загружать клиентскую машину лишней работой уж больно не хотелось. Было желание произвести подготовку коллажа на стороне сервера. Причём, не с фиксированной высотой строки как у Яндекс.картинок, а именно с полной динамикой как по вертикали, так и по горизонтали.
Поиски огорчили, готового решения на python нет. Но есть библиотека Photocollage, работающая лишь на gui и, по-моему, не на всех даже платформах. Не долго думав, мной был заимствован единственный его ключевой модуль collage.py), содержащий все объекты, всю математику расчёта нужного мне коллажа и невообразимо удобный API с доступом ко всем параметрам каждого элемента сгенерированного коллажа. Для его работы требуется лишь установленный PIL. И нужен он только для снятия ширины и высоты исходных изображений при создании объекта Photo.
На основании класса UserCollage, передав ему просто список полных адресов картинок на сервере, мы получаем объект-прототип коллажа new_collage.
И всё бы просто, надо лишь вывести этот коллаж в html, но я быстро столкнулся с парой проблем:
• Оказалось, что коллаж генерируется под горизонтальный рост. Т.е. каждая 3я картинка генерировала дополнительный столбец. Что не может устраивать, если ширина коллажа строго ограничена и его рост должен идти вниз.
• Объект new_collage просто не предусматривает отступы между картинками. Исходный модуль делает это при отрисовке.
Первая проблема решилась быстро, переписав расчёт в UserCollage и поделив коллажи на горизонтальные и вертикальные. Вызывая метод make_page, назначаем критичную ширину коллажа, получаем готовый объект new_collage.page. С ним и работаем.
class UserCollage(object):
"""UserCollage получает список фотографий и подбирает количество колонок в коллаже.
Задавая collage_type можно задать тип коллажа: вертикальный или горизонтальный.
При горизонтальном коллаже количество колонок не ограничено, что недопустимо при вертикальном.
Вертикальный коллаж ограничен 3 колонками.
"""
def __init__(self, filelist, collage_type = "horizontal", no_cols=None):
self.photolist = self.build_photolist(filelist)
self.collage_type = collage_type
if not no_cols:
if self.collage_type == "horizontal":
no_cols = int(round(1.5 * math.sqrt(len(self.photolist))))
elif self.collage_type == "vertical":
no_cols = int(math.ceil(math.sqrt(len(self.photolist))))
#no_cols = no_cols if no_cols<3 else 3
no_cols = 3 if len(self.photolist)>3 else no_cols
self.no_cols = no_cols or 1
def build_photolist(self, filelist):
'''
Получаем список полных адресов картинок, собираем и возвращаем список объектов Photo
'''
ret = []
for name in filelist:
try:
img = PIL.Image.open(name)
except IOError:
raise BadPhoto(name)
w, h = img.size
orientation = 0
try:
exif = img._getexif()
if 274 in exif: # orientation tag
orientation = exif[274]
if orientation == 6 or orientation == 8:
w, h = h, w
except:
pass
ret.append(collage.Photo(name, w, h, orientation))
return ret
def make_page(self, target_w=300):
'''
Собирает и возвращает объект Page. Это протитип коллажа.
Метод adjust финализирует коллаж, убирая пустоты и,
выравнивая столбцы Collumn и ячейки Cell по высоте и ширине соот-но.
Можно задать целевую ширину target_w, но не факт, что коллаж получится не меньше.
'''
self.page = Page(target_w, self.no_cols)
for num, photo in enumerate(self.photolist):
if num==0:
col = self.page.next_free_col()
left = col.left_neighbor()
right = col.right_neighbor()
if left and abs(col.h - left.h) < 0.5 * col.w:
self.page.add_cell_multi_col(left, col, photo)
elif right and abs(col.h - right.h) < 0.5 * col.w:
self.page.add_cell_multi_col(col, right, photo)
else: self.page.add_cell(photo)
else: self.page.add_cell(photo)
Вторую проблему пришлось решать при составлении html коллажа. Решено было назначить каждой ячейке style=”position:absolute”, передать ей координаты left и top, значение border отнять от её ширины и высоты, кроме последней. Также не забываем, в случае, если картинка больше ячейки, отцентровать её по сердцевине. Итог получился таким:
class MyPage ( collage.Page ):
def to_html(self):
'''Возвращает html-представление коллажа'''
n = 0
border = 10
end = False
div = ['<div style="width:287px;"><div style="height:%dpx;display:block;position:relative;overflow:hidden;">' % self.h]
img={}
while not end:
end = True
for no_col,col in enumerate(self.cols):
if n < len(col.cells):
last_column = bool(no_col == len(self.cols)-1)
if not col.cells[n].photo.filename in img:
img[col.cells[n].photo.filename] = {'width':0}
if col.cells[n].is_extension():
img[col.cells[n].photo.filename]['width']+=col.w
else:
margin_left = 0
margin_top = 0
y = col.cells[n].y # абсолютная координата Y
x = col.x # абсолютная координата X
if not last_column:
if col.cells[n].photo.w-col.cells[n].w>0:
margin_left = -int((col.cells[n].photo.w - col.cells[n].w)/2)
if col.cells[n].photo.h-col.cells[n].h>0:
margin_top = -int((col.cells[n].photo.h - col.cells[n].h)/2)
img[col.cells[n].photo.filename]={
'width':img[col.cells[n].photo.filename]['width'] + col.w - border/2,
'img_width': col.cells[n].photo.w,
'height':col.cells[n].h - border/2,
'x':x,
'y':y,
'margin_left':margin_left if last_column else 0,
'margin_top' :margin_top}
if last_column:
img[col.cells[n].photo.filename]['width'] = ''
#print n,no_col, img[col.cells[n].photo.filename]['width']
if n < len(col.cells) - 1:
end = False
n += 1
div.append('\n'.join(['''<a style="margin:5px;overflow:hidden;float:left;position:absolute;
width:{width}px;height:{height}px;top:{top}px;left:{left}px;">
<img src="{img}" style="margin-left: {margin_left}px;margin-top:{margin_top}px;width:{img_width}px;"></a>'''.format(
width = attrs['width'] ,
img_width = attrs['img_width'] ,
height = attrs['height'] ,
top = attrs['y'] ,
left = attrs['x'] ,
margin_left = attrs['margin_left'],
margin_top = attrs['margin_top'] ,
img = src) for src,attrs in img.items()]))
div.append('</div></div>')
return ''.join(div)
Данный способ создания коллажа имеет недостатки:
• В нём присутствует рандом. Т.е. невозможно точно предугадать какой коллаж мы получим на выходе. Готового шаблона нет.
• Иногда не получаются коллажи идеально ровной формы.
• Объединение по ширине одной ячейки максимум двух колонок.
Итоговый код:
# -*- coding: UTF-8 -*-
import collage, os, math
import PIL.Image
target_w=287 #Целевая ширина
class MyPage ( collage.Page ):
def to_html(self):
'''Возвращает html-представление коллажа'''
n = 0
border = 10
end = False
div = ['<div style="width:287px;"><div style="height:%dpx;display:block;position:relative;overflow:hidden;">' % self.h]
img={}
while not end:
end = True
for no_col,col in enumerate(self.cols):
if n < len(col.cells):
last_column = bool(no_col == len(self.cols)-1)
if not col.cells[n].photo.filename in img:
img[col.cells[n].photo.filename] = {'width':0}
if col.cells[n].is_extension():
img[col.cells[n].photo.filename]['width']+=col.w
else:
margin_left = 0
margin_top = 0
y = col.cells[n].y # абсолютная координата Y
x = col.x # абсолютная координата X
if not last_column:
if col.cells[n].photo.w-col.cells[n].w>0:
margin_left = -int((col.cells[n].photo.w - col.cells[n].w)/2)
if col.cells[n].photo.h-col.cells[n].h>0:
margin_top = -int((col.cells[n].photo.h - col.cells[n].h)/2)
img[col.cells[n].photo.filename]={
'width':img[col.cells[n].photo.filename]['width'] + col.w - border/2,
'img_width': col.cells[n].photo.w,
'height':col.cells[n].h - border/2,
'x':x,
'y':y,
'margin_left':margin_left if last_column else 0,
'margin_top' :margin_top}
if last_column:
img[col.cells[n].photo.filename]['width'] = ''
#print n,no_col, img[col.cells[n].photo.filename]['width']
if n < len(col.cells) - 1:
end = False
n += 1
div.append('\n'.join(['''<a style="margin:5px;overflow:hidden;float:left;position:absolute;
width:{width}px;height:{height}px;top:{top}px;left:{left}px;">
<img src="{img}" style="margin-left: {margin_left}px;margin-top:{margin_top}px;width:{img_width}px;"></a>'''.format(
width = attrs['width'] ,
img_width = attrs['img_width'] ,
height = attrs['height'] ,
top = attrs['y'] ,
left = attrs['x'] ,
margin_left = attrs['margin_left'],
margin_top = attrs['margin_top'] ,
img = src) for src,attrs in img.items()]))
div.append('</div></div>')
return ''.join(div)
def adjust_cols_heights(self):
"""Set all columns' heights to same value by shrinking them"""
all_heights = list(set([c.h for c in self.cols]))
all_heights.sort()
sum_w = sum(c.w for c in self.cols)
while target_w*0.9>sum_w>target_w or all_heights:
target_h = all_heights.pop()
for c in self.cols:
c.adjust_height(target_h)
class UserCollage(object):
"""UserCollage получает список фотографий и подбирает количество колонок в коллаже.
Задавая collage_type можно задать тип коллажа: вертикальный или горизонтальный.
При горизонтальном коллаже количество колонок не ограничено, что недопустимо при вертикальном.
Вертикальный коллаж ограничен 3 колонками.
"""
def __init__(self, filelist, collage_type = "horizontal", no_cols=None):
self.photolist = self.build_photolist(filelist)
self.collage_type = collage_type
if not no_cols:
if self.collage_type == "horizontal":
no_cols = int(round(1.5 * math.sqrt(len(self.photolist))))
elif self.collage_type == "vertical":
no_cols = int(math.ceil(math.sqrt(len(self.photolist))))
#no_cols = no_cols if no_cols<3 else 3
no_cols = 3 if len(self.photolist)>3 else no_cols
self.no_cols = no_cols or 1
def build_photolist(self, filelist):
'''
Получаем список полных адресов картинок, собираем и возвращаем список объектов Photo
'''
ret = []
for name in filelist:
try:
img = PIL.Image.open(name)
except IOError:
raise BadPhoto(name)
w, h = img.size
orientation = 0
try:
exif = img._getexif()
if 274 in exif: # orientation tag
orientation = exif[274]
if orientation == 6 or orientation == 8:
w, h = h, w
except:
pass
ret.append(collage.Photo(name, w, h, orientation))
return ret
def make_page(self, target_w=300):
'''
Собирает и возвращает объект Page. Это протитип коллажа.
Метод adjust финализирует коллаж, убирая пустоты и,
выравнивая столбцы Collumn и ячейки Cell по высоте и ширине соот-но.
Можно задать целевую ширину target_w, но не факт, что коллаж получится не меньше.
'''
self.page = MyPage(target_w, self.no_cols)
for num, photo in enumerate(self.photolist):
if num==0:
col = self.page.next_free_col()
left = col.left_neighbor()
right = col.right_neighbor()
if left and abs(col.h - left.h) < 0.5 * col.w:
self.page.add_cell_multi_col(left, col, photo)
elif right and abs(col.h - right.h) < 0.5 * col.w:
self.page.add_cell_multi_col(col, right, photo)
else: self.page.add_cell(photo)
else: self.page.add_cell(photo)
if __name__=="__main__":
dir = 'd:\\' # Папка с фотками
img_list = os.listdir(dir)
new_collage = UserCollage([dir+x for x in img_list], collage_type = "vertical")
new_collage.make_page(target_w)
new_collage.page.adjust()
print new_collage.page.to_html() # получаем HTML
Пример здесь. Можно обновлять страницу и посмотреть что предложит код. Если коллаж не впишется в target_w, будет выдана старая версия превью картинок.
Спасибо за внимание!