djangoをdockerコンテナで利用(9) – jpeg/png画像とpdfのblob列への保管

数値、文字、それらをchoices使いながら入力させたところから、次はアップロードボタンを設置してjpeg/png/pdfをテーブルのblob列に入れる。
これができたら、資料とかパンフレットとかを保管できる。

画像はjpegとpngぐらいしか自分では使わないから、jpeg保管で統一。

png画像はjpegに変換して保管。

画像は単に保管するだけでなく、検索結果画面の一覧表示のために80 x 45の縮小画像も作って保管してる。

blob列には、保管効率はあんまりやけど普通の文字列で扱えるbase64エンコードを使ってる。

こうすると重たくなることもあるけど、html内にインライン展開して表示できる。
保全策として10MBのアップロード制限している。

実測20Mbps程度のwifi環境でamazonのfireタブレットでも閲覧・登録できる。

格納した文字列に勝手にコードがついてくれて苦労した・・・。

実際の画面の動きと、djangoに書いた内容をメモ。

画面の動き

実際はログインしてから使う資産管理の画面の一部。
ログイン機能やデータベース(mariadb利用)の読み書き、入力画面の詳細は別のシリーズでやったので詳細は割愛。

モジュールも大きくなったので分割もしてるけど、追加・改善続けたらやっぱりデカくなってくなぁ。

パッケージの状態は、本当の最初の頃はdjango本体以外はこれぐらいしかなかった。

Django==3.2.7
uwsgi==2.0.19.1
django-markdownx==3.0.1
Markdown==3.3.3
Pillow==7.0.0
PyMySQL==1.0.2
matplotlib==3.4.3
numpy==1.21.2

現在pipでインストールしているモジュールは以下のとおりで、去年の年末まではdjango3、12月頃からdjangoは4にバージョンアップした。

グラフ表示やらpdf処理やら、パッケージの追加を繰り返したらいろいろ入ったな。pip3でfreeze使って出力した結果を維持してる。

asgiref==3.4.1
backports.zoneinfo==0.2.1
cycler==0.11.0
Django==4.0.1
django-markdownx==3.0.1
django-widget-tweaks==1.4.11
fonttools==4.28.5
importlib-metadata==4.10.0
kiwisolver==1.3.2
Markdown==3.3.6
matplotlib==3.5.1
numpy==1.22.0
packaging==21.3
pdf2image==1.16.0
Pillow==9.0.0
PyMySQL==1.0.2
pyparsing==3.0.6
python-dateutil==2.8.2
pytz==2021.3
six==1.16.0
sqlparse==0.4.2
supervisor==4.2.4
uWSGI==2.0.20
zipp==3.7.0

検索結果一覧画面

作った資産管理の検索画面。

リストボックスで「ハードウェア」とか「ソフトウェア」とか選んでおき、登録年度を選んでやるとその結果が表示される。

レコード1つ1つには製品名や取得金額があって、3つの添付が定義してある。

その縮小画像も列として存在させて表示させてる。

django4-blob

blob箇所だけ別テーブルにしよっかなって昔思ったことあったけど、もし分割してたらdjangoで複数テーブルを扱うのめっちゃ面倒そうなのでやらなくてよかった。

左端の「編集」を押すと編集画面に行く。

編集登録画面

一覧画面にあった縮小表示を上のほうでそれぞれ表示させてる。
テーブルに格納されてる数値や文字列より上に表示させてる。

django4-blob

ファイルのアップロード

「参照」のボタンを押すとファイルが選択できる。
jpeg/png/pdfしか選べないようにtemplates内で絞ってる。
ここでは右端の「参照」ボタンを押してファイル選んでる。

django4-blob

10MBを超えるファイルを選ぶとエラーメッセージが出るようにしてある。
django側でもできるけど、苦手克服のためjavascriptでチェックさせてみた。

django4-blob

やってないけど、「参照」でファイル選んだとき、javascript使えばファイルのプレビューとかできるらしい。

djangoのtemplatesにけっこう書かないといけないみたいやけど、そのうちやってみるかな。

ファイルを選んで参照させたら、画面の下のほうにある「保存」ボタンを押す。
すると確認ポップアップが出る。

django4-blob

この確認ポップアップもjavascript。
djangoでもできるみたいやけど、書き方がシンプルじゃなさそうだったのでやらんかった。

確認ポップアップで「OK」を押して保存すると検索結果一覧画面に戻る。

django4-blob

3番目の添付に80×45に縮めた画像が入っている。
phpではimage magick使ってたけど、djangoでも画像縮小して保存ができる。

画像の表示

編集画面と検索結果一覧画面に「元データ」ってリンクを作ってあり、クリックすると別窓でその内容が表示できる。

django4-blob

いつ見てもギャバン隊長カッコええ。
今でも尊敬してる。

pdfの表示

さっきの編集画面でpdfを登録しておき、「元データ」ってリンクをクリックすると、pdfがインライン表示できる。ただ、iosとandroidでは1ページ目しか表示できてないことに気づいた。タブレットやスマホのブラウザでpdfのインライン表示できんのかなぁ。また調べとこ。

django4-blob

去年にrtx1210の後継機出てたな。

まだ使えてるからいいけど、買い換えようかなぁ。

models.pyの構成

データベースはmariadb使ってる。

djangoで処理は作ってるけど、phpでの処理が今は本利用なので、データベースの定義を変えられない。

そのため、modelsはinspectdbして自動生成してある。
GVIS_zaikoテーブルのを全部書くと長いので画像を保存しているとこだけを抜粋。

元になっているmariadb内のsql定義

mariadb内でのテーブル定義。

長いので画像を保持しているとこを抜粋。
3つまで添付できるようにするため、同じような記述が3つ。

CREATE TABLE `GVIS_zaiko` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',

:(中略)

  `BLOB_data1` longblob DEFAULT NULL COMMENT 'BLOBデータ1',
  `BLOB_data2` longblob DEFAULT NULL COMMENT 'BLOBデータ2',
  `BLOB_data3` longblob DEFAULT NULL COMMENT 'BLOBデータ3',
  `BLOB_extent1` varchar(100) DEFAULT NULL COMMENT '添付mime1',
  `BLOB_extent2` varchar(100) DEFAULT NULL COMMENT '添付mime2',
  `BLOB_extent3` varchar(100) DEFAULT NULL COMMENT '添付mime3',
  `BLOB_medium1` mediumblob DEFAULT NULL COMMENT '添付-埋め込み画像用1',
  `BLOB_medium2` mediumblob DEFAULT NULL COMMENT '添付-埋め込み画像用2',
  `BLOB_medium3` mediumblob DEFAULT NULL COMMENT '添付-埋め込み画像用3',

:(中略)

) ENGINE=InnoDB AUTO_INCREMENT=114 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC

「BLOB_data」は画像/pdfをbase64でそのまま入れる。
「BLOB_extent」はmimeタイプを入れる。
「BLOB_medium」は画像/pdfを80×45のサイズで入れる。

画像とpdfをhtml展開して表示させるとき「image/jpeg」とか「application/pdf」って書くので、BLOB_extentの内容をdjango側で使う。

inspectさせて自動生成されたmodels.py

去年にやったとき、最初に参考にしたのはstackoverflowの英語ページやったけど、日本語で書いてる方もおられた。

作者さんありがとう。

[Django]既存のデータベースを利用する方法 - Qiita
この記事についてDjangoから既存のデータベースを利用する方法のメモです。既存システムの管理ツールをDjangoで開発するという状況を想定しています。参考:公式 レガシーなデータベースと D…

blobとしてmariadb上に定義した項目はdjangoでは小文字に変数名が変換され、TextrFieldになっている。

from django.db import models
from django.utils import timezone
from pymysql import NULL

:(中略)

    blob_data1 = models.TextField(db_column='BLOB_data1', blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.blob_data1

    blob_data2 = models.TextField(db_column='BLOB_data2', blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.blob_data2

    blob_data3 = models.TextField(db_column='BLOB_data3', blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.blob_data3

    blob_extent1 = models.CharField(db_column='BLOB_extent1', max_length=100, blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.blob_extent1

    blob_extent2 = models.CharField(db_column='BLOB_extent2', max_length=100, blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.blob_extent2

    blob_extent3 = models.CharField(db_column='BLOB_extent3', max_length=100, blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.blob_extent3

    blob_medium1 = models.TextField(db_column='BLOB_medium1', blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.blob_medium1

    blob_medium2 = models.TextField(db_column='BLOB_medium2', blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.blob_medium2

    blob_medium3 = models.TextField(db_column='BLOB_medium3', blank=True, null=True)  # Field name made lowercase.
    def __str__(self):
        return self.blob_medium3

:(中略)

    class Meta:
        managed = False
        db_table = 'GVIS_zaiko'

templateの追記

本来、djangoはmodelsとviewsとformsをうまく処理して、入力画面を勝手に作ってくれる。

自動生成を使わないとき

表を使って表現するなら、これだけでいい。

めっちゃ楽。

<table border=4 align=center>
{{ form.as_table }}
</table>

自分は画面をできるだけ好きなように作りたかったので、この方法はそのまま使わず、自分で画面レイアウトを展開させた

phpでの現行web処理をdjangoで置き換えるのが最終目的なので、レイアウトを現行のものに寄せていくようにしている。

手動展開は面倒やけど、やれなくはない。

添付ファイル1~3をアップロードするためのhtml

htmlの表要素を使ってだいたいのレイアウトを作った。

添付ファイルを処理する箇所はこんな感じ。

django4-blob

骨格はこんな感じ。
templatesの中に書いている箇所を抜粋。

<h1>資産更新</h1>
<form name="koushinform" method="post" onsubmit="return kakuninPopup()" enctype="multipart/form-data">
  {% csrf_token %}
  {{ form.non_field_errors }}

  <TABLE BORDER="0" align=center>

    <tr>

:(中略 - 添付1)

      <td valign="top">
        <table border=4 align=center width="100%"><caption>添付1</caption><tr bgcolor="#cccccc">

:(中略)

        </table>
      </td>   

:(中略 - 添付2と3)

    <tr>
      <td colspan="3" align=center> ※アップロード可能なファイルはgif/bmp/jpeg/pdf(macからの出力除く)が10MBまで</td>
    </tr>

    <tr>
      <td colspan="3" align=center>  </td>
    </tr>

:(中略)

    <tr>
      <td colspan="3" align=center>

        <table border=4 align=center>
          <tr>
            <th> 項目 </th>
            <th> 値 </th>
          </tr>

          {% for field in form %}
            <tr>

:(中略 - 各フィールドを手動展開)

            </tr>
          {% endfor %}
        </table>

        <br>
        <input class="darkmode-ignore" type="submit" value=" 保 存 ">
        <br>
        <br>
        <input class="darkmode-ignore" type="button" name="close_btn" onclick="history.back();" value=" 戻 る " >

      </td>
    </tr>

  </TABLE>   
</form>

htmlって複数行にまたがって書けないのな。
formにonsubmitとか書いたら、どうしても横に長くなってしまうのでtemplatesが見づらい。

添付を処理する箇所だけを見てみる。

django4-blob
<th width="60%">添付</th><th width="40%">更新</th></tr>
<td align=center>
{% if GvisZaiko.blob_medium1|length %}
    <a target='_blank' href="{% url 'gvisWebApp:gvis_105_GvisZaikoTENPU' GvisZaiko.pk %}?tenpu=1"> 元データ</a><br>
    <img id="blob_medium1" src="data:image/jpeg;base64,{% autoescape off %}{% gvis_rawblob GvisZaiko.blob_medium1 %}{% endautoescape %}">
{% endif %}
</td>
<td align=left>
<input type="file" name="upfile1" id="upfile1" accept="image/jpeg,image/png,application/pdf"><br>

</td>

djangoのtemplatesで書いてることは、こんな内容。

  1. テンプレートが受け取ったデータに、縮小画像の入ったblob_mediumがあれば「元データ」として縮小画像をインライン展開
  2. 「参照」ボタンを置いてjpeg/png/pdfのみを受け付ける

あとはテンプレートタグを使っているのと、参照ボタンでアップロードするファイルサイズチェックがある。

簡単に書いてるけど、表要素の中でwidth指定してる箇所は調整に悩んでる。
60%と40%って書いてはいるけど、見た目は20%と80%ぐらいになってるし、表示調整はうまくできてない。

フロントエンジニアやwebデザイナーと呼ばれる方々だったら、あっさり超えられる壁かもしれないけど、やっぱりhtml苦手。

テンプレート内での分岐

さっきのhtml内で、こんな呼び出し方をするリンクを作ってる。

href="{% url 'gvisWebApp:gvis_105_GvisZaikoTENPU' GvisZaiko.pk %}?tenpu=1"

djangoとしては、パラメータが渡るようにurls.pyに書いていてパラメータに添付で参照したい番号1~3を指定してる。

urlpatterns = [
:(中略)
    path('gvis_105_GvisZaikoTENPU/<int:pk>/', gvis_zaiko_views.GvisZaikoTENPU.as_view(), name="gvis_105_GvisZaikoTENPU"),
]

<int:pk>って書いている箇所はレコード番号だけど、その中の添付1~3のどれを表示展開させるかはurlにパラメータとして埋め込んで渡している。

urlpatternsは必須のパラメータのことを書けばいいので、ここではレコード番号は指定必須やけど、添付番号は必須じゃないから書かなくていい。

?tenpu=1って指定したパラメータはテンプレート内ではrequest.GET.tenpuとすると参照できる。

参考にさせてもらったURL。
それぞれの作者さんありがとう。

<embed> - オブジェクトの埋めこみ - とほほのWWW入門

呼び出される側の処理は以下のとおり。

{% extends 'gvisWebApp/base.html' %}
{% load static %}
{% load gvis_tags %}  {# templatetagフォルダにカスタムタグ作って使うために追記 #}

{% block content %}

  <h1>元データ 添付{{ request.GET.tenpu }}</h1>
  <br>
  <input class="darkmode-ignore" type="button" name="close_btn" onclick="window.close();" value="ウィンドウを閉じる" >
  <br>
  <br>

  {% if request.GET.tenpu|slugify == "3" %}
    {% if GvisZaiko.blob_data3|length %}
      {% if GvisZaiko.blob_extent3|stringformat:"s" == "application/pdf" %}
        <embed src="data:application/pdf;base64,{% autoescape off %}{% gvis_rawblob GvisZaiko.blob_data3 %}{% endautoescape %}" align=justify width=90% height=90%>
      {% else %}
        <img src="data:image/jpeg;base64,{% autoescape off %}{% gvis_rawblob GvisZaiko.blob_data3 %}{% endautoescape %}" align=justify width=90%>
      {% endif %}
    {% endif %}

  {% elif request.GET.tenpu|slugify == "2" %}
    {% if GvisZaiko.blob_data2|length %}
:(中略)
    {% endif %}

  {% else %}
    {% if GvisZaiko.blob_data1|length %}
:(中略)
      {% endif %}
    {% endif %}
  {% endif %}
{% endblock content %}

添付ファイル番号を受け取ったら、そのどれかで判断分岐している。
また、blob_extentにpdfかjpegかのデータタイプが入っているのも判断分岐。

この判断分岐がなかなかうまくいかず悩んだ。

以下、参考にさせてもらったURL。
作者さんありがとう。

知識の枝 テンプレート 文字列でif分岐する方法
Djangoではテンプレート(html)上でif文を使用することができます。テンプレートタグと呼ばれすDjango特有の文法を使用します。そのテンプレートタグの1つであるif文について、文字列でif分岐する方法を解説します。
404 Not Found | Read the Docs

slugifyって書いたらいいのかなって思ってたら、django-docsの中の隣の項目にstringformat:"s"ってあって、そっちを書かないとifが思ったとおりに動かない箇所もあった。

数字と文字の判断ってことかな。
djangoのテンプレート内のお作法ということで覚えとこう。

gvis_rawblobってのはカスタムテンプレートタグでbase64エンコードを処理させてる。

カスタムテンプレートタグでbase64や表示形式の変換

htmlで<img>ってのがある。<embed>ってのも類似品で自分はpdf表示に使ってる。

普通はURLを指定して表示させるのに使うけど、インライン展開したら画像やpdfを埋め込むことができる。

htmlに展開されるから、バイナリそのままにするわけにはいかないのでbase64エンコードして扱う。

base64は元のファイルよりサイズが膨らむので、あまり大きなものは扱わないほうがいい。保存ボタン押したとき「まだかなぁ」って自分が思わない程度は10MB程度。

実際にbase64でblob列に保存すると2倍近いサイズになることもあった。

blob列を取ってきてhtml展開するとき、data:image/jpeg;base64,[データ]っていうふうに書くんやけど、djangoでやってみたらそのままではうまく展開できんかった。

[データ]の先頭箇所に変な文字が入ってたので「{% autoescape off %}」をつけてみたけどうまくいかない。

テンプレートの中にはフィルタとかテンプレートタグってのがあるらしい。
htmlに展開する内容を加工してくれるみたい。

そこでカスタムテンプレートタグ作って、utf-8デコードさせてみたら表示できるようになった。

参考にさせてもらったページ。
作者さんありがとう。

【Django】カスタムテンプレートフィルタ・テンプレートタグの作り方 - DjangoBrothers
PythonをベースとしたWebフレームワーク『Django』のチュートリアルサイトです。入門から応用まで、レベル別のチュートリアルで学習することができます。徐々にレベルを上げて、実務でDjangoを使えるところを目指しましょう。ブログでは...
Pythonで文字列が数字か英字か英数字か判定・確認 | note.nkmk.me
Pythonでは、文字列str型が数字か英字か英数字かを判定し確認するための文字列メソッドがいくつか用意されている。文字列が十進数字か判定: str.isdecimal() 文字列が数字か判定: str.isdigit() 文字列が数を表す...
【Python】一行でif/elseを書く - Qiita
Pythonの基本中の基本だけれど、いっつも忘れるのでメモif a > 5: i = aelse: i = 0はi = a if a > 5 else 0みたいな感じで書けるTrue/F…
from django import template

register = template.Library() # Djangoのテンプレートタグライブラリ

@register.simple_tag
def gvis_rawblob(value1):
    if value1 is None : return ' '
    return ' ' if value1 is None else value1.decode("UTF-8")

同じようなやり方で、日付の表示形式で0000-00-00とかやりたいときにも使ってる。

@register.simple_tag
def gvis_zeropad4(value1):
    if value1 is None : return '0000'
    return '%04d' % value1 if str(value1).isdigit() else '0000'

@register.simple_tag
def gvis_zeropad2(value1):
    if value1 is None : return '00'
    return '%02d' % value1 if str(value1).isdigit() else '00'

c言語が昔に勉強して覚えた言語の1つなので、表示形式はprintfの形式指定がいちばんやりやすい。だから%02dって感じですぐ書きたくなる。

この指定方法はもう昭和なものなのかもしれんなぁ。

ファイルサイズチェック

「参照」のボタン押して選んだファイルがデカい場合、ここでは10MBを超えた場合に、エラーメッセージをポップアップしてくれるためのjavascriptを足した。

django側でやるのが正しいのかもしれんけど、やり方調べたら回りくどかった。

もっと簡単にできないのかって探してみたら、javascriptですぐできそうやった。

読み込んだ画像のプレビュー表示もできるらしいけど、テンプレートの後ろのほうに書き足す量がまぁまぁ増えたのと、すぐには必要ないなって思ったのでやめとく。

参考にさせてもらったページ。
作者さんありがとう。

input type="file"でファイルをアップロードする方法|HTMLリファレンス
HTMLのinput type="file"の使い方を詳しく解説!JavaScriptで画像のプレビューを表示したり、アップロードの上限サイズを指定する方法、ドラッグ&ドロップで画像を選択する方法などをサンプルコード付きで紹介します。
<script type="text/javascript">
:(中略)
  const sizeLimit = 1024 * 1024 * 10;

  const f1 = document.getElementById('upfile1');
  const f2 = document.getElementById('upfile2');
  const f3 = document.getElementById('upfile3');

  f1.addEventListener('change', handleFile1);
  f2.addEventListener('change', handleFile2);
  f3.addEventListener('change', handleFile3);

  function handleFile1() {
    const file = f1.files;
    if (file[0].size > sizeLimit) {
      alert('ファイルサイズは10MB以下にしてください'); // エラーメッセージを表示
      f1.value = ''; // inputの中身をリセット
      return; // 終了する
    }
  }

  function handleFile2() {
    const file = f2.files;
:(中略)
  }

  function handleFile3() {
    const file = f3.files;
:(中略)
  }
</script>

addEventListenerって記述は、ファイル選択したときの定義で「発火」って言うらしい。

英語直訳してそう言うのかもしれん。
ドカンといきそうで、なんか物騒やな。

変換処理するためのmediaフォルダ

ここまでで既存データは読めるようになった。
次はmariadbのblob列への保存のための準備。

png/jpegは80×45、pdfだったら1ページ目のみを80×45のjpegに変換して保存。

BLOB_medium列に保存するためには一時置き場が必要なんやな。

mediaフォルダと.gitignore

mediaってフォルダをプロジェクト内に作って一時的な置き場がないと、画像を縮小できんかった(探し方や書き方が悪いのかもしれんけど)。

仕方ないので、「参照」ボタンでメモリに取り込まれたファイルはいったんmediaフォルダに書きだすことにした。

django4-blob

同時に複数人数で使われる想定なら、ファイル名にユーザIDとかセッションIDみたいなのを加味する。

あとはcrontabで一定日数残ったファイルがあれば削除するとかすればいい。

自分一人しか使わないのでファイル名は適当につけた。

.gitignoreを置いて一時ファイルを除外しておき、gitlabにファイルが入っていかないようにおまじない。

アプリのsettings.pyに追記

作った一時置き場がdjangoに認識してもらえますように。

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

アプリのurls.py

これも、作った一時置き場がdjangoに認識してもらえますように。

urlpatternsに追加する。

+=って書き方ができるんやなぁ。

urlpatterns = [
    path('admin/', admin.site.urls),
    path('gvisDjango3/', include('gvisDjango3.urls')),
    path('gvisWebApp/', include('gvisWebApp.urls')),
]

urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

djangoにモジュールを追加

画像取り扱いはPillowってのが既に入ってた。

PIL(Python Image Library)ってのを使う。
たぶんグラフのサンプル作ったときに入ってくれたんかも。

ここではpdf取り扱いのためのパッケージをdjangoに追加する。

pdf2imageをpipでインストール

やり方はググればすぐ出てくる。
dockerコンテナに入って打ったコマンドラインは以下のとおり。

$ docker exec -it docker_sv_django_1 bash
# pip3 install pdf2image

pdf2imageのページを見るとwindows/mac/linuxの場合のことが書いてあった。

pdf2image
A wrapper around the pdftoppm and pdftocairo command line tools to convert PDF to a PIL Image list.
Linux

Most distros ship with pdftoppm and pdftocairo. 
If they are not installed, 
refer to your package manager to install poppler-utils

poppler-utilsが必要らしいので次にインストール。

poppler-utilsをdockerコンテナに追加

dockerコンテナに入ってaptで普通に入れる。コマンドラインは以下のとおり。

$ docker exec -it docker_sv_django_1 bash
# apt install poppler-utils

作り直したときのため、Dockerfileのapt-getしてる箇所にも忘れず足しとく。

# Install required packages and remove the apt packages cache when done.

RUN apt-get update && \
    apt-get upgrade -y && \
    apt-get install -y \
    git \
    vim \
    python3 \
    python3-dev \
    python3-setuptools \
    python3-pip \
    nginx \
    supervisor \
    fonts-ipaexfont-gothic \
    fonts-ipaexfont-mincho \
    poppler-utils \
    libmysqlclient-dev && \
   rm -rf /var/lib/apt/lists/*

RUN pip3 install --upgrade pip
RUN pip3 install -U pip setuptools

viewsの追記

資産管理する処理としては最終的には400行ぐらいある。

ページネーションしながら検索結果表示・新規登録・削除・更新させてる。
長いから抜粋して書く。

インポートする箇所

モジュールが使えるように書き足す。

import base64                               ## 画像をbase64変換して扱うため

from PIL import Image                       ## Python Image Library (PIL)で画像を編集するため
import os                                   ## テンポラリファイルを操作するため

from django.core.files.storage import FileSystemStorage
                                            ## pdfテンポラリファイルを保存するため

from pathlib import Path                    ## pdfファイルをjpegに変換するため
from pdf2image import convert_from_path

次は編集画面のクラス定義に書き足していく。

テーブルの取り扱い項目について

viewsにはmodel名の指定以外に、fieldsの指定がある。

templatesに値を渡すには全部ここに書かないといけないのかなって最初思ってたら、そうじゃない。

実際は画面を生成させる項目を指定するだけ。
他のmodel項目はテンプレートに渡る。

blob列の表示をtemplates内で手動生成するため、fieldsには書く必要がなくblobで扱う以外の、数字・文字列のカラムのみ書いている。

templatesで生成された画面で「参照」ボタンを押して指定されたファイルはメモリに取り込まれ、バリデーションがうまく行けばform_validの内容が動いてくれる。

このときファイルをbase64変換したり、pdfやjpegに対する処理を実行して保管する。

class GvisZaikoUpdate(LoginRequiredMixin, UpdateView):

    ## 参考URL  https://noumenon-th.net/programming/2019/11/19/django-updateview/
    model = GvisZaiko
    template_name = 'gvisWebApp/gvis_104_ShisanUpdate.html'
    context_object_name = "GvisZaiko"
    fields = [
        'serialshubetsu','serialseq1','serialseq2','gvis_no',
        'toroku','massho',
        'category1','category2','tehai',
        'makerurl','shanaiurl',
        'kazu','yoteitnk','shutokukng',
        'koteishisan','shokyakuhou','taiyonen','shokyakurit','jigyowariai',
        'weight','electricity','alwayson','biko',
        'ins_date','upd_date'
        ]

    ## 保存前に管理データを自動でセットする
    def form_valid(self, form):
:(中略)
    def form_invalid(self, form):
        messages.error(self.request,'入力に誤りがあります。登録に失敗しました。「戻る」で戻ってください。')
        return super().form_invalid(form)

    def get_success_url(self):
        messages.success(self.request,'登録内容を更新しました。')
        return reverse_lazy('gvisWebApp:gvis_100_ShisanKensaku')

保存前の設定

次にform_validの内容について。

保存前にレコードを編集して、更新時刻やblob列に書き込む。

本当は、この形になる前、テーブルへの保存内容(GvisZaiko)の内容を丸ごとdef定義の処理に参照渡しして保存内容を加工しようとしてた。

djangoは「参照渡し」が実際はそうではなかったので、base64エンコードした文字列を返すdef定義を作った。

def定義では、「参照」ボタンでメモリに取り込んだファイルを一時置き場に書き出したり(store_jpeg)、pdfから1ページ目だけを抽出してjpeg画像を作ったり(store_pdf)、縮小(store_thumbnail)したりしている。

骨格はこんな感じ。

## 保存前に管理データを自動でセットする
def form_valid(self, form):

    ## データベースに保存せず入力されたフォーム内容を単に取得する
    GvisZaiko = form.save(commit=False)

    ## 管理データを自動セットする
    GvisZaiko.upd_date = timezone.now()

    ## ----------------------------------------------------------------
    ## 画像をjpegにしてbase64エンコード結果を返す
    def store_jpeg(updata,tempfile):
:(中略)
    ## ----------------------------------------------------------------

    ## ----------------------------------------------------------------
    ## サムネイル画像をテンポラリファイル(80x45)に保存しbase64エンコード結果を返す
    def store_thumbnail(tempfile_in,tempfile_out):
:(中略)
    ## ----------------------------------------------------------------

    ## ----------------------------------------------------------------
    ## pdfの1ページ目をjpegにして保存しbase64エンコード結果を返す
    def store_pdf(updata,tempfile_in,tempfile_out):
:(中略)
    ## ----------------------------------------------------------------

    tempfile1 = "media/Zaiko_temp1.jpg"     ## jpeg保存用テンポラリ
    tempfile2 = "media/Zaiko_temp2.jpg"     ## サムネイルjpeg保存用テンポラリ
    tempfile3 = "Zaiko_temp3.pdf"           ## pdf保存用テンポラリ

    ## 添付1の処理
    updata1 = self.request.FILES.get('upfile1')
    if  updata1 :
:(中略)
    else:
:(中略)

    ## 添付2の処理
    updata2 = self.request.FILES.get('upfile2')
    if  updata2 :
:(中略)
    else:
:(中略)

    ## 添付3の処理
    updata3 = self.request.FILES.get('upfile3')
    if  updata3 :
:(中略)
    else:
:(中略)

    ## データベースに保存する
    GvisZaiko.save()
    return super().form_valid(form)

28~30行目でさっき作ったmediaフォルダに置く一時ファイルを指定してる。
updata1~3の指定があったら、それぞれ使いまわしてる。

今は資産管理処理のためだけに作っているけど、jpeg/pdfを保管したりファイル操作して加工するための処理は、本当は別の場所に移したいなぁ。

別のファイルに書いておいてインポートしたらできるんやろけど、今は置いとこう。

添付の処理

テンプレートで「参照」ボタンで指定されたアップロード対象のファイルを処理する。

以下は添付3のための処理。
添付1と2も同じような処理で、「updata3」と書いた処理はそれぞれ「updata1/updata2」になってる。

## 添付3の処理
updata3 = self.request.FILES.get('upfile3')
if  updata3 :
    ## 拡張子を判定する
    extension = os.path.splitext(updata3.name.lower())[1]

    if extension == '.pdf' :
        GvisZaiko.blob_extent3 = 'application/pdf'
        GvisZaiko.blob_data3 = store_pdf(updata3,tempfile3,tempfile1)
        GvisZaiko.blob_medium3 = store_thumbnail(tempfile1,tempfile2)
    else:
        GvisZaiko.blob_extent3 = 'image/jpeg'
        GvisZaiko.blob_data3 = store_jpeg(updata3,tempfile1)
        GvisZaiko.blob_medium3 = store_thumbnail(tempfile1,tempfile2)
else:
    ## base64でエンコードしてある文字列になぜか「b'」がついてしまうので外す
    GvisZaiko.blob_data3 = None if GvisZaiko.blob_data3 is None else GvisZaiko.blob_data3.decode("UTF-8")
    GvisZaiko.blob_medium3 = None if GvisZaiko.blob_medium3 is None else GvisZaiko.blob_medium3.decode("UTF-8")

2行目でファイルそのものを受け取っている。

3行目のifでファイル指定があるかないかを判断。

5行目のextensionにはファイル名の拡張子を設定している。
添え字に[1]としているけど、[0]とするとファイル名が得られるはず。

そもそもファイル指定はjpg/pdfしかできないようにしているのでextensionの判断はifとelseのみでいい。

xlsxとかpptxとか扱いたいならelifを追記したらいいから、7行目でpdfかどうか判断したら11行目でjpegの処理してる。

8~10行目と12~14行目は似たような処理で、extentにmime typeをセットし、jpeg/pdfをそれぞれbase64に変換した結果をセットしている。

ファイル指定がなくblobに何か入っているとき、17行目と18行目では元々のデータを少し加工しないといけない。

なんと「b’」って勝手に先頭2バイトついてしまうことがあった。

自分の使い方が悪いのかもしれへんし、言語の仕様とかで理由はあるんやろけど、「djangoさん、なんちゅーことしてくれるねん」ってムカついた。

逃げ道として、カスタムテンプレートタグでも書いた.decode("UTF-8")をつけてる。

store_jpegで画像変換と保存

取り込んだ画像をjpegにしてbase64エンコード結果を返す処理。

参考にさせてもらったサイト。
作者さんありがとう。

【django】画像アップロードして保存前にリサイズやサニタイズ処理を実行する方法 | Free Hero Blog
djangoで画像をアップロードした時に、可読性の観点からアップロードついでにリサイズまたはサニタイズで位置情報の削除をしてから保存をしたいのに、適切な方法が見つからない!!なんてことはありませんでしょうか?本日はその解決方法を紹介します。
def store_jpeg(updata,tempfile):
    ## 画像変換用テンポラリを指定し、ファイルがあれば削除しとく
    if os.path.isfile(tempfile) : 
        os.remove(tempfile) 

    ## メモリ上の画像ファイルを開いてjpegで保存する
    img = Image.open(updata)
    img_cnv = img.convert("RGB")
    img_cnv.save(tempfile)

    with open(tempfile,"br") as f:
        blob_data = base64.b64encode(f.read()).decode("UTF-8")

    return blob_data

updataはファイルへのパスとかじゃなくて、ファイルそのものの内容が入ってくる。

ファイル内容がpng画像でもjpeg画像でも、ファイル内容を2つ目の引数の一時ファイルへjpeg画像として書き出す。

書き出した内容からbase64変換した結果の文字列を返し、後の画像縮小でも使う。

store_pdfでjpeg化と保存

pdfの1ページ目をjpegにして保存しbase64エンコード結果を返すための処理。

参考にさせてもらったサイト。
作者さんありがとう。

Djangoでのファイルアップロード | nMoMo's
Djangoでの基本的なファイルアップロードの概念とModelFormを使用してファイルアップロードする方法を説明します。今回もGitHubにサンプルコードをアップしています。 ...
PythonでPDFを画像ファイル(JPEG、PNG)に変換する方法 - ガンマソフト
今回はPDFを画像ファイル(JPEG、PNG)にPythonで変換する方法をご紹介します。 PDFを画像ファイルに変換するには、通常は有料のAdobe® Acrobat®などのソフトを...
PDFファイルの画像ファイルへの変換 - Qiita
###はじめにPDFから画像ファイルへの変換について調べたので、その備忘録として記載します。###参考PythonでPDFを画像ファイル(JPEG、PNG)に変換する方法Install の方法…
def store_pdf(updata,tempfile_in,tempfile_out):

    ## テンポラリファイルを準備する
    temp = FileSystemStorage()
    temp.delete(tempfile_in)
    temp.delete(tempfile_out)
    pdf_file = 'media/' + temp.save(tempfile_in,updata)

    ## pdf1ページ目を解像度400でjpeg保存する
    pdfpages = convert_from_path(pdf_file,400)
    pdfpages[0].save(tempfile_out,'JPEG')

    ## pdfファイルを開いてbase
    with open(pdf_file,"br") as f:
        blob_data = base64.b64encode(f.read()).decode("UTF-8")

    return blob_data

updataはファイルへのパスとかじゃなくて、pdfファイルそのものの内容が入ってくる。

tempfile_inはupdataのpdf内容をいったん保存する。
tempfile_outはいったん保存したpdfの1ページ目を解像度400でjpeg保存する。

base64エンコードした文字列を返すのはstore_jpegと一緒。

store_thumbnailで画像縮小と保存

サムネイル画像をテンポラリファイル(80×45)に保存しbase64エンコード結果を返す。

参考にさせてもらったサイト。
作者さんありがとう。

Python, Pillowで画像を一括リサイズ(拡大・縮小) | note.nkmk.me
画像処理ライブラリPillow(PIL)のImageモジュールに、画像をリサイズ(拡大・縮小)するメソッドresize()が用意されている。ここでは、以下の内容について説明する。Image.resize()の使い方 一括で処理するコード例特...
Base64によるエンコードとデコード | Python学習講座
def store_thumbnail(tempfile_in,tempfile_out):
    ## リサイズ用テンポラリファイルがあれば削除しとく
    if os.path.isfile(tempfile_out) : 
        os.remove(tempfile_out) 

    ## メモリ上の画像ファイルを開いて縮小したものをjpegで保存する
    img = Image.open(tempfile_in)
    img_resize = img.resize((80,45),Image.NEAREST)
    img_resize.save(tempfile_out)

    with open(tempfile_out,"br") as f:
        blob_medium = base64.b64encode(f.read()).decode("UTF-8")

    return blob_medium

tempfile_inはjpegファイル、またはpdfの1ページ目から作成したjpegが指定されてくる。

tempfile_outはtempfile_inを80×45のサイズにして書き出す。

リサイズした結果をbase64エンコードして返す。

ssl側のnginxの設定

10MB制限でファイル保存したら失敗

最初はテストとしてアップロードサイズを1MBに制限してた。
普段は1MB超えるファイルは少ないし、javascriptのサイズ制限のテストするためでもあった。

10MBに増やしたら、そのままではjpeg/pdfでうまく行かずnginxのエラーが出た。

Request Entity Too Large

ググったらnginxの回避策を書いておられる方がいた。
作者さんありがとう。

Nginx での 413 Request Entity Too Large エラーの対処法 - Qiita
アップロードしようとしたデータサイズが大きいと怒られているので上限値を上げます。client_max_body_size を下記のように配置します。 http { include mime.typ…

パケットの最大値

webアプリにはパケット制限てのがある。
mariadbとかデータベースにも似たようなものがあったような気がする。

phpならpost_max_sizeとかupload_max_filesizeあたりを指定する。

djangoそのものにはないかもしれないけど、djangoのコンテナに入っているnginxの設定ファイルnginx-app.confにclient_max_body_sizeってのがある。

でも設定値は100Mってなってるので、ここでは引っかかってないはず。

自分のdjangoアプリは8080で公開したものを、さらにSSLのコンテナで拾わせてイントラ公開しているで、djangoコンテナとは別にSSL用のコンテナがある。

ssl側のnginx.confにあるclient_max_body_sizeを見ると小さかったので変えてみる。

sslコンテナの設定変更

sslコンテナの利用説明にパラメータのことが書いてあるっぽいんやけど、ビルドのときに設定しておく方法はすぐわからず。

GitHub - SteveLTN/https-portal: A fully automated HTTPS server powered by Nginx, Let's Encrypt and Docker.
A fully automated HTTPS server powered by Nginx, Let's Encrypt and Docker. - GitHub - SteveLTN/https-portal: A fully aut...

いったんコンテナにbashで入って、設定ファイルを更新して20MBに変更した。
10MBでサイズ制限していて、base64にエンコードすると2倍近くになることもあったので20MBを指定。

さらっと書いたけど、コンテナにはvi入ってないのでapt-get update ; apt-get install vimでインストールして設定ファイルを編集。

default.conf.erbとdefault.ssl.conf.erbにあるcient_max_bosy_sizeの箇所を書き換えて、sslコンテナを再起動しnginx -Tで設定が反映されているか確認した。

$ docker exec -it docker_sv_https-portal_1 bash
# cd /var/lib/nginx-conf/
/var/lib/nginx-conf# ls
default.conf.erb  default.ssl.conf.erb  nginx.conf.erb
/var/lib/nginx-conf# grep gvis *
default.conf.erb:    client_max_body_size 20M; ## for gvisapp
default.ssl.conf.erb:    client_max_body_size 20M; ## for gvisapp
/var/lib/nginx-conf#
/var/lib/nginx-conf# nginx -T | grep body
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
                      '$status $body_bytes_sent "$http_referer" '
    client_max_body_size 20M; ## for gvisapp
    client_max_body_size 20M; ## for gvisapp
/var/lib/nginx-conf#

ここまででやっとpdf/jpegをレコードに保存することができるようになり、データを画面で確認できるようになった。

設定を自動的にする

その後、「ビルドのときに設定しておく方法はすぐわからず」についてもう一回githubの中を読んでみた。

そしたら「Configure Nginx through Environment Variables」ってある。
ちゃんとそこにCLIENT_MAX_BODY_SIZEって書いてあるやん。

Dockerfileのenvironmentに書き足した。

environment:
  DOMAINS: 'nafslinux.intra.gavann-it.com -> http://svdjango:8080'
  STAGE: 'local' # or 'production'
  CLIENT_MAX_BODY_SIZE: 20M

ちゃんと反映されてるっぽいし、pdfとか入れたらちゃんとデータのアップロードできた。

root@91b06cc22e98:/var/lib/nginx-conf# nginx -T | grep body 
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
                      '$status $body_bytes_sent "$http_referer" '
    client_max_body_size 20M;
root@91b06cc22e98:/var/lib/nginx-conf# 

これでいちいち編集せんでええ。

あとで気づいたけど、minikube環境はデプロイメントの中にCLIENT_MAX_BODY_SIZEのことを書いとかなアカン。
ファイルアップロードのテストして気づいた。

      containers:
        - env:
            - name: DOMAINS
              value: gvis-mac.intra.gavann-it.com -> http://sv-django:38080
            - name: STAGE
              value: local
            - name: CLIENT_MAX_BODY_SIZE
              value: 20M

ファイル保管ではなくblobに保管する理由

ネットを探すと、よくdjangoのmediaフォルダにファイルを保管しているサンプルを見かける。

保管はそっちのほうが速度が速い。

自分の環境にもファイルパスやurlをテーブルの列に残してる箇所があるけど、サーバをcentosからubuntuに変えたりgoogle cloud使うようになったり、何度か引っ越しさせてるとファイルパスで参照できなくなった。

今ではファイルパスはただの残骸(手動で読み替えないとちゃんと開かない)。

djangoのフォルダに保管していちいちgit管理対象のフォルダに入れるのは肥大化してイヤ。

「自分はずーっとdjango使うねん」ならいいけど、10年経ってそうではないかもしれない。

もっと優れたweb開発方法が出てきてるかもしれないし、飽き性の自分がそっちに走らないわけがない。

「紙保管で資料が色褪せたり置き場に困らないように」って考え方でやってるのに、その資料が必要なときに読めなかったらツライ。

自分の環境では10MB程度のpdf保管でも10秒かかるわけでもないからなぁ。
10秒かかったとしても、やっぱりblob列にpdfや画像を保管することにしている。

業務でrailsのアプリを扱ったとき、DBじゃなくオブジェクトストレージに保管するように改良することがあった。

階層構造みたいなファイルシステムはないけど、疑似的にそういうのも扱える上、保管容量を気にせずに済むことがメリット。

高価になるので自分では使わないけど、amazonのs3とかgoogleのcloud storageみたいなオブジェクトストレージに保存するとかが今風なのかも。

タイトルとURLをコピーしました