mirror of
https://github.com/9001/copyparty.git
synced 2025-08-17 09:02:15 -06:00
thumbnails: add pyvips as alt/supp. to pillow
This commit is contained in:
parent
3dd460717c
commit
b64cabc3c9
13
README.md
13
README.md
|
@ -174,7 +174,7 @@ feature summary
|
||||||
* ☑ image gallery with webm player
|
* ☑ image gallery with webm player
|
||||||
* ☑ textfile browser with syntax hilighting
|
* ☑ textfile browser with syntax hilighting
|
||||||
* ☑ [thumbnails](#thumbnails)
|
* ☑ [thumbnails](#thumbnails)
|
||||||
* ☑ ...of images using Pillow
|
* ☑ ...of images using Pillow and/or pyvips
|
||||||
* ☑ ...of videos using FFmpeg
|
* ☑ ...of videos using FFmpeg
|
||||||
* ☑ ...of audio (spectrograms) using FFmpeg
|
* ☑ ...of audio (spectrograms) using FFmpeg
|
||||||
* ☑ cache eviction (max-age; maybe max-size eventually)
|
* ☑ cache eviction (max-age; maybe max-size eventually)
|
||||||
|
@ -403,7 +403,7 @@ press `g` to toggle grid-view instead of the file listing, and `t` toggles icon
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
it does static images with Pillow and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
|
it does static images with Pillow and/or pyvips, and uses FFmpeg for video files, so you may want to `--no-thumb` or maybe just `--no-vthumb` depending on how dangerous your users are
|
||||||
|
|
||||||
audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
|
audio files are covnerted into spectrograms using FFmpeg unless you `--no-athumb` (and some FFmpeg builds may need `--th-ff-swr`)
|
||||||
|
|
||||||
|
@ -1096,10 +1096,13 @@ enable music tags:
|
||||||
* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
* or `ffprobe` (20x slower, more accurate, possibly dangerous depending on your distro and users)
|
||||||
|
|
||||||
enable [thumbnails](#thumbnails) of...
|
enable [thumbnails](#thumbnails) of...
|
||||||
* **images:** `Pillow` (requires py2.7 or py3.5+)
|
* **images:** `Pillow` and/or `pyvips` (requires py2.7 or py3.5+)
|
||||||
* **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
* **videos/audio:** `ffmpeg` and `ffprobe` somewhere in `$PATH`
|
||||||
* **HEIF pictures:** `pyheif-pillow-opener` (requires Linux or a C compiler)
|
* **HEIF pictures:** `pyvips` or `pyheif-pillow-opener` (requires Linux or a C compiler)
|
||||||
* **AVIF pictures:** `pillow-avif-plugin`
|
* **AVIF pictures:** `pyvips` or `pillow-avif-plugin`
|
||||||
|
* **JPEG XL pictures:** `pyvips`
|
||||||
|
|
||||||
|
`pyvips` gives higher quality thumbnails than `Pillow` and is 320% faster, using 270% more ram: `sudo apt install libvips42 && python3 -m pip install --user -U pyvips`
|
||||||
|
|
||||||
|
|
||||||
## install recommended deps
|
## install recommended deps
|
||||||
|
|
|
@ -500,6 +500,7 @@ def run_argparse(argv, formatter):
|
||||||
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=cores, help="num cpu cores to use for generating thumbnails")
|
ap2.add_argument("--th-mt", metavar="CORES", type=int, default=cores, help="num cpu cores to use for generating thumbnails")
|
||||||
ap2.add_argument("--th-convt", metavar="SEC", type=int, default=60, help="conversion timeout in seconds")
|
ap2.add_argument("--th-convt", metavar="SEC", type=int, default=60, help="conversion timeout in seconds")
|
||||||
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image")
|
||||||
|
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,ff", help="decoders, in order of preference")
|
||||||
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
ap2.add_argument("--th-no-jpg", action="store_true", help="disable jpg output")
|
||||||
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
ap2.add_argument("--th-no-webp", action="store_true", help="disable webp output")
|
||||||
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg for video thumbs")
|
ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg for video thumbs")
|
||||||
|
|
|
@ -17,7 +17,7 @@ from .util import Unrecv
|
||||||
from .httpcli import HttpCli
|
from .httpcli import HttpCli
|
||||||
from .u2idx import U2idx
|
from .u2idx import U2idx
|
||||||
from .th_cli import ThumbCli
|
from .th_cli import ThumbCli
|
||||||
from .th_srv import HAVE_PIL
|
from .th_srv import HAVE_PIL, HAVE_VIPS
|
||||||
from .ico import Ico
|
from .ico import Ico
|
||||||
|
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ class HttpConn(object):
|
||||||
self.cert_path = hsrv.cert_path
|
self.cert_path = hsrv.cert_path
|
||||||
self.u2fh = hsrv.u2fh
|
self.u2fh = hsrv.u2fh
|
||||||
|
|
||||||
enth = HAVE_PIL and not self.args.no_thumb
|
enth = (HAVE_PIL or HAVE_VIPS) and not self.args.no_thumb
|
||||||
self.thumbcli = ThumbCli(hsrv) if enth else None
|
self.thumbcli = ThumbCli(hsrv) if enth else None
|
||||||
self.ico = Ico(self.args)
|
self.ico = Ico(self.args)
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ from .util import mp, start_log_thrs, start_stackmon, min_ex, ansi_re
|
||||||
from .authsrv import AuthSrv
|
from .authsrv import AuthSrv
|
||||||
from .tcpsrv import TcpSrv
|
from .tcpsrv import TcpSrv
|
||||||
from .up2k import Up2k
|
from .up2k import Up2k
|
||||||
from .th_srv import ThumbSrv, HAVE_PIL, HAVE_WEBP
|
from .th_srv import ThumbSrv, HAVE_PIL, HAVE_VIPS, HAVE_WEBP
|
||||||
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
|
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,20 +78,32 @@ class SvcHub(object):
|
||||||
self.tcpsrv = TcpSrv(self)
|
self.tcpsrv = TcpSrv(self)
|
||||||
self.up2k = Up2k(self)
|
self.up2k = Up2k(self)
|
||||||
|
|
||||||
|
decs = {k: 1 for k in self.args.th_dec.split(",")}
|
||||||
|
if not HAVE_VIPS:
|
||||||
|
decs.pop("vips", None)
|
||||||
|
if not HAVE_PIL:
|
||||||
|
decs.pop("pil", None)
|
||||||
|
if not HAVE_FFMPEG or not HAVE_FFPROBE:
|
||||||
|
decs.pop("ff", None)
|
||||||
|
|
||||||
|
self.args.th_dec = list(decs.keys())
|
||||||
self.thumbsrv = None
|
self.thumbsrv = None
|
||||||
if not args.no_thumb:
|
if not args.no_thumb:
|
||||||
if HAVE_PIL:
|
m = "decoder preference: {}".format(", ".join(self.args.th_dec))
|
||||||
|
self.log("thumb", m)
|
||||||
|
if "vips" in self.args.th_dec:
|
||||||
|
self.thumbsrv = ThumbSrv(self)
|
||||||
|
elif "pil" in self.args.th_dec:
|
||||||
if not HAVE_WEBP:
|
if not HAVE_WEBP:
|
||||||
args.th_no_webp = True
|
msg = "disabling webp thumbnails because either libwebp is not available or your Pillow is too old"
|
||||||
msg = "setting --th-no-webp because either libwebp is not available or your Pillow is too old"
|
|
||||||
self.log("thumb", msg, c=3)
|
self.log("thumb", msg, c=3)
|
||||||
|
|
||||||
|
self.log("thumb", "using pillow")
|
||||||
self.thumbsrv = ThumbSrv(self)
|
self.thumbsrv = ThumbSrv(self)
|
||||||
else:
|
else:
|
||||||
msg = "need Pillow to create thumbnails; for example:\n{}{} -m pip install --user Pillow\n"
|
msg = "need Pillow and/or pyvips to create thumbnails; for example:\n{0}{1} -m pip install --user Pillow\n{0}{1} -m pip install --user pyvips\n"
|
||||||
self.log(
|
msg = msg.format(" " * 37, os.path.basename(sys.executable))
|
||||||
"thumb", msg.format(" " * 37, os.path.basename(sys.executable)), c=3
|
self.log("thumb", msg, c=3)
|
||||||
)
|
|
||||||
|
|
||||||
if not args.no_acode and args.no_thumb:
|
if not args.no_acode and args.no_thumb:
|
||||||
msg = "setting --no-acode because --no-thumb (sorry)"
|
msg = "setting --no-acode because --no-thumb (sorry)"
|
||||||
|
|
|
@ -4,7 +4,7 @@ from __future__ import print_function, unicode_literals
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from .util import Cooldown
|
from .util import Cooldown
|
||||||
from .th_srv import thumb_path, THUMBABLE, FMT_FFV, FMT_FFA
|
from .th_srv import thumb_path, THUMBABLE, FMT_FFV, FMT_FFA, HAVE_WEBP
|
||||||
from .bos import bos
|
from .bos import bos
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,6 +18,9 @@ class ThumbCli(object):
|
||||||
# cache on both sides for less broker spam
|
# cache on both sides for less broker spam
|
||||||
self.cooldown = Cooldown(self.args.th_poke)
|
self.cooldown = Cooldown(self.args.th_poke)
|
||||||
|
|
||||||
|
d = next((x for x in self.args.th_dec if x in ("vips", "pil")), None)
|
||||||
|
self.can_webp = HAVE_WEBP or d == "vips"
|
||||||
|
|
||||||
def log(self, msg, c=0):
|
def log(self, msg, c=0):
|
||||||
self.log_func("thumbcli", msg, c)
|
self.log_func("thumbcli", msg, c)
|
||||||
|
|
||||||
|
@ -42,6 +45,8 @@ class ThumbCli(object):
|
||||||
elif want_opus:
|
elif want_opus:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
is_img = not is_vid and not is_au
|
||||||
|
|
||||||
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]:
|
if rem.startswith(".hist/th/") and rem.split(".")[-1] in ["webp", "jpg"]:
|
||||||
return os.path.join(ptop, rem)
|
return os.path.join(ptop, rem)
|
||||||
|
|
||||||
|
@ -49,7 +54,11 @@ class ThumbCli(object):
|
||||||
fmt = "w"
|
fmt = "w"
|
||||||
|
|
||||||
if fmt == "w":
|
if fmt == "w":
|
||||||
if self.args.th_no_webp or ((is_vid or is_au) and self.args.th_ff_jpg):
|
if (
|
||||||
|
self.args.th_no_webp
|
||||||
|
or (is_img and not self.can_webp)
|
||||||
|
or (not is_img and self.args.th_ff_jpg)
|
||||||
|
):
|
||||||
fmt = "j"
|
fmt = "j"
|
||||||
|
|
||||||
histpath = self.asrv.vfs.histtab.get(ptop)
|
histpath = self.asrv.vfs.histtab.get(ptop)
|
||||||
|
|
|
@ -47,9 +47,18 @@ try:
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pyvips
|
||||||
|
|
||||||
|
HAVE_VIPS = True
|
||||||
|
except:
|
||||||
|
HAVE_VIPS = False
|
||||||
|
|
||||||
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
# https://pillow.readthedocs.io/en/stable/handbook/image-file-formats.html
|
||||||
|
# https://github.com/libvips/libvips
|
||||||
# ffmpeg -formats
|
# ffmpeg -formats
|
||||||
FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm"
|
FMT_PIL = "bmp dib gif icns ico jpg jpeg jp2 jpx pcx png pbm pgm ppm pnm sgi tga tif tiff webp xbm dds xpm"
|
||||||
|
FMT_VIPS = "jpg jpeg jp2 jpx jxl tif tiff png webp heic avif fit fits fts exr pdf svg hdr ppm pgm pfm gif nii dzi"
|
||||||
FMT_FFV = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 h264 avc mts h265 hevc mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv"
|
FMT_FFV = "av1 asf avi flv m4v mkv mjpeg mjpg mpg mpeg mpg2 mpeg2 h264 avc mts h265 hevc mov 3gp mp4 ts mpegts nut ogv ogm rm vob webm wmv"
|
||||||
FMT_FFA = "aac m4a ogg opus flac alac mp3 mp2 ac3 dts wma ra wav aif aiff au alaw ulaw mulaw amr gsm ape tak tta wv mpc"
|
FMT_FFA = "aac m4a ogg opus flac alac mp3 mp2 ac3 dts wma ra wav aif aiff au alaw ulaw mulaw amr gsm ape tak tta wv mpc"
|
||||||
|
|
||||||
|
@ -59,8 +68,8 @@ if HAVE_HEIF:
|
||||||
if HAVE_AVIF:
|
if HAVE_AVIF:
|
||||||
FMT_PIL += " avif avifs"
|
FMT_PIL += " avif avifs"
|
||||||
|
|
||||||
FMT_PIL, FMT_FFV, FMT_FFA = [
|
FMT_PIL, FMT_VIPS, FMT_FFV, FMT_FFA = [
|
||||||
{x: True for x in y.split(" ") if x} for y in [FMT_PIL, FMT_FFV, FMT_FFA]
|
{x: True for x in y.split(" ") if x} for y in [FMT_PIL, FMT_VIPS, FMT_FFV, FMT_FFA]
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -73,6 +82,9 @@ if HAVE_FFMPEG and HAVE_FFPROBE:
|
||||||
THUMBABLE.update(FMT_FFV)
|
THUMBABLE.update(FMT_FFV)
|
||||||
THUMBABLE.update(FMT_FFA)
|
THUMBABLE.update(FMT_FFA)
|
||||||
|
|
||||||
|
if HAVE_VIPS:
|
||||||
|
THUMBABLE.update(FMT_VIPS)
|
||||||
|
|
||||||
|
|
||||||
def thumb_path(histpath, rem, mtime, fmt):
|
def thumb_path(histpath, rem, mtime, fmt):
|
||||||
# base16 = 16 = 256
|
# base16 = 16 = 256
|
||||||
|
@ -101,6 +113,8 @@ def thumb_path(histpath, rem, mtime, fmt):
|
||||||
|
|
||||||
class ThumbSrv(object):
|
class ThumbSrv(object):
|
||||||
def __init__(self, hub):
|
def __init__(self, hub):
|
||||||
|
global THUMBABLE
|
||||||
|
|
||||||
self.hub = hub
|
self.hub = hub
|
||||||
self.asrv = hub.asrv
|
self.asrv = hub.asrv
|
||||||
self.args = hub.args
|
self.args = hub.args
|
||||||
|
@ -141,6 +155,18 @@ class ThumbSrv(object):
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
t.start()
|
t.start()
|
||||||
|
|
||||||
|
THUMBABLE = {}
|
||||||
|
|
||||||
|
if "pil" in self.args.th_dec:
|
||||||
|
THUMBABLE.update(FMT_PIL)
|
||||||
|
|
||||||
|
if "vips" in self.args.th_dec:
|
||||||
|
THUMBABLE.update(FMT_VIPS)
|
||||||
|
|
||||||
|
if "ff" in self.args.th_dec:
|
||||||
|
THUMBABLE.update(FMT_FFV)
|
||||||
|
THUMBABLE.update(FMT_FFA)
|
||||||
|
|
||||||
def log(self, msg, c=0):
|
def log(self, msg, c=0):
|
||||||
self.log_func("thumb", msg, c)
|
self.log_func("thumb", msg, c)
|
||||||
|
|
||||||
|
@ -211,11 +237,16 @@ class ThumbSrv(object):
|
||||||
ext = abspath.split(".")[-1].lower()
|
ext = abspath.split(".")[-1].lower()
|
||||||
fun = None
|
fun = None
|
||||||
if not bos.path.exists(tpath):
|
if not bos.path.exists(tpath):
|
||||||
if ext in FMT_PIL:
|
for lib in self.args.th_dec:
|
||||||
|
if fun:
|
||||||
|
break
|
||||||
|
elif lib == "pil" and ext in FMT_PIL:
|
||||||
fun = self.conv_pil
|
fun = self.conv_pil
|
||||||
elif ext in FMT_FFV:
|
elif lib == "vips" and ext in FMT_VIPS:
|
||||||
|
fun = self.conv_vips
|
||||||
|
elif lib == "ff" and ext in FMT_FFV:
|
||||||
fun = self.conv_ffmpeg
|
fun = self.conv_ffmpeg
|
||||||
elif ext in FMT_FFA:
|
elif lib == "ff" and ext in FMT_FFA:
|
||||||
if tpath.endswith(".opus") or tpath.endswith(".caf"):
|
if tpath.endswith(".opus") or tpath.endswith(".caf"):
|
||||||
fun = self.conv_opus
|
fun = self.conv_opus
|
||||||
else:
|
else:
|
||||||
|
@ -296,6 +327,24 @@ class ThumbSrv(object):
|
||||||
|
|
||||||
im.save(tpath, **args)
|
im.save(tpath, **args)
|
||||||
|
|
||||||
|
def conv_vips(self, abspath, tpath):
|
||||||
|
crops = ["centre", "none"]
|
||||||
|
if self.args.th_no_crop:
|
||||||
|
crops = ["none"]
|
||||||
|
|
||||||
|
w, h = self.res
|
||||||
|
kw = {"height": h, "size": "down", "intent": "relative"}
|
||||||
|
|
||||||
|
for c in crops:
|
||||||
|
try:
|
||||||
|
kw["crop"] = c
|
||||||
|
img = pyvips.Image.thumbnail(abspath, w, **kw)
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
img.write_to_file(tpath, Q=40)
|
||||||
|
|
||||||
def conv_ffmpeg(self, abspath, tpath):
|
def conv_ffmpeg(self, abspath, tpath):
|
||||||
ret, _ = ffprobe(abspath)
|
ret, _ = ffprobe(abspath)
|
||||||
|
|
||||||
|
@ -350,10 +399,23 @@ class ThumbSrv(object):
|
||||||
def _run_ff(self, cmd):
|
def _run_ff(self, cmd):
|
||||||
# self.log((b" ".join(cmd)).decode("utf-8"))
|
# self.log((b" ".join(cmd)).decode("utf-8"))
|
||||||
ret, sout, serr = runcmd(cmd, timeout=self.args.th_convt)
|
ret, sout, serr = runcmd(cmd, timeout=self.args.th_convt)
|
||||||
if ret != 0:
|
if not ret:
|
||||||
|
return
|
||||||
|
|
||||||
|
c = "1;30"
|
||||||
m = "FFmpeg failed (probably a corrupt video file):\n"
|
m = "FFmpeg failed (probably a corrupt video file):\n"
|
||||||
|
if cmd[-1].endswith(b".webp") and (
|
||||||
|
"Error selecting an encoder" in serr
|
||||||
|
or "Automatic encoder selection failed" in serr
|
||||||
|
or "Default encoder for format webp" in serr
|
||||||
|
or "Please choose an encoder manually" in serr
|
||||||
|
):
|
||||||
|
self.args.th_ff_jpg = True
|
||||||
|
m = "FFmpeg failed because it was compiled without libwebp; enabling --th_ff_jpg to force jpeg output:\n"
|
||||||
|
c = 1
|
||||||
|
|
||||||
m += "\n".join(["ff: {}".format(x) for x in serr.split("\n")])
|
m += "\n".join(["ff: {}".format(x) for x in serr.split("\n")])
|
||||||
self.log(m, c="1;30")
|
self.log(m, c=c)
|
||||||
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
|
||||||
|
|
||||||
def conv_spec(self, abspath, tpath):
|
def conv_spec(self, abspath, tpath):
|
||||||
|
|
3
setup.py
3
setup.py
|
@ -114,9 +114,10 @@ args = {
|
||||||
"install_requires": ["jinja2"],
|
"install_requires": ["jinja2"],
|
||||||
"extras_require": {
|
"extras_require": {
|
||||||
"thumbnails": ["Pillow"],
|
"thumbnails": ["Pillow"],
|
||||||
|
"thumbnails2": ["pyvips"],
|
||||||
"audiotags": ["mutagen"],
|
"audiotags": ["mutagen"],
|
||||||
"ftpd": ["pyftpdlib"],
|
"ftpd": ["pyftpdlib"],
|
||||||
"ftps": ["pyopenssl"],
|
"ftps": ["pyftpdlib", "pyopenssl"],
|
||||||
},
|
},
|
||||||
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
|
"entry_points": {"console_scripts": ["copyparty = copyparty.__main__:main"]},
|
||||||
"scripts": ["bin/copyparty-fuse.py", "bin/up2k.py"],
|
"scripts": ["bin/copyparty-fuse.py", "bin/up2k.py"],
|
||||||
|
|
Loading…
Reference in a new issue