Черкая статьи в блоге приходится нередко вставлять изображения, загружать их отдельными кнопками и диалогами на сервер не всегда удобно. Намного проще вставлять из буфера обмена скриншоты либо всякие зарисовки из Gimp. Как это реализовать и будет рассказано в статье. В качестве редактора я использую популярный TinyMCE 4ой версии, на бэк-енде у меня Django на python3. 

Итак есть модель Post (пост блога), одним из полей которых есть content типа TextField:

class Post(Model):
    title = CharField(max_length=128)
    content = TextField()
...

На фронтенде это поле рендерится в textarea (id="id_content") и на это поле мы вешаем tinymce:

 tinymce.init({
     selector : "#id_content",
     paste_data_images: true,
     ...

Собственно чтобы вставлять картинки из буфера обмена достаточно включить опцию paste_data_images, как сделано выше. Таким образом при наличии картинки в clipboard нажимая Ctrl+V в редакторе картинка згружается в формате blob (капля), то есть в src будет указано что-то в виде: 

< img src="blob:http%3A//bovs.org/43b92f51-3f04-4027-b6a4-01c5bcc68b2d" alt=""  / >

Такой blob-url пока указывает на ресурс который еще хранится в браузере, но как только вы отправите форму с полем content на сервер методом POST, он автоматически будет заменен на представление в base64:

src="data:< mime_тип >;base64,<Длинная строка в base64>"

Mime_тип может быть к примеру image/png, image/gif.

Заметка: base64 представляет собой определенный текстовый алфавит из 64х символов которым можно представить любые бинарные данные. Если обычный байт данных должен быть представлен одним из 256 разных значений, не все из которых можно удобно отобразить на видимый человечиский алфавит, то один символ в base64 может быть представлен исключительно большими и маленкими латинскими Буквамии цифрами. То есть по своей сути base64 дает возможность передавать бинарные данные поверх чисто текстовых каналов передачи данных. 

В принципе дальше можноничего не делать и такие base64-врезки можно хранить в базе данных, они будут отлично загружаться в html при просмотре статей пользователями (Например можете изучить логотип к этой статье). Однако из такого хранение вытекают несколько ограничений:

  1. Эффективность хранения в base64 явно меньше чем у обычных бинарных данных, по этому большие картинки просадят время зугрузки страниц.
  2. Ограниение возможностей кеширования. Далеко не факт что все браузеры сумеют кашировать такие картинки, а кое-кто может и вовсе не умеет отображать такие картинки. Это же касается CDN серверов (если такие у вас используются), которые могут кешировать у себя ресурсы только по определенным URL, то есть если бы картинка лежала как обычно по урлу http://bovs.org/static/images.png, CDN бы загрузил ее на свой кеширующий прокси-сервер и шустро отдавал их клиентам, а так он будет думать что это HTML, который нельзя кешировать (Отмечу что алгоритм работы разных CDN может отличатся, возможно кто-то умеет и кешировать blob).
  3. Излишняя нагрузка на БД и отсутствие возможности раздавать картинки как статику отдельно вне сервера приложений. То есть все данные будут перегрибаться сервером приложений начиная от использования канала по которому база данных будет отдавать ответ на запрос SELECT, и вплоть до прохождения через всякие рендеры темплейтов в вашем фреимворке. А серверу приложений иногда и без этого вполне хватает чем себя занять. Намного эффективней все файлы запрашиваемые по урлу http://bovs.org/static/ сразу отдавать nginx-ом или apach-ем, так как они смогут сделать это максимально эффективно.
  4. Невозможно (или сложно) подредактивровать изображение на сервере, например наложить водяной знак.

В силу этих причин после загрузки содержимого поста на сервер можно выпарсить из него все картинки и посохранять их в отдельные файлы. Вот небольшой кусок моего кода:

def proc_blob(post, mo):
    data = mo.group('dat')
    fmt = mo.group('fmt')
    fname = "{}_{}.{}".format(post.id, str(time.time()).replace('.', '_'), fmt)
    full_name = os.path.join(IMAGES_ROOT, fname)
    fh = open(full_name, "wb")
    fh.write(base64.b64decode(data))
    fh.close()
    # опционально можно сохранить имя файла в бд, например чтобы учитывать какие картинки с какими постами связаны
    im = bovs.models.Image(post=post, filename=fname)
    im.save()
    
    return '<img src="/images/{}" class="img-responsive" />'.format(fname)


# Это view в который передаются POST-данные
@staff_member_required
def editpost(request, oid):
    if request.POST:
        if oid == 'n':
            form = PostEditForm(request.POST)
        else:
            post = get_object_or_404(Post, id=oid)
            form = PostEditForm(request.POST, instance=post)

        if form.is_valid():

            post = form.save(commit=False)
            post.content = re.sub(
                r'<img (class=".*?" )?src="data:image/(?P<fmt>.+?);base64,(?P<dat>.*?)".*?>',
                lambda x: proc_blob(post, x), post.content)
            post.save()
           
            ...

 Принцип работы весьма прост - для поиска и замены блобов вызывается функция re.sub, которая ищет подходящие теги img и затем передает их в функцию proc_blob с двумя важными именоваными группами (?P<fmt>) - формат MIME, грубо считаем что расширение файла и (?P<dat>) - строка данных в base64. Так как вторым аргументом функции sub нужно передать функцию с одним аргументом а я туда хочу передать еще и модель поста, я использовал лямбду с одним параметром. Найденная строка будет заменена на то что вернет proc_blob.