Djangoで、ブログアプリの本文に画像を挿入する方法まとめ

2019-05-14 / PythonDjango

概要

ブログはWebアプリの基本を学ぶには良い題材です。中には自分で作ったブログを実際に使いつつ、継続的に開発している方も多いようです。このブログもその1つです。

その中でよく来る質問の1つが、記事本文へ画像を挿入する方法です。これは記事をどうやって書いているかにもよります。

HTMLで本文を書いている場合

HTMLで本文を直接書いていたり、django-summernoteのようなWYSIWYG(ウィジウィグ)エディタを使っていて、それがHTMLを自動的に作成する場合もあります。

この例は非常に単純で、HTMLで直接書いている方は、次のようにimg要素を使います。

<h2>今日の朝食</h2>
<p>今日は納豆ご飯にオリーブオイルをかけて食べました。</p>

<p>朝食の画像↓</p>
<img src="画像URL" alt="説明テキスト">

エディタを使っている場合は、画像を埋め込むための専用のボタンなりがあるので、こちらも迷うことはないでしょう。

Markdownで本文を書いている場合

例えばMarkdownで本文を書いているならば、画像を本文に埋め込むには次のように書きます。![説明テキスト](URL)という部分が画像になります。

## 今日の朝食
今日は納豆ご飯にオリーブオイルをかけて食べました。

朝食の画像↓  
![画像の説明テキスト](画像URL)

## 今日の昼食
...
...

HTMLやMarkdownで本文を書いていない場合

HTMLを直接書いている訳でもなく、Markdownで書いている訳でもない場合があります。

どういうことかというと、本文を次のように入力していて...

今日は良い天気でした。

ブログを更新しました。
https://narito.ninja/blog/

テンプレートファイルにて、次のようにしている場合です。

{{ post.text | linebreaks | urlize}}

テンプレートフィルタのlinebreaksによって、p要素やbr要素を自動で作成してくれます(p要素を作らない、linebreaksbrもあります)。また、urlizeによってhttpから始まる文字列はa要素のリンクになります。言い換えるとこの方法は、Djangoのテンプレートフィルタを前提に本文を作成している場合と言えます。

この方法は、本文をHTMLで書いたりMarkdownで書くのに比べると、非常に楽に文章が書けます。HTMLやMarkdownの構文を覚える必要もないですし、導入や使い方の把握が少し面倒なWYSIWYGエディタも使う必要がありません。

なので、Djangoでブログを作ろう的なチュートリアルではこの書き方がよく紹介されます。私のUdemyの講座も、この方法にしました。

その代わり、表現力はありません。文章の一部を太字にしたいとか、そういったことは難しくなります。画像を挿入するのも少し工夫が必要です。この本文の書き方における、画像を挿入する恐らく一番スマートな方法は、末尾がpngやjpgといったURLをimg要素へ変換するテンプレートフィルタを作成することです。

ちょっとしたサンプルということで、django.template.defaultfiltersurlizeを基に、通常のURLならa要素を、画像URLだったらimg要素にする、そんなテンプレートフィルタを作りました。

from django import template
from django.template.defaultfilters import stringfilter
from django.utils.html import *
import html
from django.utils.safestring import mark_safe

register = template.Library()


@keep_lazy_text
def _urlize2(text, trim_url_limit=None, nofollow=False, autoescape=False):
    """
    Convert any URLs in text into clickable links.
    Works on http://, https://, www. links, and also on links ending in one of
    the original seven gTLDs (.com, .edu, .gov, .int, .mil, .net, and .org).
    Links can have trailing punctuation (periods, commas, close-parens) and
    leading punctuation (opening parens) and it'll still do the right thing.
    If trim_url_limit is not None, truncate the URLs in the link text longer
    than this limit to trim_url_limit - 1 characters and append an ellipsis.
    If nofollow is True, give the links a rel="nofollow" attribute.
    If autoescape is True, autoescape the link text and URLs.
    """
    safe_input = isinstance(text, SafeData)

    def trim_url(x, limit=trim_url_limit):
        if limit is None or len(x) <= limit:
            return x
        return '%s…' % x[:max(0, limit - 1)]

    def trim_punctuation(lead, middle, trail):
        """
        Trim trailing and wrapping punctuation from `middle`. Return the items
        of the new state.
        """
        # Continue trimming until middle remains unchanged.
        trimmed_something = True
        while trimmed_something:
            trimmed_something = False
            # Trim wrapping punctuation.
            for opening, closing in WRAPPING_PUNCTUATION:
                if middle.startswith(opening):
                    middle = middle[len(opening):]
                    lead += opening
                    trimmed_something = True
                # Keep parentheses at the end only if they're balanced.
                if (middle.endswith(closing) and
                        middle.count(closing) == middle.count(opening) + 1):
                    middle = middle[:-len(closing)]
                    trail = closing + trail
                    trimmed_something = True
            # Trim trailing punctuation (after trimming wrapping punctuation,
            # as encoded entities contain ';'). Unescape entities to avoid
            # breaking them by removing ';'.
            middle_unescaped = html.unescape(middle)
            stripped = middle_unescaped.rstrip(TRAILING_PUNCTUATION_CHARS)
            if middle_unescaped != stripped:
                trail = middle[len(stripped):] + trail
                middle = middle[:len(stripped) - len(middle_unescaped)]
                trimmed_something = True
        return lead, middle, trail

    def is_email_simple(value):
        """Return True if value looks like an email address."""
        # An @ must be in the middle of the value.
        if '@' not in value or value.startswith('@') or value.endswith('@'):
            return False
        try:
            p1, p2 = value.split('@')
        except ValueError:
            # value contains more than one @.
            return False
        # Dot must be in p2 (e.g. example.com)
        if '.' not in p2 or p2.startswith('.'):
            return False
        return True

    words = word_split_re.split(str(text))
    for i, word in enumerate(words):
        if '.' in word or '@' in word or ':' in word:
            # lead: Current punctuation trimmed from the beginning of the word.
            # middle: Current state of the word.
            # trail: Current punctuation trimmed from the end of the word.
            lead, middle, trail = '', word, ''
            # Deal with punctuation.
            lead, middle, trail = trim_punctuation(lead, middle, trail)

            # Make URL we want to point to.
            url = None
            nofollow_attr = ' rel="nofollow"' if nofollow else ''
            if simple_url_re.match(middle):
                url = smart_urlquote(html.unescape(middle))
            elif simple_url_2_re.match(middle):
                url = smart_urlquote('http://%s' % html.unescape(middle))
            elif ':' not in middle and is_email_simple(middle):
                local, domain = middle.rsplit('@', 1)
                try:
                    domain = punycode(domain)
                except UnicodeError:
                    continue
                url = 'mailto:%s@%s' % (local, domain)
                nofollow_attr = ''

            # Make link.
            if url:
                trimmed = trim_url(middle)
                if autoescape and not safe_input:
                    lead, trail = escape(lead), escape(trail)
                    trimmed = escape(trimmed)
                if url.endswith(('.png', '.PNG', '.bmp', '.BMP', '.jpg', '.JPG', '.jpeg', '.JPEG', '.gif', '.GIF')):
                    middle = '<a href="%s"%s><img src="%s"></a>' % (escape(url), nofollow_attr, escape(url))
                else:
                    middle = '<a href="%s"%s>%s</a>' % (escape(url), nofollow_attr, trimmed)
                words[i] = mark_safe('%s%s%s' % (lead, middle, trail))
            else:
                if safe_input:
                    words[i] = mark_safe(word)
                elif autoescape:
                    words[i] = escape(word)
        elif safe_input:
            words[i] = mark_safe(word)
        elif autoescape:
            words[i] = escape(word)
    return ''.join(words)


@register.filter(is_safe=True, needs_autoescape=True)
@stringfilter
def urlize2(value, autoescape=True):
    return mark_safe(_urlize2(value, nofollow=True, autoescape=autoescape))

ほとんどはdjango.template.defaultfiltersurlizeそのままです。109、110行目付近で、画像だったらimg要素も作るようにしただけです。これを使えば、テンプレートファイルでは次のように書けます。

{% extends 'app/base.html' %}
{% load app %}

{% block content %}
     <h1>{{ post.title }}</h1>
    {{ post.text | linebreaks | urlize2 }}
{% endblock %}

アプリケーション直下にtemplatetagsディレクトリがあり、その中にapp.pyを作り、↑にあるコードを書きました。urlize2を利用するには、{% load app %}として、app.pyを読み込む必要があります。その後は、 {{ post.text | linebreaks | urlize2 }} のようにすれば画像URLだった場合はimg要素になり、そうでなければ通常のa要素になります。

本文に画像を埋め込むには単純で、次のような感じで書くことができます。

今日は良い天気でした。

お空が青いです。
https://narito.ninja/blog/sora.png

太陽が真っ赤です。
https://narito.ninja/blog/taiyou.jpg

画像URLをどう取得する?

幾つかのパターンごとに、本文への画像の挿入方法を説明しました。

いずれの方法を使うにしても、画像のURLが必要になることがわかりますか。画像のURLが必要ということは、画像を何処かにアップロードする必要がある訳です。それをどうするか?というのがこの記事の本題です。

例えば、WYSIWYGエディタ「summernote」をDjangoで利用するためのライブラリdjango-summernoteでは、画像ファイルをテキストエリアへドラッグアンドドロップするだけで本文に画像が挿入されます。 django-summernoteのデモ

Markdownエディタのdjango-markdownxも、同様にドラッグアンドドロップだけで画像ファイルのアップロード・本文への挿入が行われます。

ただ、WYSIWYGエディタやMarkdownエディタを使わない主義の方も多くいるはずです。他の方法もいくつか紹介していきましょう。

外部のアップローダーを利用する

もっとも単純な方法です。てきとうなアップローダーに画像をアップして、画像URLを取得し、それを本文に挿入するのです。

どのアップローダーが良いかというと、オススメはGithubです。Issueを作るページでも開きましょう。

あとは、ファイルをドラッグアンドドロップするだけです。

画像のURLを取得できました。これはMarkdown形式なので、Markdownを使わない場合はURL部分だけをコピーするなりしましょう。

アップローダーを自作する

ファイルアップロード機能を自分のDjangoアプリケーションに作るという方法もあります。

Djangoで、ファイルアップロードでは、Djangoでの様々なファイルアップロード方法を紹介しています。興味があればご覧ください。

ドラッグアンドドロップでアップロードさせる

先ほど紹介したdjango-summernoteのように、ボタンクリックやドラッグアンドドロップで、アップロードと本文への挿入ができると非常に楽ですね。

Django、ファイルアップロード付きテキストウィジェットを作るでは、テキストウィジェットにファイルをドラッグアンドドロップすると、本文にファイルURLが挿入されるような仕組みを作っていきますので、こちらも興味があればご覧ください。

通常のページ

管理画面

この記事の関連記事

Djangoで、ファイルアップロード

2018-11-26 / PythonDjango

- Djangoで、ちょっとしたファイルアップローダーを作りながら、幾つかのファイルアップロード方法についてを説明していきます

django-markdownxの紹介

2018-12-19 / PythonDjangoDjangoライブラリMarkdown

- django-markdownを使い、DjangoアプリケーションにMarkdownエディタを導入していくサンプルです。

Django、ファイルアップロード付きテキストエリアを作る

2019-07-12 / PythonDjangoDjangoカスタムウィジェット

- Djangoで、テキストウィジェットにファイルをドラッグアンドドロップすると、本文にファイルURLが挿入される処理を実装していきます。ブログ等のアプリケーションでは便利な機能です。

コメント欄

記事にコメントする

まだコメントはありません。