option to set thumbnail quality (#1092);

plus these fixes:

* adds a previously missed libvips optimization,
   giving much smaller files at the same quality

* try to align the quality-scale of each backend
   (pillow, libvips, ffmpeg) by filesize
This commit is contained in:
ed
2025-12-12 07:51:01 +00:00
parent 1b222fb576
commit a1cbac0252
5 changed files with 75 additions and 12 deletions

View File

@@ -1646,6 +1646,7 @@ def add_thumbnail(ap):
ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=th_ram, help="max memory usage (GiB) permitted by thumbnailer; not very accurate")
ap2.add_argument("--th-crop", metavar="TXT", type=u, default="y", help="crop thumbnails to 4:3 or keep dynamic height; client can override in UI unless force. [\033[32my\033[0m]=crop, [\033[32mn\033[0m]=nocrop, [\033[32mfy\033[0m]=force-y, [\033[32mfn\033[0m]=force-n (volflag=crop)")
ap2.add_argument("--th-x3", metavar="TXT", type=u, default="n", help="show thumbs at 3x resolution; client can override in UI unless force. [\033[32my\033[0m]=yes, [\033[32mn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)")
ap2.add_argument("--th-qv", metavar="N", type=int, default=40, help="thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qv)")
ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,raw,ff", help="image decoders, in order of preference")
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")

View File

@@ -2384,7 +2384,7 @@ class AuthSrv(object):
if vf not in vol.flags:
vol.flags[vf] = getattr(self.args, ga)
zs = "forget_ip gid nrand tail_who th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
zs = "forget_ip gid nrand tail_who th_qv th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
for k in zs.split():
if k in vol.flags:
vol.flags[k] = int(vol.flags[k])

View File

@@ -133,6 +133,7 @@ def vf_vmap() -> dict[str, str]:
"tail_tmax",
"tail_who",
"tcolor",
"th_qv",
"th_spec_p",
"txt_eol",
"unlist",
@@ -289,6 +290,7 @@ flagcats = {
"thsize": "thumbnail res; WxH",
"crop": "center-cropping (y/n/fy/fn)",
"th3x": "3x resolution (y/n/fy/fn)",
"th_qv=40": "thumbnail quality (10~90)",
"convt": "convert-to-image timeout in seconds",
"aconvt": "convert-to-audio timeout in seconds",
"th_spec_p=1": "make spectrograms? 0=never 1=fallback 2=always",

View File

@@ -14,7 +14,7 @@ import time
from queue import Queue
from .__init__ import ANYWIN, PY2, TYPE_CHECKING
from .__init__ import ANYWIN, PY2, TYPE_CHECKING, unicode
from .authsrv import VFS
from .bos import bos
from .mtag import HAVE_FFMPEG, HAVE_FFPROBE, au_unpk, ffprobe
@@ -56,6 +56,56 @@ EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split())
PTN_TS = re.compile("^-?[0-9a-f]{8,10}$")
# for n in {1..100}; do rm -rf /home/ed/Pictures/wp/.hist/th/ ; python3 -m copyparty -qv /home/ed/Pictures/wp/::r --th-no-webp --th-qv $n --th-dec pil >/dev/null 2>&1 & p=$!; printf '\033[A\033[J%3d ' $n; while true; do sleep 0.1; curl -s 127.1:3923 >/dev/null && break; done; curl -s '127.1:3923/?tar=j' >/dev/null ; cat /home/ed/Pictures/wp/.hist/th/1n/bs/1nBsjDetfie1iDq3y2D4YzF5/*.* | wc -c; kill $p; wait >/dev/null 2>&1; done
# filesize-equivalent, not quality (ff looks much shittier)
FF_JPG_Q = {
0: b"30", # 0
1: b"30", # 5
2: b"30", # 10
3: b"30", # 15
4: b"28", # 20
5: b"21", # 25
6: b"17", # 30
7: b"15", # 35
8: b"13", # 40
9: b"12", # 45
10: b"11", # 50
11: b"10", # 55
12: b"9", # 60
13: b"8", # 65
14: b"7", # 70
15: b"6", # 75
16: b"5", # 80
17: b"4", # 85
18: b"3", # 90
19: b"2", # 95
20: b"2", # 100
}
# FF_JPG_Q = {xn: ("%d" % (xn,)).encode("ascii") for xn in range(2, 33)}
VIPS_JPG_Q = {
0: 4, # 0
1: 7, # 5
2: 12, # 10
3: 17, # 15
4: 22, # 20
5: 27, # 25
6: 32, # 30
7: 37, # 35
8: 42, # 40
9: 47, # 45
10: 52, # 50
11: 56, # 55
12: 61, # 60
13: 66, # 65
14: 71, # 70
15: 75, # 75
16: 80, # 80
17: 85, # 85
18: 89, # 90 (vips explodes past this point)
19: 91, # 95
20: 97, # 100
}
try:
if os.environ.get("PRTY_NO_PIL"):
@@ -529,7 +579,7 @@ class ThumbSrv(object):
im.thumbnail(self.getres(vn, fmt))
fmts = ["RGB", "L"]
args = {"quality": 40}
args = {"quality": vn.flags["th_qv"]}
if tpath.endswith(".webp"):
# quality 80 = pillow-default
@@ -573,7 +623,12 @@ class ThumbSrv(object):
raise
assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40)
args = {}
qv = vn.flags["th_qv"]
if tpath.endswith("jpg"):
qv = VIPS_JPG_Q[qv // 5]
args["optimize_coding"] = True
img.write_to_file(tpath, Q=qv, strip=True, **args)
def conv_raw(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
self.wait4ram(0.2, tpath)
@@ -607,7 +662,12 @@ class ThumbSrv(object):
raise
assert img # type: ignore # !rm
img.write_to_file(tpath, Q=40)
args = {}
qv = vn.flags["th_qv"]
if tpath.endswith("jpg"):
qv = VIPS_JPG_Q[qv // 5]
args["optimize_coding"] = True
img.write_to_file(tpath, Q=qv, strip=True, **args)
elif HAVE_PIL:
if thumb.format == rawpy.ThumbFormat.BITMAP:
im = Image.fromarray(thumb.data, "RGB")
@@ -671,12 +731,12 @@ class ThumbSrv(object):
if tpath.endswith(".jpg"):
cmd += [
b"-q:v",
b"6", # default=??
FF_JPG_Q[vn.flags["th_qv"] // 5], # default=??
]
else:
cmd += [
b"-q:v",
b"50", # default=75
unicode(vn.flags["th_qv"]).encode("ascii"), # default=75
b"-compression_level:v",
b"6", # default=4, 0=fast, 6=max
]
@@ -722,7 +782,7 @@ class ThumbSrv(object):
if len(lines) > 50:
lines = lines[:25] + ["[...]"] + lines[-25:]
txt = "\n".join(["ff: " + str(x) for x in lines])
txt = "\n".join(["ff: " + unicode(x) for x in lines])
if len(txt) > 5000:
txt = txt[:2500] + "...\nff: [...]\nff: ..." + txt[-2500:]
@@ -880,12 +940,12 @@ class ThumbSrv(object):
if tpath.endswith(".jpg"):
cmd += [
b"-q:v",
b"6", # default=??
FF_JPG_Q[vn.flags["th_qv"] // 5], # default=??
]
else:
cmd += [
b"-q:v",
b"50", # default=75
unicode(vn.flags["th_qv"]).encode("ascii"), # default=75
b"-compression_level:v",
b"6", # default=4, 0=fast, 6=max
]
@@ -1143,7 +1203,7 @@ class ThumbSrv(object):
ret = []
for k, vs in raw_tags.items():
for v in vs:
if len(str(v)) >= 1024:
if len(unicode(v)) >= 1024:
bv = k.encode("utf-8", "replace")
ret += [b"-metadata", bv + b"="]
break

View File

@@ -158,7 +158,7 @@ class Cfg(Namespace):
ex = "hash_mt hsortn qdel safe_dedup scan_pr_r scan_pr_s scan_st_r srch_time tail_fd tail_rate th_spec_p u2abort u2j u2sz unp_who"
ka.update(**{k: 1 for k in ex.split()})
ex = "ac_convt au_vol dl_list du_iwho mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt ups_who ver_iwho zip_who"
ex = "ac_convt au_vol dl_list du_iwho mtab_age reg_cap s_thead s_tbody tail_tmax tail_who th_convt th_qv ups_who ver_iwho zip_who"
ka.update(**{k: 9 for k in ex.split()})
ex = "ctl_re db_act forget_ip idp_cookie idp_store k304 loris no304 nosubtle qr_pin qr_wait re_maxage rproxy rsp_jtr rsp_slp s_wr_slp snap_wri theme themes turbo u2ow zipmaxn zipmaxs"