Djangoでアップロードされた.aiと.psファイルのサムネイルを自動生成させる【PhotoShop,Illustrator】

ファイルをアップロードした後、ファイル名だけ表示されている状態では、それが何なのかパット見でよくわからない。

だからこそ、事前にサムネイルを用意させる。こんなふうに

だが、サムネイル画像のアップロードまでユーザーに押し付けるのは、気軽なファイル共有を前提としたウェブアプリのコンセプトが台無しになる。そこで、アップロードしたファイルのサムネイルをサーバーサイドに自動生成してもらう。
本記事ではファイルアップロード後のサムネイルの自動生成、とりわけ、PhotoShopの.psファイル、及びIllustratorの.aiファイルのサムネイルを自動生成する方法を解説する。
ソースコードはDjangoで画像及びファイルをアップロードする方法から流用する。
事前準備
まず、.psファイル及び.aiファイルを解析するため、Pythonのライブラリをインストールさせる
pip install Pillow
pip install psd-tools
.psファイルのサムネイル作成はpsd-toolsというライブラリを使用する。一方で、.aiファイルのサムネイル作成には画像編集でお馴染みのPillowだけでOK。
それぞれ、ファイルを読み込み、画像を生成するスクリプトの例をここに記す。
#Illustrator
from PIL import Image
image = Image.open("example.ai")
image.save("example.png")
#PhotoShop
from psd_tools import PSDImage
image = PSDImage.open('example.psd')
image.composite().save('example.png')
いずれも数行でサムネイル生成ができるが、フォトショップの場合はレイヤーが多いと生成に時間がかかってしまう。
リクエストのタイムアウトを考慮すれば、この処理はバッチ処理に回すべきだと思うが、本記事ではviews.pyに記す。(いずれバッチ処理を書く)
サムネイルを自動生成する処理
冒頭でも述べたとおり、ソースコードはDjangoで画像及びファイルをアップロードする方法から流用する。
まずmodels.py。サムネイルを格納するフィールドを作る。さらにクラス名、カラム名いずれも雑だったので一部修正した。
class Document(models.Model):
class Meta:
db_table = "document"
file = models.FileField(verbose_name="ファイル",upload_to="file/document/")
mime = models.TextField(verbose_name="MIMEタイプ")
thumbnail = models.ImageField(verbose_name="サムネイル",upload_to="file/thumbnail/",null=True)
mimeにpython-magicで抜き取ったMIMEタイプを格納する。これでフォトショップとイラストレーターの判定ができるようにする。
thumbnailはnullをTrueにしておくのがポイント。レコード作成時にはサムネイルは空になるので、nullを許可しないとエラーが出る。サムネイル生成に失敗した時の対策にもなる。
続いて、views.py。ユーザーからのPOSTリクエストを受け取った後に保存、その後にサムネイルを自動生成している。
from django.shortcuts import render,redirect
from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import Photo,Document
from .forms import PhotoForm,DocumentForm
from django.conf import settings
import magic
ALLOWED_MIME = [ "image/vnd.adobe.photoshop","application/postscript" ]
"""
省略
"""
class DocumentView(View):
"""
省略
"""
def post(self, request, *args, **kwargs):
#fileが指定されていない場合、後述の直接参照でインデックスエラーを防ぐためアーリーリターン
if "file" not in request.FILES:
return redirect("upload:document")
mime_type = magic.from_buffer(request.FILES["file"].read(1024) , mime=True)
print(mime_type)
#mime属性の保存(後のバッチ処理に繋げる)
copied = request.POST.copy()
copied["mime"] = mime_type
form = DocumentForm(copied,request.FILES)
if not form.is_valid():
print("バリデーションNG")
return redirect("upload:document")
if mime_type not in ALLOWED_MIME:
print("このファイルは許可されていません。")
return redirect("upload:document")
print("バリデーションOK")
result = form.save() #TIPS:←返り値がモデルクラスのオブジェクトになるので、id属性を参照すれば良い。
print(result.id)
#======ここから先、サムネイル作成処理==========
#処理結果のIDを元に、サムネイルの保存を行い、thumbnailに保存したパスを指定する
document = Document.objects.filter(id=result.id).first()
#upload_to、settings内にあるMEDIA_ROOTを読み取り、そこに画像ファイルを保存。
from django.conf import settings
path = Document.thumbnail.field.upload_to # ←予めmediaディレクトリにthumbnailディレクトリを作る
thumbnail_path = path + str(document.id) + ".png"
full_path = settings.MEDIA_ROOT + "/" + thumbnail_path
#フォトショップの場合
if document.mime == "image/vnd.adobe.photoshop":
from psd_tools import PSDImage
image = PSDImage.open(settings.MEDIA_ROOT + "/" + str(document.file))
image.composite().save(full_path)
#イラストレーターの場合
elif document.mime == "application/postscript":
from PIL import Image
image = Image.open(settings.MEDIA_ROOT + "/" + str(document.file))
image.save(full_path)
else:
return redirect("upload:document")
document.thumbnail = thumbnail_path
document.save()
return redirect("upload:document")
document = DocumentView.as_view()
form.save()するまではこれまでのファイルアップロードと同様である。ただ、注意する点はform.save()した後、返り値としてモデルオブジェクトが得られるので(result)、それを使い.id属性でDBにレコード作成したデータのidを抜き取る。
生成したサムネイルの保存先のパスと、URLから見たパスは全く異なるので、それぞれ生成する。その時、thumbnailで定義したフィールドオプションのupload_toは下記に倣って抜き取る
Document.file.field.upload_to
参照:Djangoでviews.pyからmodels.pyのフィールドオプションを参照する【verbose_name,upload_to】
他にもsettings.pyのMEDIA_ROOTを読み取る。MEDIA_ROOTは画像の実体を保存するパスだ。
パスを定義した後は、事前準備の項で解説したフォトショップ、イラストレーターの画像生成処理を元にコードを書く。
#フォトショップの場合
if document.mime == "image/vnd.adobe.photoshop":
from psd_tools import PSDImage
image = PSDImage.open(settings.MEDIA_ROOT + "/" + str(document.file))
image.composite().save(full_path)
#イラストレーターの場合
elif document.mime == "application/postscript":
from PIL import Image
image = Image.open(settings.MEDIA_ROOT + "/" + str(document.file))
image.save(full_path)
else:
return redirect("upload:document")
最後に、URLから見たパスをthumbnailに指定、保存する。upload_toに書かれた内容と、ファイル名が指定されている。
document.thumbnail = thumbnail_path
document.save()
これで、生成したサムネイルがウェブアプリ側から見れる。

結論
これだけでユーザビリティは大幅に向上するだろう。閲覧、投稿いずれも行いやすくなったのだから。
ただ、レイヤー数の多いフォトショップファイル、大容量のファイルのサムネイルを自動生成する時、処理時間が極端に長くなる(5秒から10秒ぐらい)。これではリクエストがタイムアウトになってしまう。レスポンスが遅いから、アップロードした後で閲覧することも難しいだろう。
この問題はバッチ処理に書き換えることで対処できる。サムネイルがnullになっているデータを炙り出し、バッチ処理がサムネイルを生成する。それでリクエストとサムネイル自動生成の処理を分離できる。一時的にサムネイルがnullになって表示されるが、その時はDTLでNO-IMAGE等マークがついたデフォルトのサムネイルを指定すれば良いだろう。
それから、サムネイルの画像サイズも調整するべきだと思う。現状では画像サイズが大きいので、大量に表示させようとすると遅延がひどいことになる。画像加工用ライブラリのPillowを使ってどうにかするなど、対策が必要と思われる。
ソースコード
https://github.com/seiya0723/auto_thumbnail_create
カスタムユーザーモデルも含まれている。