Fix video preview rotation and quality.

This commit is contained in:
Leo Vasanko 2025-08-15 08:25:29 -06:00
parent c47ff317c3
commit 44428eec71
2 changed files with 44 additions and 13 deletions

View File

@ -6,9 +6,11 @@ import urllib.parse
from pathlib import PurePosixPath from pathlib import PurePosixPath
from urllib.parse import unquote from urllib.parse import unquote
from wsgiref.handlers import format_date_time from wsgiref.handlers import format_date_time
import av import av
import av.datasets
import fitz # PyMuPDF import fitz # PyMuPDF
import numpy as np
import pillow_heif
from PIL import Image from PIL import Image
from sanic import Blueprint, empty, raw from sanic import Blueprint, empty, raw
from sanic.exceptions import NotFound from sanic.exceptions import NotFound
@ -16,7 +18,6 @@ from sanic.log import logger
from cista import config from cista import config
from cista.util.filename import sanitize from cista.util.filename import sanitize
import pillow_heif
pillow_heif.register_heif_opener() pillow_heif.register_heif_opener()
@ -60,7 +61,8 @@ async def preview(req, path):
def dispatch(path, quality, maxsize, maxzoom): def dispatch(path, quality, maxsize, maxzoom):
if path.suffix.lower() in (".pdf", ".xps", ".epub", ".mobi"): if path.suffix.lower() in (".pdf", ".xps", ".epub", ".mobi"):
return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom) return process_pdf(path, quality=quality, maxsize=maxsize, maxzoom=maxzoom)
if mimetypes.guess_type(path.name)[0].startswith("video/"): type, _ = mimetypes.guess_type(path.name)
if type and type.startswith("video/"):
return process_video(path, quality=quality, maxsize=maxsize) return process_video(path, quality=quality, maxsize=maxsize)
return process_image(path, quality=quality, maxsize=maxsize) return process_image(path, quality=quality, maxsize=maxsize)
@ -72,17 +74,16 @@ def process_image(path, *, maxsize, quality):
# Fix rotation based on EXIF data # Fix rotation based on EXIF data
try: try:
rotate_values = {3: 180, 6: 270, 8: 90} rotate_values = {3: 180, 6: 270, 8: 90}
orientation = img._getexif().get(274) orientation = img.getexif().get(274)
if orientation in rotate_values: if orientation in rotate_values:
logger.debug(f"Rotating preview {path} by {rotate_values[orientation]}") logger.debug(f"Rotating preview {path} by {rotate_values[orientation]}")
img = img.rotate(rotate_values[orientation], expand=True) img = img.rotate(rotate_values[orientation], expand=True)
except AttributeError:
...
except Exception as e: except Exception as e:
logger.error(f"Error rotating preview image: {e}") logger.error(f"Error rotating preview image: {e}")
# Save as webp # Save as webp
imgdata = io.BytesIO() imgdata = io.BytesIO()
img.save(imgdata, format="avif", quality=quality, method=4) print("Image quality", quality)
img.save(imgdata, format="avif", quality=quality)
return imgdata.getvalue() return imgdata.getvalue()
@ -120,7 +121,38 @@ def process_video(path, *, maxsize, quality):
new_height = int(frame.height * scale_factor) new_height = int(frame.height * scale_factor)
frame = frame.reformat(width=new_width, height=new_height) frame = frame.reformat(width=new_width, height=new_height)
ostream = ocontainer.add_stream("av1", options={"quality": str(quality)}) # Simple rotation detection and logging
if frame.rotation:
try:
fplanes = frame.to_ndarray()
# Split into Y, U, V planes of proper dimensions
planes = [
fplanes[: frame.height],
fplanes[frame.height : frame.height + frame.height // 4].reshape(
frame.height // 2, frame.width // 2
),
fplanes[frame.height + frame.height // 4 :].reshape(
frame.height // 2, frame.width // 2
),
]
# Rotate
planes = [np.rot90(p, frame.rotation // 90) for p in planes]
# Restore PyAV format
planes = np.hstack([p.flat for p in planes]).reshape(
-1, planes[0].shape[1]
)
frame = av.VideoFrame.from_ndarray(planes, format=frame.format.name)
del planes, fplanes
except Exception as e:
if "not yet supported" in str(e):
logger.warning(
f"Not rotating {path.name} preview image by {frame.rotation}°:\n PyAV: {e}"
)
else:
logger.exception(f"Error rotating video frame: {e}")
crf = str(int(63 * (1 - quality / 100) ** 2)) # Closely matching PIL quality-%
ostream = ocontainer.add_stream("av1", options={"crf": crf})
assert isinstance(ostream, av.VideoStream) assert isinstance(ostream, av.VideoStream)
ostream.width = frame.width ostream.width = frame.width
ostream.height = frame.height ostream.height = frame.height

View File

@ -22,6 +22,7 @@ dependencies = [
"inotify", "inotify",
"msgspec", "msgspec",
"natsort", "natsort",
"numpy>=2.3.2",
"pathvalidate", "pathvalidate",
"pillow", "pillow",
"pillow-heif>=1.1.0", "pillow-heif>=1.1.0",
@ -71,11 +72,9 @@ testpaths = [
"tests", "tests",
] ]
[tool.ruff.isort] [tool.ruff.lint]
known-first-party = ["cista"] isort.known-first-party = ["cista"]
per-file-ignores."tests/*" = ["S", "ANN", "D", "INP"]
[tool.ruff.per-file-ignores]
"tests/*" = ["S", "ANN", "D", "INP"]
[dependency-groups] [dependency-groups]
dev = [ dev = [