При написании собственного «инстаграма» появилась необходимость в наложении фильтров на изображение при аплоаде. Изначально, чтобы особо не нагружать сервер, было решено вынести процесс преобразования картинки на клиентскую сторону. Основная идея – загрузка изображения в канву, манипуляции над ним и выгрузка потока байт (результирующего изображения) на сервер. Для реализации была выбрана js-библиотека CamanJS , которая может работать как в браузере, так и на стороне сервера через NodeJS. Однако пришлось от нее отказаться из-за трех причин:
Затем была предпринята попытка использовать CamanJS на стороне сервера. Результат опять оказался неудовлетворительным:
В итоге пришлось полностью отказаться от CamanJS.
Для обработки изображений на серверной стороне самым оптимальным вариантом оказался программный комплекс ImageMagick, который обладает довольно богатым функционалом и имеет множество расширений для различных языков программирования. Поскольку наш проект работает на django, то нас, прежде всего, интересовали python-расширения для ImageMagick – PythonMagick и Wanda. Как выяснилось, они поддерживают не все возможности ImageMagick, часть графических эффектов просто отсутствует, поэтому мы воспользовались прямым вызовом imagemagick через subprocess.
Применение эффектов происходит через специальные bash-скрипты, которые были получены с помощью очень полезных ресурсов http://www.fmwconcepts.com/imagemagick — здесь лежат сами скрипты с описанием, http://jqmagick.imagemagick.org – а здесь можно поэкспериментировать с различными эффектами и подобрать параметры.
Сначала заходим на http://jqmagick.imagemagick.org, аплоадим картинку, выбираем нужный эффект, подбираем параметры к нему. Если все красиво и все нас устраивает, копируем пример команды для выполнения скрипта с необходимыми параметрами из нижнего поля раздела «options» (по дефолту правая нижняя часть интрефейса jqmagick). Например:
Интересующий нас скрипт называется vintage1.sh. Перемещаемся на http://www.fmwconcepts.com/imagemagick, находим нужный скрипт, скачиваем его и не забываем выставить флажок «на исполнение». И вот таким образом подбираем все нужные нам эффекты.
Теперь все готово для программной реализации.
Итак, задача:
Дать возможность пользователям (в нашем случае это — продавцы на e-commerce площадке по продаже уникальных товаров) загружать изображения на сервер, применять к ним эффекты из заранее подготовленного набора с возможностью отображения результата.
Для загрузки изображения на сервер мы использовали js-библиотеку filereader.js. Использование и пример конфигурации можно найти в спецификации для этой библиотеки. Непосредственную отправку файла на сервер мы реализовали с помощью метода send() oбъекта XMLHttpRequest после того, как файл будет полностью выгружен в объект FileReader браузера. Для этого определяем опцию «load»:
Не сервере мы принимаем выгружаемое изображение, создаем его копию для превью и возвращаем клиенту название оригинальной картинки и название превью к ней, чтобы заполнить атрибут src элемента у превью.
Сам TEMP_IMG_DIR в setting.py определяется так:
Основная идея здесь заключается в том, что эффекты будут применяться не к оригинальному изображению, а к его превью, и лишь после финального нажатия на кнопку «Сохранить» выбранный эффект применится к самому изображению.
Функция применения эффекта на клиентской стороне:
На серверной стороне определяем команды для применения эффектов и сопоставляем их с названиями фильтров, здесь удобно использовать словарь:
Функция применения эффекта на стороне сервера:
Функция apply_filter вызывается обработчик set_filter, который принимает 2 аргумента: путь к изображению и наименование фильтра, а возвращает — путь к измененному изображению.
После того, как пользователь определился с нужным эффектом, он нажимает кнопку «Сохранить», и вызывается функция set_filter, в качестве аргумента передается сохраненный ранее путь к оригинальному изображению и результирующий эффект.
Напоследок приложу скрин того, как все это выглядит у нас:
- CamanJS не поддерживается мобильными браузерами (Safari, Chrome в частности);
- CamanJS заставляет течь память в браузере (особенно при работе с крупными изображениями);
- CamanJS сильно тормозит в Firefox при наложении фильтров.
Затем была предпринята попытка использовать CamanJS на стороне сервера. Результат опять оказался неудовлетворительным:
- После преобразования изображение увеличивалось в 3-4 раза;
- Преобразование изображения происходило совсем не быстро.
В итоге пришлось полностью отказаться от CamanJS.
Для обработки изображений на серверной стороне самым оптимальным вариантом оказался программный комплекс ImageMagick, который обладает довольно богатым функционалом и имеет множество расширений для различных языков программирования. Поскольку наш проект работает на django, то нас, прежде всего, интересовали python-расширения для ImageMagick – PythonMagick и Wanda. Как выяснилось, они поддерживают не все возможности ImageMagick, часть графических эффектов просто отсутствует, поэтому мы воспользовались прямым вызовом imagemagick через subprocess.
Применение эффектов происходит через специальные bash-скрипты, которые были получены с помощью очень полезных ресурсов http://www.fmwconcepts.com/imagemagick — здесь лежат сами скрипты с описанием, http://jqmagick.imagemagick.org – а здесь можно поэкспериментировать с различными эффектами и подобрать параметры.
Сначала заходим на http://jqmagick.imagemagick.org, аплоадим картинку, выбираем нужный эффект, подбираем параметры к нему. Если все красиво и все нас устраивает, копируем пример команды для выполнения скрипта с необходимыми параметрами из нижнего поля раздела «options» (по дефолту правая нижняя часть интрефейса jqmagick). Например:
bash scripts/vintage1.sh -b 0 -c 35 -s roundrectangle -T torn -I grunge -C white output/8526-603.jpg output/3347-9458.jpg
Интересующий нас скрипт называется vintage1.sh. Перемещаемся на http://www.fmwconcepts.com/imagemagick, находим нужный скрипт, скачиваем его и не забываем выставить флажок «на исполнение». И вот таким образом подбираем все нужные нам эффекты.
Теперь все готово для программной реализации.
Итак, задача:
Дать возможность пользователям (в нашем случае это — продавцы на e-commerce площадке по продаже уникальных товаров) загружать изображения на сервер, применять к ним эффекты из заранее подготовленного набора с возможностью отображения результата.
Загрузка изображения на сервер
Для загрузки изображения на сервер мы использовали js-библиотеку filereader.js. Использование и пример конфигурации можно найти в спецификации для этой библиотеки. Непосредственную отправку файла на сервер мы реализовали с помощью метода send() oбъекта XMLHttpRequest после того, как файл будет полностью выгружен в объект FileReader браузера. Для этого определяем опцию «load»:
var opts = {
// …
load: function(e, file) {
var xhr = new XMLHttpRequest();
xhr.open('POST', '{% url upload_file %}', true);
xhr.onload = function() {
if (this.status == 200) {
// обрабатываем ответ от сервера после удачной загрузки
var resp = JSON.parse(this.response);
// сохраняем пути к оригинальному изображению и превью (для применения фильтров к нему)
filter_image = resp['image'];
filter_thumb = resp['thumb'];
}
};
xhr.send(file);
},
//…
};
Не сервере мы принимаем выгружаемое изображение, создаем его копию для превью и возвращаем клиенту название оригинальной картинки и название превью к ней, чтобы заполнить атрибут src элемента у превью.
import os
import datetime
from PIL import Image
try:
from cStringIO import StringIO
except ImportError:
from StringIO import StringIO
import simplejson as json
from django.http import HttpResponse
from django.conf import settings
def upload_file(request):
max_size = (2560, 2048) # задаем максимальное разрешение для оригинала
thumb_size = (325, 325) # максимальное разрешение для превью
f_data = request.body
fake_file = StringIO()
fake_file.write(f_data)
fake_file.seek(0)
img = Image.open(fake_file)
img.thumbnail(max_size, Image.ANTIALIAS) # ужимаем при необходимости оригинал
tmp_dir = settings.TEMP_IMG_DIR # все временные изображения будем хранить в отведенном месте
if not os.path.exists(tmp_dir):
os.makedirs(tmp_dir)
# директории с изображениями будут группироваться по датам
inner_dir_name = datetime.datetime.now().strftime('%d.%m.%Y')
inner_dir = os.path.abspath(os.path.join(tmp_dir, inner_dir_name))
if not os.path.exists(inner_dir):
os.makedirs(inner_dir)
tmp_file_name = generate_tmp_file_name() # получение уникального имени для файла
thumb_tmp_file_name = 'thumb_' + tmp_file_name # для превью добавляем префикс
output = os.path.abspath(os.path.join(inner_dir, tmp_file_name))
output_thumb = os.path.abspath(os.path.join(inner_dir, thumb_tmp_file_name))
if not img.mode == 'RGB':
img = img.convert('RGB')
img.save(output, "JPEG") # преобразуем оригинал и сохраняем в jpeg для экономии места
img.thumbnail(thumb_size, Image.ANTIALIAS)
img.save(output_thumb, "JPEG") # аналогично и для превью
to_response = json.dumps({
'image': ''.join([settings.MEDIA_URL, '/'.join([settings.TEMP_IMG_DIR_NAME,
innder_dir_name, tmp_file_name])]),
'thumb': ''.join([settings.MEDIA_URL, '/'.join([settings.TEMP_IMG_DIR_NAME,
innder_dir_name, thumb_tmp_file_name])]),
})
return HttpResponse(to_response, mimetype="application/json")
Сам TEMP_IMG_DIR в setting.py определяется так:
# Директория для хранения временных картинок
TEMP_IMG_DIR = os.path.abspath(os.path.join(MEDIA_ROOT, 'temp_img'))
Применение эффектов к изображению
Основная идея здесь заключается в том, что эффекты будут применяться не к оригинальному изображению, а к его превью, и лишь после финального нажатия на кнопку «Сохранить» выбранный эффект применится к самому изображению.
Функция применения эффекта на клиентской стороне:
function setFilter(filter_name) {
result_filter = filter_name
$.ajax({
url: '{% url set_filter %}',
method: 'POST',
data: {
'img_path': filter_thumb, // отправляем путь к превью
'filter_name': filter_name // и наименование фильтра
},
success: function(response) {
$('#result_img').attr('src', response); // отображаем результат
}
})
}
На серверной стороне определяем команды для применения эффектов и сопоставляем их с названиями фильтров, здесь удобно использовать словарь:
FILTERS_COMMAND = {
'f1': "bash_scripts/colortemp.sh -t 10950 {file_name} {output}",
'f2': "bash_scripts/colortemp.sh -t 5736 {file_name} {output}",
# …
'f8': "bash_scripts/colorfilter.sh -c sepia -m 1 -d 28 {file_name} {output}",
'f9': "bash_scripts/colorfilter.sh -c underwater -m 1 -d 20 {file_name} {output}"
}
Функция применения эффекта на стороне сервера:
import os
import sys
import subprocess
def apply_filter(img_path, filter_name, output=None):
if output is None:
output_file_name = ''.join([filter_name, '_', os.path.basename(img_path)])
output_file = os.path.abspath(os.path.join(img_path.replace(os.path.basename(img_path), ''), output_file_name))
if os.path.exists(output_file):
return output_file
else:
output_file = output
command = FILTERS_COMMAND[filter_name]
# Используем bash, стало быть, скобки должны быть в виде:
command = command.replace('(', '\(').replace(')', '\)')
command = command.format(file_name=img_path, output=output_file)
subprocess.check_output(command, shell=True, stderr=subprocess.STDOUT)
return output_file
Функция apply_filter вызывается обработчик set_filter, который принимает 2 аргумента: путь к изображению и наименование фильтра, а возвращает — путь к измененному изображению.
После того, как пользователь определился с нужным эффектом, он нажимает кнопку «Сохранить», и вызывается функция set_filter, в качестве аргумента передается сохраненный ранее путь к оригинальному изображению и результирующий эффект.
Напоследок приложу скрин того, как все это выглядит у нас: