Files
troggle-unchained/core/views/editor_helpers.py

315 lines
13 KiB
Python

import io
import re
from pathlib import Path
import django.forms as forms
import piexif
from django.http import JsonResponse
from django.shortcuts import render
from django.template import loader
from django.urls import reverse
from PIL import Image
import troggle.settings as settings
from troggle.core.utils import COOKIE_MAX_AGE, WriteAndCommitError, get_cookie, git_string, write_and_commit, current_expo
from .auth import login_required_if_public
MAX_IMAGE_WIDTH = 1000
MAX_IMAGE_HEIGHT = 800
THUMBNAIL_WIDTH = 200
THUMBNAIL_HEIGHT = 200
"""This is all written by Martin Green, and completely undocumented.
It uses a lot of opinionated Django default magic, none of which I am familiar with.
Philip
The function image_selector() is only called from a template file doing a popup input form,
but it stores files directly in expoweb/i /l /t instead of in
/expoweb/1623/2018-MS-01/i , /l, /t
so ALL new caves have photos in the smae place, which makes the default upload EXTREMELY CONFUSING
"""
def get_dir(path):
"From a path sent from urls.py, determine the directory."
# todo re-write this to use modern pathlib functions
if "/" in path:
return path.rsplit("/", 1)[0]
else:
return ""
def insert_gps_data(image_path, gps_data):
"""Function to insert GPS data into the resized image
"""
exif_dict = piexif.load(image_path)
exif_dict['GPS'] = gps_data
exif_bytes = piexif.dump(exif_dict)
piexif.insert(exif_bytes, image_path)
def image_selector(request, path):
"""Returns available images
called from
templates/html_editor_scripts_css.html: $('#image_popup_content').load("{% url 'image_selector' path %}" + path, function() {
"""
directory = get_dir(path)
print(f" - image selector {directory=} {path=}")
thumbnailspath = Path(settings.EXPOWEB) / directory / "t"
thumbnails = []
if thumbnailspath.is_dir():
for f in thumbnailspath.iterdir():
if f.is_file():
if directory:
base = f"{directory}/"
else:
base = ""
thumbnail_url = reverse("expopage", args=[f"{base}t/{f.name}"])
name_base = f.name.rsplit(".", 1)[0]
page_path_base = Path(settings.EXPOWEB) / directory / "l"
if (page_path_base / (f"{name_base}.htm")).is_file():
page_url = reverse("expopage", args=[f"{base}l/{name_base}.htm"])
else:
page_url = reverse("expopage", args=[f"{base}l/{name_base}.html"])
thumbnails.append({"thumbnail_url": thumbnail_url, "page_url": page_url})
return render(request, "image_selector.html", {"thumbnails": thumbnails})
def reorient_image(img, exif_dict):
if piexif.ImageIFD.Orientation in exif_dict["0th"]:
# print(f"reorient_image(): found ImageIFD.Orientation in 0th ")
# for ifd in exif_dict:
# print(f"reorient_image(): \"{ifd}\"\n {exif_dict[ifd]} ")
orientation = exif_dict["0th"].pop(piexif.ImageIFD.Orientation)
if orientation == 2:
img = img.transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 3:
img = img.rotate(180)
elif orientation == 4:
img = img.rotate(180).transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 5:
img = img.rotate(-90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 6:
img = img.rotate(-90, expand=True)
elif orientation == 7:
img = img.rotate(90, expand=True).transpose(Image.FLIP_LEFT_RIGHT)
elif orientation == 8:
img = img.rotate(90, expand=True)
return img
@login_required_if_public
def new_image_form(request, path):
"""Manages a form to upload new images
exif_dict = piexif.load(im.info["exif"])
exif_dict = {"0th":zeroth_ifd, "Exif":exif_ifd, "GPS":gps_ifd, ...more}
The "Exif.Image.NewSubfileType" tag (ID 41729) serves to identify
the type of image or subfile data contained in the image file
0: full resolution, 1: reduced resolution
"""
year = current_expo() # replace with year from photo exif if possible
THUMB_QUALITY = 70
IMAGE_QUALITY = 85
directory = get_dir(path)
print(f"new_image_form(): {directory=} {path=}")
editor = get_cookie(request)
if request.method == "POST":
# print(f"new_image_form(): POST ")
form = NewWebImageForm(request.POST, request.FILES, directory=directory)
if form.is_valid():
# print(f"new_image_form(): form is valid ")
year = form.cleaned_data["year"]
descrip = form.cleaned_data["description"]
editor = form.cleaned_data["who_are_you"]
editor = git_string(editor)
title = form.cleaned_data["header"]
f = request.FILES["file_"]
if not title:
title = f.name
binary_data = io.BytesIO()
for chunk in f.chunks():
binary_data.write(chunk)
i = Image.open(binary_data)
if "exif" in i.info:
exif_dict = piexif.load(i.info["exif"])
gps_data = exif_dict['GPS']
# print(f"new_image_form() EXIF loaded from {f.name}\n {gps_data=}")
i = reorient_image(i, exif_dict)
exif_dict['Exif'][41729] = b'1' # I am not sure this should be binary..
# can crash here with bad exif data
try:
# This is never written back into the images ?!!
exif = piexif.dump(exif_dict)
# int(f"new_image_form() After DUMP {exif=}")
except:
exif = None
if not descrip:
# date and time from exif data
descrip = f"{exif_dict['Exif'][36867].decode()} {exif_dict['Exif'][36880].decode()}"
else:
exif = None
width, height = i.size
# print(f"new_image_form(): {i.size=}")
if width > MAX_IMAGE_WIDTH or height > MAX_IMAGE_HEIGHT:
scale = max(width / MAX_IMAGE_WIDTH, height / MAX_IMAGE_HEIGHT)
print(f"new_image_form(): rescaling {scale=}")
try:
i = i.resize((int(width / scale), int(height / scale)), Image.LANCZOS)
except Exception as e:
print(f"new_image_form(): rescaling exception: {e} ")
print(f"new_image_form(): rescaled ")
tscale = max(width / THUMBNAIL_WIDTH, height / THUMBNAIL_HEIGHT)
t = i.resize((int(width / tscale), int(height / tscale)), Image.LANCZOS)
t = t.convert('RGB')
i = i.convert('RGB')
ib = io.BytesIO()
tb = io.BytesIO()
if "exif" in i.info:
exif_dict = piexif.load(i.info["exif"])
exif_dict['GPS'] = gps_data # saved from before
exif_bytes = piexif.dump(exif_dict)
i.save(ib, format='JPEG', quality = IMAGE_QUALITY, exif=exif_bytes)
exif_dict = piexif.load(t.info["exif"])
exif_dict['GPS'] = gps_data # saved from before
exif_bytes = piexif.dump(exif_dict)
t.save(tb, format='JPEG', quality = THUMB_QUALITY, exif=exif_bytes)
i.save(ib, format='JPEG', quality = IMAGE_QUALITY)
t.save(tb, format='JPEG', quality = THUMB_QUALITY)
image_rel_path, thumb_rel_path, desc_rel_path = form.get_rel_paths()
print(f"new_image_form(): \n {image_rel_path=}\n {thumb_rel_path=}\n {desc_rel_path=}")
image_page_template = loader.get_template("image_page_template.html")
image_page = image_page_template.render(
{
"header": title,
"description": descrip,
"photographer": form.cleaned_data["photographer"],
"year": year,
"filepath": f"/{image_rel_path}",
}
)
image_path, thumb_path, desc_path = form.get_full_paths()
# Create directories if required
for full_path in image_path, thumb_path, desc_path:
print(full_path, full_path.parent)
full_path.parent.mkdir(parents=True, exist_ok=True)
try:
change_message = form.cleaned_data["change_message"]
write_and_commit(
[
(desc_path, image_page, "utf-8"),
(image_path, ib.getbuffer(), False),
(thumb_path, tb.getbuffer(), False),
],
f"{change_message} - online adding of an image",
editor # this works, a new who_are_you typed on the Image form is used as the git comment
)
except WriteAndCommitError as e:
print(f"new_image_form(): WriteAndCommitError: {e.message}")
return JsonResponse({"error": e.message})
except Exception as e:
print(f"new_image_form(): EXCEPTION: {e.message}")
return JsonResponse({"error": e.message})
linked_image_template = loader.get_template("linked_image_template.html")
html_snippet = linked_image_template.render(
{"thumbnail_url": f"/{thumb_rel_path}", "page_url": f"/{desc_rel_path}"}, request
)
j_response = JsonResponse({"html": html_snippet})
j_response.set_cookie('editor_id', editor, max_age=COOKIE_MAX_AGE) # does NOT seem to work updating who_are_you cookie
return j_response
else:
# print(f"new_image_form(): not POST ")
initial={"who_are_you":editor,
"year": year, "photographer": extract_git_name(editor),
"change_message": "Uploading photo"}
form = NewWebImageForm(directory=directory, initial=initial )
# print(f"new_image_form(): POST and not POST ")
template = loader.get_template("new_image_form.html")
htmlform = template.render({"form": form, "path": path}, request)
return JsonResponse({"form": htmlform})
def extract_git_name(git_str):
pattern = r"^([^<]+)"
match = re.match(pattern, git_str)
if match:
return match.group(1).strip()
return "Anon."
class NewWebImageForm(forms.Form):
"""The form used by the editexpopage function"""
header = forms.CharField(
widget=forms.TextInput(
attrs={"size": "60", "placeholder": "Enter title (displayed as a header and in the tab)"}
), required=False
)
file_ = forms.FileField()
description = forms.CharField(
widget=forms.Textarea(attrs={"cols": 80, "rows": 20, "placeholder": "Describe the photo (using HTML)"}), required=False
)
photographer = forms.CharField(
widget=forms.TextInput(attrs={"size": "60", "placeholder": "Photographers name"}), required=False
)
year = forms.CharField(
widget=forms.TextInput(attrs={"size": "60", "placeholder": "Year photo was taken"}), required=False
)
change_message = forms.CharField(
widget=forms.Textarea(attrs={"cols": 80, "rows": 3, "placeholder": "Describe the change made (for git)"}), required=False
)
who_are_you = forms.CharField(
widget=forms.TextInput(
attrs={"size": 60, "placeholder": "You are editing this page, who are you ? e.g. 'Becka' or 'Animal <mta@gasthof.expo>'",
"style": "vertical-align: text-top;"}
)
)
def __init__(self, *args, **kwargs):
self.directory = Path(kwargs.pop("directory"))
super(forms.Form, self).__init__(*args, **kwargs)
def get_rel_paths(self):
f = self.cleaned_data["file_"]
return [
self.directory / "i" / (f.name.rsplit(".", 1)[0] + ".jpg"),
self.directory / "t" / (f.name.rsplit(".", 1)[0] + ".jpg"),
self.directory / "l" / (f.name.rsplit(".", 1)[0] + ".html"),
]
def get_full_paths(self):
return [Path(settings.EXPOWEB) / x for x in self.get_rel_paths()] # this is where we would want to insert the /caveid/ ?!
def clean_file_(self):
for rel_path, full_path in zip(self.get_rel_paths(), self.get_full_paths()):
if full_path.exists():
raise forms.ValidationError(f"File already exists in {rel_path}")
return self.cleaned_data["file_"]
class HTMLarea(forms.Textarea):
"""This is called from CaveForm in core/forms.py which is called from core/views/caves.py when editing a cave description
(and similarly for Entrance Descriptions). It is also called from core/views/expo.py when editing expo (i.e. handbook) pages.
"""
template_name = "widgets/HTMLarea.html"
def __init__(self, *args, **kwargs):
self.preview = kwargs.pop("preview", False)
super(forms.Textarea, self).__init__(*args, **kwargs)
def get_context(self, name, value, attrs):
c = super(forms.Textarea, self).get_context(name, value, attrs)
c["preview"] = self.preview
return c