Use AVIF for all previews (now has good browser support). Support HDR for both images and video previews.
This commit is contained in:
parent
b1763a610e
commit
2bce21a5ab
@ -6,7 +6,6 @@ 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 av.datasets
|
||||||
import fitz # PyMuPDF
|
import fitz # PyMuPDF
|
||||||
@ -36,13 +35,13 @@ async def preview(req, path):
|
|||||||
etag = config.derived_secret(
|
etag = config.derived_secret(
|
||||||
"preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
|
"preview", rel, stat.st_mtime_ns, quality, maxsize, maxzoom
|
||||||
).hex()
|
).hex()
|
||||||
savename = PurePosixPath(path.name).with_suffix(".webp")
|
savename = PurePosixPath(path.name).with_suffix(".avif")
|
||||||
headers = {
|
headers = {
|
||||||
"etag": etag,
|
"etag": etag,
|
||||||
"last-modified": format_date_time(stat.st_mtime),
|
"last-modified": format_date_time(stat.st_mtime),
|
||||||
"cache-control": "max-age=604800, immutable"
|
"cache-control": "max-age=604800, immutable"
|
||||||
+ ("" if config.config.public else ", private"),
|
+ ("" 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())}",
|
"content-disposition": f"inline; filename*=UTF-8''{urllib.parse.quote(savename.as_posix())}",
|
||||||
}
|
}
|
||||||
if req.headers.if_none_match == etag:
|
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}")
|
logger.error(f"Error rotating preview image: {e}")
|
||||||
# Save as webp
|
# Save as webp
|
||||||
imgdata = io.BytesIO()
|
imgdata = io.BytesIO()
|
||||||
img.save(imgdata, format="webp", quality=quality, method=4)
|
img.save(imgdata, format="avif", quality=quality, method=4)
|
||||||
return imgdata.getvalue()
|
return imgdata.getvalue()
|
||||||
|
|
||||||
|
|
||||||
@ -94,48 +93,48 @@ def process_pdf(path, *, maxsize, maxzoom, quality, page_number=0):
|
|||||||
zoom = min(maxsize / w, maxsize / h, maxzoom)
|
zoom = min(maxsize / w, maxsize / h, maxzoom)
|
||||||
mat = fitz.Matrix(zoom, zoom)
|
mat = fitz.Matrix(zoom, zoom)
|
||||||
pix = page.get_pixmap(matrix=mat)
|
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):
|
def process_video(path, *, maxsize, quality):
|
||||||
with av.open(str(path)) as container:
|
frame = None
|
||||||
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))
|
|
||||||
imgdata = io.BytesIO()
|
imgdata = io.BytesIO()
|
||||||
if rot and rot != 0:
|
with (
|
||||||
img = img.rotate(-rot, expand=True) # Negative rotation for correct orientation
|
av.open(str(path)) as container,
|
||||||
img.save(imgdata, format="webp", quality=quality, method=4)
|
av.open(imgdata, "w", format="avif") as ocontainer,
|
||||||
del img
|
):
|
||||||
|
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()
|
ret = imgdata.getvalue()
|
||||||
del imgdata
|
del imgdata
|
||||||
gc.collect()
|
gc.collect()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user