mirror of
https://github.com/mealie-recipes/mealie.git
synced 2026-01-21 08:21:21 -05:00
feature: proper multi-tenant-support (#969)(WIP)
* update naming * refactor tests to use shared structure * shorten names * add tools test case * refactor to support multi-tenant * set group_id on creation * initial refactor for multitenant tags/cats * spelling * additional test case for same valued resources * fix recipe update tests * apply indexes to foreign keys * fix performance regressions * handle unknown exception * utility decorator for function debugging * migrate recipe_id to UUID * GUID for recipes * remove unused import * move image functions into package * move utilities to packages dir * update import * linter * image image and asset routes * update assets and images to use UUIDs * fix migration base * image asset test coverage * use ids for categories and tag crud functions * refactor recipe organizer test suite to reduce duplication * add uuid serlization utility * organizer base router * slug routes testing and fixes * fix postgres error * adopt UUIDs * move tags, categories, and tools under "organizers" umbrella * update composite label * generate ts types * fix import error * update frontend types * fix type errors * fix postgres errors * fix #978 * add null check for title validation * add note in docs on multi-tenancy
This commit is contained in:
7
mealie/pkgs/img/__init__.py
Normal file
7
mealie/pkgs/img/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""
|
||||
The img package is a collection of utilities for working with images. While it offers some Mealie specific functionality, libraries
|
||||
within the img package should not be tightly coupled to Mealie.
|
||||
"""
|
||||
|
||||
|
||||
from .minify import *
|
||||
130
mealie/pkgs/img/minify.py
Normal file
130
mealie/pkgs/img/minify.py
Normal file
@@ -0,0 +1,130 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from logging import Logger
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
WEBP = ".webp"
|
||||
FORMAT = "WEBP"
|
||||
|
||||
IMAGE_EXTENSIONS = {".jpg", ".jpeg", ".png", ".webp"}
|
||||
|
||||
|
||||
def get_format(image: Path) -> str:
|
||||
img = Image.open(image)
|
||||
return img.format
|
||||
|
||||
|
||||
def sizeof_fmt(file_path: Path, decimal_places=2):
|
||||
if not file_path.exists():
|
||||
return "(File Not Found)"
|
||||
size = file_path.stat().st_size
|
||||
for unit in ["B", "kB", "MB", "GB", "TB", "PB"]:
|
||||
if size < 1024.0 or unit == "PiB":
|
||||
break
|
||||
size /= 1024.0
|
||||
return f"{size:.{decimal_places}f} {unit}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MinifierOptions:
|
||||
original: bool = True
|
||||
minature: bool = True
|
||||
tiny: bool = True
|
||||
|
||||
|
||||
class ABCMinifier(ABC):
|
||||
def __init__(self, purge=False, opts: MinifierOptions = None, logger: Logger = None):
|
||||
self._purge = purge
|
||||
self._opts = opts or MinifierOptions()
|
||||
self._logger = logger or Logger("Minifier")
|
||||
|
||||
def get_image_sizes(self, org_img: Path, min_img: Path, tiny_img: Path):
|
||||
self._logger.info(
|
||||
f"{org_img.name} Minified: {sizeof_fmt(org_img)} -> {sizeof_fmt(min_img)} -> {sizeof_fmt(tiny_img)}"
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def minify(self, image: Path, force=True):
|
||||
...
|
||||
|
||||
def purge(self, image: Path):
|
||||
if not self._purge:
|
||||
return
|
||||
|
||||
for file in image.parent.glob("*.*"):
|
||||
if file.suffix != WEBP:
|
||||
file.unlink()
|
||||
|
||||
|
||||
class PillowMinifier(ABCMinifier):
|
||||
@staticmethod
|
||||
def to_webp(image_file: Path, dest: Path = None, quality: int = 100) -> Path:
|
||||
"""
|
||||
Converts an image to the webp format in-place. The original image is not
|
||||
removed By default, the quality is set to 100.
|
||||
"""
|
||||
if image_file.suffix == WEBP:
|
||||
return image_file
|
||||
|
||||
img = Image.open(image_file)
|
||||
|
||||
dest = dest or image_file.with_suffix(WEBP)
|
||||
img.save(dest, FORMAT, quality=quality)
|
||||
|
||||
return dest
|
||||
|
||||
@staticmethod
|
||||
def crop_center(pil_img: Image, crop_width=300, crop_height=300):
|
||||
img_width, img_height = pil_img.size
|
||||
return pil_img.crop(
|
||||
(
|
||||
(img_width - crop_width) // 2,
|
||||
(img_height - crop_height) // 2,
|
||||
(img_width + crop_width) // 2,
|
||||
(img_height + crop_height) // 2,
|
||||
)
|
||||
)
|
||||
|
||||
def minify(self, image_file: Path, force=True):
|
||||
if not image_file.exists():
|
||||
raise FileNotFoundError(f"{image_file.name} does not exist")
|
||||
|
||||
org_dest = image_file.parent.joinpath("original.webp")
|
||||
min_dest = image_file.parent.joinpath("min-original.webp")
|
||||
tiny_dest = image_file.parent.joinpath("tiny-original.webp")
|
||||
|
||||
if not force and min_dest.exists() and tiny_dest.exists() and org_dest.exists():
|
||||
self._logger.info(f"{image_file.name} already minified")
|
||||
return
|
||||
|
||||
success = False
|
||||
|
||||
if self._opts.original:
|
||||
if not force and org_dest.exists():
|
||||
self._logger.info(f"{image_file.name} already minified")
|
||||
else:
|
||||
PillowMinifier.to_webp(image_file, org_dest, quality=70)
|
||||
success = True
|
||||
|
||||
if self._opts.minature:
|
||||
if not force and min_dest.exists():
|
||||
self._logger.info(f"{image_file.name} already minified")
|
||||
else:
|
||||
PillowMinifier.to_webp(image_file, min_dest, quality=70)
|
||||
self._logger.info(f"{image_file.name} minified")
|
||||
success = True
|
||||
|
||||
if self._opts.tiny:
|
||||
if not force and tiny_dest.exists():
|
||||
self._logger.info(f"{image_file.name} already minified")
|
||||
else:
|
||||
img = Image.open(image_file)
|
||||
tiny_image = PillowMinifier.crop_center(img)
|
||||
tiny_image.save(tiny_dest, FORMAT, quality=70)
|
||||
self._logger.info("Tiny image saved")
|
||||
success = True
|
||||
|
||||
if self._purge and success:
|
||||
self.purge(image_file)
|
||||
Reference in New Issue
Block a user