神社 地域 にデジタルを
❝画像のトリミング❞のコーディング
Django4 python3.10 javascript pillow wsl bootstrap crooper_js
cropper.jsを用いると、トリミングの実装ができるとわかり取り組みました。
アスペクト変更やサイズ変更といった操作を、WEB上でインタラクティブに操作できる実装に悩みましたが、以下の概要でコーディングしてほぼ意図した動きにできました。
データベースにはWEBP形式で保存し、WEBサイト上からのダウンロードはPNG形式とするように設計。
保存領域を軽量なWEBPにし、ダウンロードするユーザー側には後の扱いがしやすいPNG形式としました。
概要を記します。
事前準備(概略)
Djangoプロジェクトにアプリを作成
python manage.py startapp crop_images
<Poetryで仮想環境を準備(例)>事前にPoetryをインストールしておく
仮想環境をフォルダ直下にする
poetry config virtualenvs.in-project true
~$ mkdir o_poetry_dj
~$ cd o_poetry_dj/
~/o_poetry_dj$ poetry init
以下の設問にEnter、yes、noを入力
This command will guide you through creating your pyproject.toml config.
Package name [o_poetry_dj]:※Enterキー
Version [0.1.0]:※Enterキー
Description []:※Enterキー
Author [user , n to skip]:※Enterキー
License []:※Enterキー
Compatible Python versions [^3.10]:※Enterキー
Would you like to define your main dependencies interactively? (yes/no) [yes] no
Would you like to define your development dependencies interactively? (yes/no) [yes] no
Generated file
[tool.poetry]
name = "o_poetry_dj"
version = "0.1.0"
description = ""
authors = ["user "]
[tool.poetry.dependencies]
python = "^3.10"
[tool.poetry.dev-dependencies]
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Do you confirm generation? (yes/no) [yes] yes
poetry shell
<venvの場合(例)>※venvがインストールされていることを確認
python3 -m venv app_crop_image
source app_crop_image/bin/activate
以後、仮想環境で作業
必要なパッケージをインストール
(仮想名) poetry add pillow numpy 他
<pipの場合>
(仮想名) pip install
pillow numpy 他
スーパーユーザーを作成
python manage.py createsuperuser
settings.py(config/settings.py)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"crop_images",
...省略...
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [BASE_DIR / "templates"],
...省略...
STATIC_URL = 'static/'
STATICFILES_DIRS = [BASE_DIR / "static"]
MEDIA_URL = 'media/'
MEDIA_ROOT = BASE_DIR / 'media'
プロジェクトフォルダのsettings.pyに記述
urls.py(config/urls.py)
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path("crop_image/", include("crop_images.urls")),
...省略...
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
プロジェクトフォルダのurls.pyに記述
フォルダ構成
├── crop_images/
│ ├── admin.py
│ ├── apps.py
│ ├── forms.py
│ ├── __init__.py
│ ├── migrations/
│ ├── models.py
│ ├── __pycache__/
│ ├── tests.py
│ ├── urls.py
│ └── views.py
models.py
from django.db import models
from PIL import Image
import numpy as np
from io import BytesIO
from django.core.files.base import ContentFile
import uuid
def image_folder(instance, filename):
return 'crop_images/{}.webp'.format(uuid.uuid4().hex)
class CropImage(models.Model):
your_name = models.CharField(max_length=50, blank=True, null=True)
file = models.ImageField(upload_to=image_folder)
uploaded = models.DateTimeField(auto_now_add=True)
def __str__(self):
return str(self.pk)
def save(self, *args, **kwargs):
# open image
pil_img = Image.open(self.file)
# 画像の横幅が1500pxより大きい場合はリサイズ
if pil_img.width > 1500:
# scale_to_width(pil_img)
print("Yes")
height = round(pil_img.height * 1500 / pil_img.width)
print(height)
pil_img = pil_img.resize((1500, height))
print("Resize Ok")
# save
buffer = BytesIO()
buffer.seek(0)
pil_img.save(buffer, format="webp")
image_png = buffer.getvalue()
self.file.save(str(self.file), ContentFile(image_png), save=False)
super().save(*args, **kwargs)
models.pyには以下の要素をもたせます。
- your_nameのところは、管理サイトで検索しやすいように自分の名前などフィルタリングしやすいようにしておく。16行目
- ファイル名はuuidを用いてセキュアに。11行目
- アップロードされた画像が1500px以上の場合はリサイズ。29行目
- 拡張子をwebpにしてデータベースに保存。63行目
admin.py
管理画面で操作できるように、modelsの内容を登録
from django.contrib import admin
from .models import CropImage
from django.utils.safestring import mark_safe
import os
class CropImageAdmin(admin.ModelAdmin):
list_display = ("id", "omoto", "file", "uploaded", "custom_column", "cropimg_thumbnail")
search_fields = ("omoto", )
def custom_column(self, obj):
# return '<a href="/media/' + str(obj.file) + '" download"' + str(obj.file) + '">' +\
# os.path.basename(str(obj.file)) + '</a>'
return '<img src="/media/' + str(obj.file) + '" width="" style="">'
custom_column.short_description = "Downloadリンク生成"
def cropimg_thumbnail(self, obj):
return mark_safe('<img src="{}" style="width:100px;height:auto">'.format(obj.file.url))
admin.site.register(CropImage, CropImageAdmin)
管理画面を便利に使うため、以下のことをしています。
- custom_columnでimgタグをhtmlコードで出力(コピー・アンド・ペーストで使えるように)14行目
- photo_thumbnailは、加工した後の画像が管理画面上に一覧表示される記述。20行目
forms.py
シンプルなモデルフォームを用意します。
from django import forms
from .models import CropImage
class CropImageForm(forms.ModelForm):
class Meta:
model = CropImage
fields = ("file",)
urls.py
from django.urls import path
from .views import crop_image_main
app_name = "crop_images"
urlpatterns = [
path("", crop_image_main, name="main"),
]
views.py
from django.shortcuts import render
from .models import CropImage
from .forms import CropImageForm
from django.http import JsonResponse
from PIL import Image
import os
import io
from django.core.files.uploadedfile import InMemoryUploadedFile
def crop_image_main(request):
form = CropImageForm(request.POST or None, request.FILES or None)
if form.is_valid():
instance = form.save(commit=False)
if request.user.is_superuser and request.user.last_name == "自分の名前など":
instance.omoto = "your_name"
form.save()
return JsonResponse({"message": "works"})
context = {'form': form}
return render(request, "crop_images/main.html", context)
管理画面でのフィルタリング用に、スーパーユーザーかつ自分の名前に合致した場合に、目印ワードをつけておきます。19行目
templates/base.html
※雛形のイメージhtmlです 用途に合わせて手直してください
{% load static %}
<!doctype html>
<html lanh=ja>
<head>
<!-- Require meta tags -->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0", shrink-to-fit=none>
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
<!-- custom css & js -->
<link rel="stylesheet" href="{% static 'style.css' %}">
<script src="{% static 'main.js' %}" defer></script>
<title>Spiner | {% block title %}{% endblock title %}</title>
</head>
<body>
<div class="container mt-3">
{% block content %}
{% endblock content %}
</div>
<!-- Optional Javascript -->
<!-- jQuery first, then Popper.js, then Bootstrap JS-->
<script
src="https://code.jquery.com/jquery-3.6.3.min.js"
integrity="sha256-pvPw+upLPUjgMXY0G+8O0xUf+/Im1MZjXxxgOcBQBXU="
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.9.2/dist/umd/popper.min.js" integrity="sha384-IQsoLXl5PILFhosVNubq5LC7Qb9DXgDA9i+tQ8Zj3iwWAwPtgFTxbJ8NT4GN1R8p" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.min.js" integrity="sha384-cVKIPhGWiC2Al4u+LWgxfKTRIcfu0JTxR+EQDz/bgldoEyl4H0zUF0QKbrJ0EcQF" crossorigin="anonymous"></script>
</body>
</html>
templates/crop_images/main.html
base.htmlを継承したWEBサイト表示用のhtmlを作成します。
{% extends 'base.html' %}
{% load static %}
{% load widget_tweaks %}
{% block title %}Crop Image{% endblock title %}
{% block head_scripts %}
<script src="{% static 'js/crop_image2.js' %}" defer></script>
{% endblock head_scripts %}
{% block contents %}
<div class="row">
<div class="col">
<h3 class="mb-3">画像のトリミング</h3>
<div id="alert-box"></div>
<div id="image-box" class="mb-3"></div>
<div id="preview-box" class="mb-3"></div>
<div id="info-box" class="mb-3"></div>
<form action="" id="image-form">
{% csrf_token %}
{% comment %} {{ form.as_p }} {% endcomment %}
<label class="upload-label">
{% render_field form.file name="file" class="js-upload-file" %}画像ファイルを選択
</label>
<div class="js-upload-filename mb-3">ファイルが未選択です</div>
</form>
<div id="aspect" class="aspect not-visible">
<span>アスペクト指定</span>
<button class="btn-outline-primary btn mr-1 mb-1">16:9</button>
<button class="btn-outline-primary btn mr-1 mb-1">4:3</button>
<button class="btn-outline-primary btn mr-1 mb-1">1:1</button>
<button class="btn-outline-primary btn mr-1 mb-1">2:3</button>
<button class="btn-info btn mr-1 mb-1">Free</button>
</div>
<div id="ori_size" class="ori_size not-visible">
<span>サイズ指定</span>
<button class="btn-outline-primary btn mr-1 mb-1" type="butto">1000x666</button>
<button class="btn-outline-primary btn mr-1 mb-1" type="butto">900x386</button>
<button class="btn-outline-primary btn mr-1 mb-1" type="butto">539x363</button>
<button class="btn-outline-primary btn mr-1 mb-1" type="butto">263x189</button>
<button class="btn-outline-primary btn mr-1 mb-1" type="butto">263x363</button>
<button class="btn-outline-primary btn mr-1 mb-1" type="butto">512x512</button>
<p class="mt-2 text-danger text-center">サイズを指定し直す場合はFREEをクリック</p>
</div>
<div id="confirm-btn" class="not-visible">
<button class="btn btn-primary mt-3">実行する</button>
<button class="btn btn-danger mt-3" onclick="window.location.reload();">最初からはじめる</button>
</div>
<p id="png_dwn" class="text-center mt-5 not-visible"><a href="" id="download-link">出来上がった画像をPNGファイルでダウンロード</a></p>
</div>
</div>
<hr>
{% endblock contents %}
{% block end_scripts %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/cropper/4.1.0/cropper.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/cropper/4.1.0/cropper.css">
<script>
$(function() {
$('.js-upload-file').on('change', function () { //ファイルが選択されたら
var file = $(this).prop('files')[0]; //ファイルの情報を代入(file.name=ファイル名/file.size=ファイルサイズ/file.type=ファイルタイプ)
$('.js-upload-filename').text(file.name); //ファイル名を出力
});
});
</script>
{% endblock end_scripts %}
Javascriptで制御させる以下のことを行っています。
- メッセージを表示する領域を設ける。18行目
- 画像を表示させる領域を設ける。19行目
- トリミングした結果画像を表示させる領域を設ける。20行目
- 画像が表示されたら加工用のボタンを表示。35~60行目
- 既存のFileInputをカスタマイズ。69~76行目
既存のFileInputのカスタマイズには、以下のサイトを参考にさせていただきました。
参照:
https://techmemo.biz/css/input-file-name-clear/
static/js/crop_images2.js
console.log("hello world")
const alertBox = document.getElementById("alert-box")
const imageBox = document.getElementById("image-box")
const imageForm = document.getElementById("image-form")
// console.log(imageForm)
const confirmBtn = document.getElementById("confirm-btn")
const input = document.getElementById("id_file")
const previewBox = document.getElementById("preview-box")
const csrf = document.getElementsByName("csrfmiddlewaretoken")
const PngDwn = document.getElementById("png_dwn")
const InfoBox = document.getElementById("info-box")
const confirmAspect = document.getElementById("aspect")
const confirmSize = document.getElementById("ori_size")
var aspectRatio = document.querySelectorAll(".aspect .btn")
var OriSize = document.querySelectorAll(".ori_size .btn")
input.addEventListener("change", () => {
console.log("changed")
alertBox.innerHTML = ""
previewBox.innerHTML = ""
confirmBtn.classList.remove('not-visible')
confirmAspect.classList.remove('not-visible')
confirmSize.classList.remove('not-visible')
const img_data = input.files[0]
const url = URL.createObjectURL(img_data)
imageBox.innerHTML = `Before
`
var $image = $('#image');
$image.cropper({
dragMode : "move",
viewMode: 2, // 元画像が縮小しない
modal: false,
// background: false, // 透過部分を表示しない
scalable: false,
zoomable: true,
zoomRatio: 0.5,
ready: function() {
console.log("cropper ready")
const imageData = this.cropper.getImageData();
const ratio = imageData.width / imageData.naturalWidth;
console.log(ratio)
// set aspect ratio
aspectRatio[0].onclick = () => cropper.setAspectRatio(1.7777777777777777) // 16/9
aspectRatio[1].onclick = () => cropper.setAspectRatio(1.3333333333333333)
aspectRatio[2].onclick = () => cropper.setAspectRatio(1)
aspectRatio[3].onclick = () => cropper.setAspectRatio(0.6666666666666666)
aspectRatio[4].onclick = () => cropper.setAspectRatio(0) // free
// set specify size
OriSize[0].onclick = () => cropper.setCropBoxData({ height: 666*ratio, width: 1000*ratio })
OriSize[1].onclick = () => cropper.setCropBoxData({ height: 386*ratio, width: 900*ratio })
OriSize[2].onclick = () => cropper.setCropBoxData({ height: 363*ratio, width: 539*ratio })
OriSize[3].onclick = () => cropper.setCropBoxData({ height: 189*ratio, width: 263*ratio })
OriSize[4].onclick = () => cropper.setCropBoxData({ height: 363*ratio, width: 263*ratio })
OriSize[5].onclick = () => cropper.setCropBoxData({ height: 512*ratio, width: 512*ratio })
},
crop: function (event) {
console.log(event.detail.x);
console.log(event.detail.y);
console.log(event.detail.width);
console.log(event.detail.height);
console.log(event.detail.rotate);
console.log(event.detail.scaleX);
console.log(event.detail.scaleY);
InfoBox.innerHTML = `width:${Math.floor(event.detail.width)}px --- height:${Math.floor(event.detail.height)}px
`
}
});
// Get the Cropper.js instance after initialized
var cropper = $image.data('cropper');
confirmBtn.addEventListener("click", () => {
cropper.getCroppedCanvas().toBlob((blob) => {
PngDwn.classList.remove("not-visible")
console.log(blob)
const croppedCanvas = cropper.getCroppedCanvas();
// Downloadの処理 png
document.getElementById("download-link").addEventListener("click", ()=>{
const base64 = croppedCanvas.toDataURL("image/png")
let a = document.createElement('a');
a.href = base64;
a.download = "cropped.png";
a.click();
})
// 表示 webp
previewBox.innerHTML = `After
`
const fd = new FormData()
fd.append("csrfmiddlewaretoken", csrf[0].value)
fd.append("file", blob, "my-image.webp")
cropper.destroy()
$.ajax({
type: "POST",
url: imageForm.action,
enctype: "multipart/form-data",
data: fd,
success: function(response) {
console.log(response)
alertBox.innerHTML = `
選択した画像をトリミングしました
`
confirmAspect.classList.add('not-visible')
confirmSize.classList.add('not-visible')
},
error: function(error) {
console.log(error)
alertBox.innerHTML = `
処理が失敗しました
`
},
cache: false,
contentType: false,
processData: false,
})
})
})
})
一番悩んだところですが、ready項でアスペクトやサイズなどの変更に対処する指定のセットを記述できるようです。
Javascript側で、png形式でのダウンロードリンクを生成します。
cropper.jsについては、以下のサイトを参考にさせていただきました。
参照:
https://cly7796.net/blog/javascript/try-using-cropper-js/
https://www.memory-lovers.blog/entry/2021/04/22/110000
投稿内容 ランダム表示
❝地図とグラフで見る大阪府データ❞のコーディング
最終更新:2023年09月06日
❝画像のトリミング❞の使い方
最終更新:2023年07月14日
❝ユーチューブのダウンロード❞のコーディング
最終更新:2023年08月18日
❝グラフで見る豊中市データ❞の使い方
最終更新:2023年07月14日
python3.10 Django4 wsl bootstrap javascript
カテゴリ
私の願い
私は神社の宮司です。神社や地域を担う次世代の人々に対し、何かを残してお役に立ててもらいたいとの願いが、強く芽生えました。個業としての神社や、小規模な地域社会に、恩恵が届くのが遅くなりそうな「デジタル」の分野。門外漢として奮闘した実体験から得た経験則を、わずかずつでも残し未来につなぎたいと願うばかりです。
最近の投稿
- 簡易で安価なカメラで防犯・外出対策を
最終更新:2023年09月08日
- 神社のオリジナルTシャツを作ってみた
最終更新:2023年08月07日
- HTMLメールを活用してみた
最終更新:2023年07月14日
- 豊中市Graphの基データの説明
最終更新:2023年07月14日