From d0d5c4b95fb091a739c1f1d3b7e493faffb95316 Mon Sep 17 00:00:00 2001 From: Logan Endes Date: Thu, 12 Feb 2026 16:23:48 -0500 Subject: [PATCH 1/3] feat: support for m4a and mov, with some heic files supported, and nef in progress --- Dockerfile | 2 +- gallery/file_modules/__init__.py | 11 +++++++++- gallery/file_modules/heic.py | 35 ++++++++++++++++++++++++++++++++ gallery/file_modules/m4a.py | 18 ++++++++++++++++ gallery/file_modules/mov.py | 33 ++++++++++++++++++++++++++++++ gallery/file_modules/nef.py | 26 ++++++++++++++++++++++++ 6 files changed, 123 insertions(+), 2 deletions(-) create mode 100644 gallery/file_modules/heic.py create mode 100644 gallery/file_modules/m4a.py create mode 100644 gallery/file_modules/mov.py create mode 100644 gallery/file_modules/nef.py diff --git a/Dockerfile b/Dockerfile index 219a7be..adbebee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ MAINTAINER Computer Science House ENV IMAGEIO_USERDIR /var/lib/gallery RUN apt-get update && \ - apt-get install -y libldap-dev libsasl2-dev libmagic-dev ghostscript libldap-common && \ + apt-get install -y libldap-dev libsasl2-dev libmagic-dev ghostscript libldap-common imagemagick libheif1 libheif-dev libraw-dev libraw20 dcraw && \ apt-get autoremove --yes && \ apt-get clean autoclean && \ sed -i \ diff --git a/gallery/file_modules/__init__.py b/gallery/file_modules/__init__.py index b2a8030..235b541 100644 --- a/gallery/file_modules/__init__.py +++ b/gallery/file_modules/__init__.py @@ -65,6 +65,10 @@ def generate_thumbnail(self): from gallery.file_modules.txt import TXTFile from gallery.file_modules.mp3 import MP3File from gallery.file_modules.wav import WAVFile +from gallery.file_modules.heic import HEICFile +from gallery.file_modules.nef import NEFFile +from gallery.file_modules.mov import MOVFile +from gallery.file_modules.m4a import M4AFile file_mimetype_relation = { "image/jpeg": JPEGFile, @@ -76,13 +80,18 @@ def generate_thumbnail(self): "image/x-windows-bmp": BMPFile, "image/tiff": TIFFFile, "image/x-tiff": TIFFFile, + "image/heic": HEICFile, + "image/x-nikon-nef": NEFFile, "video/mp4": MP4File, "video/webm": WebMFile, "video/ogg": OggFile, + "video/quicktime": MOVFile, "application/pdf": PDFFile, "text/plain": TXTFile, "audio/mpeg": MP3File, - "audio/x-wav": WAVFile + "audio/x-wav": WAVFile, + "audio/mp4": M4AFile, + "audio/x-m4a": M4AFile, } diff --git a/gallery/file_modules/heic.py b/gallery/file_modules/heic.py new file mode 100644 index 0000000..a38d876 --- /dev/null +++ b/gallery/file_modules/heic.py @@ -0,0 +1,35 @@ +import os +from wand.image import Image +from wand.color import Color + +from gallery.file_modules import FileModule +from gallery.util import hash_file + + +class HEICFile(FileModule): + def __init__(self, file_path, dir_path): + FileModule.__init__(self, file_path, dir_path) + self.mime_type = "image/heic" + + self.generate_thumbnail() + + def generate_thumbnail(self): + self.thumbnail_uuid = hash_file(self.file_path) + ".jpg" + + with Image(filename=self.file_path) as img: + with Image(width=img.width, height=img.height, background=Color("#EEEEEE")) as bg: + # Fix orientation from EXIF + img.auto_orient() + # Force proper colorspace conversion + img.transform_colorspace('srgb') + # Remove alpha if present + img.alpha_channel = 'remove' + # Strip ICC/HDR metadata (prevents weird color shifts) + img.strip() + # Same process as other image types + bg.composite(img, 0, 0) + size = img.width if img.width < img.height else img.height + bg.crop(width=size, height=size, gravity='center') + bg.resize(256, 256) + bg.format = 'jpeg' + bg.save(filename=os.path.join(self.dir_path, self.thumbnail_uuid)) \ No newline at end of file diff --git a/gallery/file_modules/m4a.py b/gallery/file_modules/m4a.py new file mode 100644 index 0000000..155f3ff --- /dev/null +++ b/gallery/file_modules/m4a.py @@ -0,0 +1,18 @@ +import os +from wand.image import Image + +from gallery.file_modules import FileModule +from gallery.util import hash_file + +class M4AFile(FileModule): + def __init__(self, file_path, dir_path): + FileModule.__init__(self, file_path, dir_path) + self.mime_type = "audio/mp4" + + self.generate_thumbnail() + + def generate_thumbnail(self): + self.thumbnail_uuid = hash_file(self.file_path) + ".jpg" + + with Image(filename="thumbnails/reedphoto.jpg") as bg: + bg.save(filename=os.path.join(self.dir_path, self.thumbnail_uuid)) diff --git a/gallery/file_modules/mov.py b/gallery/file_modules/mov.py new file mode 100644 index 0000000..a99be5d --- /dev/null +++ b/gallery/file_modules/mov.py @@ -0,0 +1,33 @@ +from moviepy.editor import VideoFileClip +import os +from wand.image import Image +from wand.color import Color + +from gallery.file_modules import FileModule +from gallery.util import hash_file + + +class MOVFile(FileModule): + + def __init__(self, file_path, dir_path): + FileModule.__init__(self, file_path, dir_path) + self.mime_type = "video/quicktime" + + self.generate_thumbnail() + + def generate_thumbnail(self): + self.thumbnail_uuid = hash_file(self.file_path) + ".jpg" + thumbnail_loc = os.path.join(self.dir_path, self.thumbnail_uuid) + + clip = VideoFileClip(self.file_path) + time_mark = clip.duration * 0.05 + clip.save_frame(thumbnail_loc, t=time_mark) + + with Image(filename=thumbnail_loc) as img: + with img.clone() as image: + size = image.width if image.width < image.height else image.height + image.crop(width=size, height=size, gravity='center') + image.resize(256, 256) + image.background_color = Color("#EEEEEE") + image.format = 'jpeg' + image.save(filename=thumbnail_loc) diff --git a/gallery/file_modules/nef.py b/gallery/file_modules/nef.py new file mode 100644 index 0000000..6d89382 --- /dev/null +++ b/gallery/file_modules/nef.py @@ -0,0 +1,26 @@ +import os +from wand.image import Image +from wand.color import Color + +from gallery.file_modules import FileModule +from gallery.util import hash_file + + +class NEFFile(FileModule): + def __init__(self, file_path, dir_path): + FileModule.__init__(self, file_path, dir_path) + self.mime_type = "image/x-nikon-nef" + + self.generate_thumbnail() + + def generate_thumbnail(self): + self.thumbnail_uuid = hash_file(self.file_path) + ".jpg" + + with Image(filename=self.file_path) as img: + with Image(width=img.width, height=img.height, background=Color("#EEEEEE")) as bg: + bg.composite(img, 0, 0) + size = img.width if img.width < img.height else img.height + bg.crop(width=size, height=size, gravity='center') + bg.resize(256, 256) + bg.format = 'jpeg' + bg.save(filename=os.path.join(self.dir_path, self.thumbnail_uuid)) \ No newline at end of file From abbb921e36ef4ffdb2bc98f42d6180620adf537d Mon Sep 17 00:00:00 2001 From: Logan Endes Date: Thu, 12 Feb 2026 16:46:36 -0500 Subject: [PATCH 2/3] NEF and HEIC files upload correctly --- gallery/file_modules/__init__.py | 6 ++++++ gallery/file_modules/heic.py | 33 ++++++++++++++------------------ gallery/file_modules/nef.py | 23 ++++++++++++---------- requirements.txt | 5 +++++ 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/gallery/file_modules/__init__.py b/gallery/file_modules/__init__.py index 235b541..60e6bd7 100644 --- a/gallery/file_modules/__init__.py +++ b/gallery/file_modules/__init__.py @@ -98,6 +98,12 @@ def generate_thumbnail(self): # classism def parse_file_info(file_path: str, dir_path: str) -> Tuple[str, Optional[FileModule]]: print("entering parse_file_info") + ext = os.path.splitext(file_path)[-1].lower() + # .nef is a RAW file + if ext == ".nef": # .nef is a special case, magic reads it as .tiff, but it is not processed correctly by the tiff module + print("image/x-nikon-nef") + print(file_path) + return "image/x-nikon-nef", NEFFile(file_path, dir_path) mime_type = magic.from_file(file_path, mime=True) print(mime_type) print(file_path) diff --git a/gallery/file_modules/heic.py b/gallery/file_modules/heic.py index a38d876..93c2034 100644 --- a/gallery/file_modules/heic.py +++ b/gallery/file_modules/heic.py @@ -1,10 +1,11 @@ import os -from wand.image import Image -from wand.color import Color +from PIL import Image as PILImage +import pillow_heif from gallery.file_modules import FileModule from gallery.util import hash_file +pillow_heif.register_heif_opener() class HEICFile(FileModule): def __init__(self, file_path, dir_path): @@ -16,20 +17,14 @@ def __init__(self, file_path, dir_path): def generate_thumbnail(self): self.thumbnail_uuid = hash_file(self.file_path) + ".jpg" - with Image(filename=self.file_path) as img: - with Image(width=img.width, height=img.height, background=Color("#EEEEEE")) as bg: - # Fix orientation from EXIF - img.auto_orient() - # Force proper colorspace conversion - img.transform_colorspace('srgb') - # Remove alpha if present - img.alpha_channel = 'remove' - # Strip ICC/HDR metadata (prevents weird color shifts) - img.strip() - # Same process as other image types - bg.composite(img, 0, 0) - size = img.width if img.width < img.height else img.height - bg.crop(width=size, height=size, gravity='center') - bg.resize(256, 256) - bg.format = 'jpeg' - bg.save(filename=os.path.join(self.dir_path, self.thumbnail_uuid)) \ No newline at end of file + thumb_path = os.path.join(self.dir_path, self.thumbnail_uuid) + + img = PILImage.open(self.file_path).convert("RGB") + + size = min(img.width, img.height) + left = (img.width - size) // 2 + top = (img.height - size) // 2 + img = img.crop((left, top, left + size, top + size)) + + img = img.resize((256, 256)) + img.save(thumb_path, "JPEG") \ No newline at end of file diff --git a/gallery/file_modules/nef.py b/gallery/file_modules/nef.py index 6d89382..b880636 100644 --- a/gallery/file_modules/nef.py +++ b/gallery/file_modules/nef.py @@ -1,6 +1,6 @@ import os -from wand.image import Image -from wand.color import Color +import rawpy +import imageio from gallery.file_modules import FileModule from gallery.util import hash_file @@ -15,12 +15,15 @@ def __init__(self, file_path, dir_path): def generate_thumbnail(self): self.thumbnail_uuid = hash_file(self.file_path) + ".jpg" + thumb_path = os.path.join(self.dir_path, self.thumbnail_uuid) - with Image(filename=self.file_path) as img: - with Image(width=img.width, height=img.height, background=Color("#EEEEEE")) as bg: - bg.composite(img, 0, 0) - size = img.width if img.width < img.height else img.height - bg.crop(width=size, height=size, gravity='center') - bg.resize(256, 256) - bg.format = 'jpeg' - bg.save(filename=os.path.join(self.dir_path, self.thumbnail_uuid)) \ No newline at end of file + with rawpy.imread(self.file_path) as raw: + rgb = raw.postprocess(output_bps=8) + + h, w, _ = rgb.shape + size = min(h, w) + y = (h - size) // 2 + x = (w - size) // 2 + rgb = rgb[y:y+size, x:x+size] + + imageio.imwrite(thumb_path, rgb) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a5b9de1..39d2aec 100644 --- a/requirements.txt +++ b/requirements.txt @@ -70,3 +70,8 @@ Werkzeug==3.1.3 wrapt==1.17.2 xmltodict==0.14.2 zipp==3.19.1 +# NEF and HEIC support +pillow +pillow-heif +rawpy +imageio \ No newline at end of file From 140d4cdac115b9fea2827c3db637f855db48cf44 Mon Sep 17 00:00:00 2001 From: Logan Endes Date: Fri, 13 Feb 2026 13:53:19 -0500 Subject: [PATCH 3/3] Differentiate NEF from TIFF by checking if the file is parsable by rawpy instead of by extensions --- gallery/file_modules/__init__.py | 18 ++++++++++++------ requirements.txt | 8 ++++---- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/gallery/file_modules/__init__.py b/gallery/file_modules/__init__.py index 60e6bd7..a54a6e9 100644 --- a/gallery/file_modules/__init__.py +++ b/gallery/file_modules/__init__.py @@ -3,6 +3,7 @@ from typing import Any, Dict, List, Optional, Tuple from wand.image import Image from wand.color import Color +import rawpy from gallery.util import DEFAULT_THUMBNAIL_NAME from gallery.util import hash_file @@ -98,13 +99,18 @@ def generate_thumbnail(self): # classism def parse_file_info(file_path: str, dir_path: str) -> Tuple[str, Optional[FileModule]]: print("entering parse_file_info") - ext = os.path.splitext(file_path)[-1].lower() - # .nef is a RAW file - if ext == ".nef": # .nef is a special case, magic reads it as .tiff, but it is not processed correctly by the tiff module - print("image/x-nikon-nef") - print(file_path) - return "image/x-nikon-nef", NEFFile(file_path, dir_path) + mime_type = magic.from_file(file_path, mime=True) + + if "tif" in mime_type: # .nef is a special case, magic reads it as .tiff, but it is not processed correctly by the tiff module + # check if it is a raw file + try: + with rawpy.imread(file_path): # this may cause issues for other raw files + mime_type = "image/x-nikon-nef" + except rawpy.LibRawFileUnsupportedError: + pass + except rawpy.LibRawIOError: + pass print(mime_type) print(file_path) diff --git a/requirements.txt b/requirements.txt index 39d2aec..ed75835 100644 --- a/requirements.txt +++ b/requirements.txt @@ -71,7 +71,7 @@ wrapt==1.17.2 xmltodict==0.14.2 zipp==3.19.1 # NEF and HEIC support -pillow -pillow-heif -rawpy -imageio \ No newline at end of file +pillow==11.1.0 +pillow_heif==1.2.0 +rawpy==0.26.1 +imageio==2.4.0 \ No newline at end of file