import io 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 django.views.decorators.csrf import ensure_csrf_cookie from PIL import Image 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