Feature/image minify (#256)

* fix settings

* app info cleanup

* bottom-bar experiment

* remove dup key

* type hints

* add dependency

* updated image with query parameters

* read image options

* add image minification

* add image minification step

* alt image routes

* add image minification

* set mobile bar to top

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden
2021-04-02 21:54:46 -08:00
committed by GitHub
parent bc595d5cfa
commit 95213fa41b
31 changed files with 487 additions and 172 deletions

View File

@@ -65,8 +65,7 @@ class ExportDatabase:
f.write(content)
def export_images(self):
for file in app_dirs.IMG_DIR.iterdir():
shutil.copy(file, self.img_dir.joinpath(file.name))
shutil.copytree(app_dirs.IMG_DIR, self.img_dir, dirs_exist_ok=True)
def export_items(self, items: list[BaseModel], folder_name: str, export_list=True):
items = [x.dict() for x in items]

View File

@@ -11,6 +11,7 @@ from mealie.schema.restore import CustomPageImport, GroupImport, RecipeImport, S
from mealie.schema.settings import CustomPageOut, SiteSettings
from mealie.schema.theme import SiteTheme
from mealie.schema.user import UpdateGroup, UserInDB
from mealie.services.image import minify
from pydantic.main import BaseModel
from sqlalchemy.orm.session import Session
@@ -108,7 +109,13 @@ class ImportDatabase:
image_dir = self.import_dir.joinpath("images")
for image in image_dir.iterdir():
if image.stem in successful_imports:
shutil.copy(image, app_dirs.IMG_DIR)
if image.is_dir():
dest = app_dirs.IMG_DIR.joinpath(image.stem)
shutil.copytree(image, dest, dirs_exist_ok=True)
if image.is_file():
shutil.copy(image, app_dirs.IMG_DIR)
minify.migrate_images()
def import_themes(self):
themes_file = self.import_dir.joinpath("themes", "themes.json")

View File

View File

@@ -0,0 +1,101 @@
import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Union
import requests
from fastapi.logger import logger
from mealie.core.config import app_dirs
from mealie.services.image import minify
@dataclass
class ImageOptions:
ORIGINAL_IMAGE: str = "original*"
MINIFIED_IMAGE: str = "min-original*"
TINY_IMAGE: str = "tiny-original*"
IMG_OPTIONS = ImageOptions()
def read_image(recipe_slug: str, image_type: str = "original") -> Path:
"""returns the path to the image file for the recipe base of image_type
Args:
recipe_slug (str): Recipe Slug
image_type (str, optional): Glob Style Matcher "original*" | "min-original* | "tiny-original*"
Returns:
Path: [description]
"""
print(image_type)
recipe_slug = recipe_slug.split(".")[0] # Incase of File Name
recipe_image_dir = app_dirs.IMG_DIR.joinpath(recipe_slug)
for file in recipe_image_dir.glob(image_type):
return file
return None
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name:
try:
delete_image(recipe_slug)
except:
pass
image_dir = Path(app_dirs.IMG_DIR.joinpath(f"{recipe_slug}"))
image_dir.mkdir()
extension = extension.replace(".", "")
image_path = image_dir.joinpath(f"original.{extension}")
if isinstance(file_data, bytes):
with open(image_path, "ab") as f:
f.write(file_data)
else:
with open(image_path, "ab") as f:
shutil.copyfileobj(file_data, f)
minify.migrate_images()
return image_path
def delete_image(recipe_slug: str) -> str:
recipe_slug = recipe_slug.split(".")[0]
for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"):
return shutil.rmtree(file)
def scrape_image(image_url: str, slug: str) -> Path:
if isinstance(image_url, str): # Handles String Types
image_url = image_url
if isinstance(image_url, list): # Handles List Types
image_url = image_url[0]
if isinstance(image_url, dict): # Handles Dictionary Types
for key in image_url:
if key == "url":
image_url = image_url.get("url")
filename = slug + "." + image_url.split(".")[-1]
filename = app_dirs.IMG_DIR.joinpath(filename)
try:
r = requests.get(image_url, stream=True)
except:
logger.exception("Fatal Image Request Exception")
return None
if r.status_code == 200:
r.raw.decode_content = True
write_image(slug, r.raw, filename.suffix)
filename.unlink()
return filename
return None

View File

@@ -0,0 +1,84 @@
from pathlib import Path
from mealie.core.config import app_dirs
from PIL import Image, UnidentifiedImageError
def minify_image(image_file: Path, min_dest: Path, tiny_dest: Path):
"""Minifies an image in it's original file format. Quality is lost
Args:
my_path (Path): Source Files
min_dest (Path): FULL Destination File Path
tiny_dest (Path): FULL Destination File Path
"""
try:
img = Image.open(image_file)
basewidth = 720
wpercent = basewidth / float(img.size[0])
hsize = int((float(img.size[1]) * float(wpercent)))
img = img.resize((basewidth, hsize), Image.ANTIALIAS)
img.save(min_dest, quality=70)
tiny_image = crop_center(img)
tiny_image.save(tiny_dest, quality=70)
except UnidentifiedImageError:
pass
def crop_center(pil_img, 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 sizeof_fmt(size, decimal_places=2):
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}"
def move_all_images():
for image_file in app_dirs.IMG_DIR.iterdir():
if image_file.is_file():
if image_file.name == ".DS_Store":
continue
new_folder = app_dirs.IMG_DIR.joinpath(image_file.stem)
new_folder.mkdir(parents=True, exist_ok=True)
image_file.rename(new_folder.joinpath(f"original{image_file.suffix}"))
def migrate_images():
print("Checking for Images to Minify...")
move_all_images()
# Minify Loop
for image in app_dirs.IMG_DIR.glob("*/original.*"):
min_dest = image.parent.joinpath(f"min-original{image.suffix}")
tiny_dest = image.parent.joinpath(f"tiny-original{image.suffix}")
if min_dest.exists() and tiny_dest.exists():
continue
minify_image(image, min_dest, tiny_dest)
org_size = sizeof_fmt(image.stat().st_size)
dest_size = sizeof_fmt(min_dest.stat().st_size)
tiny_size = sizeof_fmt(tiny_dest.stat().st_size)
print(f"{image.name} Minified: {org_size} -> {dest_size} -> {tiny_size}")
print("Finished Minification Check")
if __name__ == "__main__":
migrate_images()

View File

@@ -1,63 +0,0 @@
import shutil
from pathlib import Path
import requests
from fastapi.logger import logger
from mealie.core.config import app_dirs
def read_image(recipe_slug: str) -> Path:
if app_dirs.IMG_DIR.joinpath(recipe_slug).is_file():
return app_dirs.IMG_DIR.joinpath(recipe_slug)
recipe_slug = recipe_slug.split(".")[0]
for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"):
return file
def write_image(recipe_slug: str, file_data: bytes, extension: str) -> Path.name:
delete_image(recipe_slug)
image_path = Path(app_dirs.IMG_DIR.joinpath(f"{recipe_slug}.{extension}"))
with open(image_path, "ab") as f:
f.write(file_data)
return image_path
def delete_image(recipe_slug: str) -> str:
recipe_slug = recipe_slug.split(".")[0]
for file in app_dirs.IMG_DIR.glob(f"{recipe_slug}*"):
return file.unlink()
def scrape_image(image_url: str, slug: str) -> Path:
if isinstance(image_url, str): # Handles String Types
image_url = image_url
if isinstance(image_url, list): # Handles List Types
image_url = image_url[0]
if isinstance(image_url, dict): # Handles Dictionary Types
for key in image_url:
if key == "url":
image_url = image_url.get("url")
filename = slug + "." + image_url.split(".")[-1]
filename = app_dirs.IMG_DIR.joinpath(filename)
try:
r = requests.get(image_url, stream=True)
except:
logger.exception("Fatal Image Request Exception")
return None
if r.status_code == 200:
r.raw.decode_content = True
with open(filename, "wb") as f:
shutil.copyfileobj(r.raw, f)
return filename
return None

View File

@@ -52,7 +52,7 @@ def get_todays_meal(session: Session, group: Union[int, GroupInDB]) -> Recipe:
Returns:
Recipe: Pydantic Recipe Object
"""
session = session if session else create_session()
session = session or create_session()
if isinstance(group, int):
group: GroupInDB = db.groups.get(session, group)

View File

@@ -5,7 +5,7 @@ import requests
import scrape_schema_recipe
from mealie.core.config import app_dirs
from fastapi.logger import logger
from mealie.services.image_services import scrape_image
from mealie.services.image.image import scrape_image
from mealie.schema.recipe import Recipe
from mealie.services.scraper import open_graph
from mealie.services.scraper.cleaner import Cleaner