diff --git a/cista/preview.py b/cista/preview.py index ad007b1..bc65f2a 100644 --- a/cista/preview.py +++ b/cista/preview.py @@ -6,7 +6,6 @@ import urllib.parse from pathlib import PurePosixPath from urllib.parse import unquote from wsgiref.handlers import format_date_time - import av import av.datasets import fitz # PyMuPDF @@ -36,13 +35,13 @@ async def preview(req, path): etag = config.derived_secret( "preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom ).hex() - savename = PurePosixPath(path.name).with_suffix(".webp") + savename = PurePosixPath(path.name).with_suffix(".avif") headers = { "etag": etag, "last-modified": format_date_time(stat.st_mtime), "cache-control": "max-age=604800, immutable" + ("" if config.config.public else ", private"), - "content-type": "image/webp", + "content-type": "image/avif", "content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}", } if req.headers.if_none_match == etag: @@ -83,7 +82,7 @@ def process_image(path, *, maxsize, quality): logger.error(f"Error rotating preview image: {e}") # Save as webp imgdata = io.BytesIO() - img.save(imgdata, format="webp", quality=quality, method=4) + img.save(imgdata, format="avif", quality=quality, method=4) return imgdata.getvalue() @@ -94,48 +93,48 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0): zoom = min(maxsize / w, maxsize / h, maxzoom) mat = fitz.Matrix(zoom, zoom) pix = page.get_pixmap(matrix=mat) - return pix.pil_tobytes(format="webp", quality=quality, method=4) + return pix.pil_tobytes(format="avif", quality=quality, method=4) def process_video(path, *, maxsize, quality): - with av.open(str(path)) as container: - stream = container.streams.video[0] - stream.codec_context.skip_frame = "NONKEY" - - # Updated side data access for newer av versions - rot = 0 - try: - # Try newer API first - if hasattr(stream, "side_data") and stream.side_data: - display_matrix = stream.side_data.get("DISPLAYMATRIX") - if display_matrix: - rot = ( - display_matrix.rotation - if hasattr(display_matrix, "rotation") - else 0 - ) - except (AttributeError, KeyError): - # Fallback for older API or missing side data - rot = 0 - - container.seek(container.duration // 8) - try: - frame = next(container.decode(stream)) - img = frame.to_image() - except StopIteration: - # If no frame found, try from beginning - container.seek(0) - frame = next(container.decode(stream)) - img = frame.to_image() - - del stream - - img.thumbnail((maxsize, maxsize)) + frame = None imgdata = io.BytesIO() - if rot and rot != 0: - img = img.rotate(-rot, expand=True) # Negative rotation for correct orientation - img.save(imgdata, format="webp", quality=quality, method=4) - del img + with ( + av.open(str(path)) as container, + av.open(imgdata, "w", format="avif") as ocontainer, + ): + istream = container.streams.video[0] + istream.codec_context.skip_frame = "NONKEY" + container.seek((container.duration or 0) // 8) + for frame in container.decode(istream): + if frame.dts is not None: + break + else: + raise RuntimeError("No frames found in video") + + # Resize frame to thumbnail size + if frame.width > maxsize or frame.height > maxsize: + scale_factor = min(maxsize / frame.width, maxsize / frame.height) + new_width = int(frame.width * scale_factor) + new_height = int(frame.height * scale_factor) + frame = frame.reformat(width=new_width, height=new_height) + + ostream = ocontainer.add_stream("av1", options={"quality": str(quality)}) + assert isinstance(ostream, av.VideoStream) + ostream.width = frame.width + ostream.height = frame.height + icc = istream.codec_context + occ = ostream.codec_context + + # Copy HDR metadata from input video stream + occ.color_primaries = icc.color_primaries + occ.color_trc = icc.color_trc + occ.colorspace = icc.colorspace + occ.color_range = icc.color_range + + ocontainer.mux(ostream.encode(frame)) + ocontainer.mux(ostream.encode(None)) # Flush the stream + ret = imgdata.getvalue() del imgdata gc.collect()