Отправка канвы на Rails-бекенд

rails4 + paperclip + canvas

В интернетах всё ещё встречаются сайты, где от пользователя требуются сложные манипуляции с изображениями в графических редакторах просто для того, чтобы поставить котика на аву. Однако с помощью HTML5-элемента canvas можно не только самостоятельно отмасштабировать и обрезать котика, но и, например, наложить на него приятный глитч-эффект. И всё это на стороне клиента: без отправки котиков на бекенд и обратно.

Почему же мир так несправедлив?

Проблематичным моментом в этом процессе является нетривиальность отправки и получения канвы, так как canvas не является input-элементом и поэтому не передаётся при отправке формы, а также не может быть получен как обычный файл на стороне бекенда.

Однако содержимое канвы можно стандартным образом преобразовывать в base64-строки вида

data:image/png;base64,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

поэтому для получения изображения канвы в виде файла на бекенде достаточно декодировать подобную base64-строку в строку бинарную, сохранить её во временный файл и провести всё это так, чтобы, например, paperclip ничего не заметил.

Фронтенд

Так как канва не может быть непосредственно передана как файл, то нам понадобится дополнительное input-поле для передачи её base64-репрезентации. Так что лучше бы вам удалить поля file_field, если они у вас ещё есть, и добавить новое скрытое поле для передачи данных канвы

<%= f.hidden_field :canvas_file %>

а также соответствующий onDomReady-обработчик, предворяющий отправку формы кодом, который свяжет это поле с содержимым канвы:

$(function(){
  $('#<id_of_your_form>').submit(function() {
    var canvas = $('#canvas')[0]
    $('#canvas_file').val(canvas.toDataURL());
  })
})

Ключевым здесь является метод toDataURL, собственно, превращающий содержимое канвы в base64-репрезентацию png-изображения, соответствующего содержимому канвы.

Кстати, через toDataURL можно декодиться и в jpeg с указанием качества, только нужно понимать, что это будет пересжатие с потерями. Ещё, не знаю, с чем это связанно, но toDataURL из консоли Chrome у меня по крайней мере возвращает изображение типа "no resource" вместо актуального содержимого.

Декодер

Расшифровывать получаемую base64-строку можно с помощью модуля datafy.rb. Для чего достаточно расположить его в lib и загружать перед использованием.

Для автозагрузки модулей из lib, можно добавить в config/application.rb строку

config.autoload_paths << Rails.root.join('lib')

либо загружать модуль вручную.

Кажется, он также существует в виде гема.

Бекенд

На бекенде нужно декодировать base64-строку и передать её в paperclip (или другой обработчик загрузок).

Учитывая, что мы добавили новое поле в форму, мы обновляем нашу модель, добавляя атрибут canvas_file (и не забывая разрешить его в контроллере), проверяем, что тип png — валидный mime-тип для загрузчика (иначе загрузка канвы будет невозможна), выполняем декодирование и передаём результат в загрузчик через временный файл:

attr_accessor :canvas_file

has_attached_file :file
validates_attachment :file, content_type: { content_type: ["image/jpg", "image/jpeg", 'image/png'] }

before_validation :save_canvas_file

private
  def save_canvas_file
    f = File.open("tmp/reply.png", "wb")
    data, mediatype = Datafy::decode_data_uri(canvas_file)
    f.write(data)
    f.close
    self.file = File.open("tmp/reply.png", "r")
  end

Изображение временно сохраняется в проектной папке tmp:

$ file tmp/reply.png
reply.png: PNG image data, 240 x 240, 8-bit/color RGBA, non-interlaced

 

 

Статья основана на исходниках git-пользователя pierrevalade и утилите datafy.rb оттуда же (автор модуля — Eric Hodel).

 

 

 

shitpoet@gmail.com

 



 

free hit counters