import io from pathlib import Path import django.forms as forms from django.http import JsonResponse from django.shortcuts import render from django.template import loader from django.urls import reverse from django.views.decorators.csrf import ensure_csrf_cookie from PIL import Image import piexif import troggle.settings as settings from troggle.core.utils import WriteAndCommitError, write_and_commit 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 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(exif_dict) 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 @ensure_csrf_cookie def new_image_form(request, path): """Manages a form to upload new images""" directory = get_dir(path) print(f"new_image_form(): {directory=} {path=}") 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 ") print(f"new_image_form(): files: {request.FILES['file_']}") f = request.FILES["file_"] binary_data = io.BytesIO() for chunk in f.chunks(): binary_data.write(chunk) print(f"new_image_form(): binary chnks written") i = Image.open(binary_data) print(f"new_image_form(): {i=}") if "exif" in i.info: print(f"new_image_form(): exif: {i=}") exif_dict = piexif.load(i.info["exif"]) i = reorient_image(i, exif_dict) exif_dict['Exif'][41729] = b'1' # can crash here with bad exif data try: exif = piexif.dump(exif_dict) except: exif = None else: exif = None print(f"new_image_form(): No exif problems") width, height = i.size print(f"new_image_form(): {i.size=}") if width > MAX_IMAGE_WIDTH or height > MAX_IMAGE_HEIGHT: print(f"new_image_form(): rescaling ") 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) thumbnail = i.resize((int(width / tscale), int(height / tscale)), Image.LANCZOS) ib = io.BytesIO() i = i.convert('RGB') i.save(ib, format="jpeg", quality = 75) tb = io.BytesIO() thumbnail = thumbnail.convert('RGB') thumbnail.save(tb, format="jpeg", quality = 70) 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") # this is the .html file in the /l/ folder image_page = image_page_template.render( { "header": form.cleaned_data["header"], "description": form.cleaned_data["description"], "photographer": form.cleaned_data["photographer"], "year": form.cleaned_data["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", ) 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 ) return JsonResponse({"html": html_snippet}) else: print(f"new_image_form(): not POST ") form = NewWebImageForm(directory=directory) 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}) 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)"} ) ) file_ = forms.FileField() description = forms.CharField( widget=forms.Textarea(attrs={"cols": 80, "rows": 20, "placeholder": "Describe the photo (using HTML)"}) ) 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": "Descibe the change made (for git)"}) ) 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 alls 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