jxl-enc: add th-qvx (separate qscale) + misc

This commit is contained in:
ed 2026-02-09 20:18:00 +00:00
parent fef34b5465
commit 48c1017843
6 changed files with 35 additions and 32 deletions

View file

@ -1706,7 +1706,8 @@ 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-qv", metavar="N", type=int, default=40, help="webp/jpg thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qv)")
ap2.add_argument("--th-qvx", metavar="N", type=int, default=64, help="jxl thumbnail quality (10~90); higher is larger filesize and better quality (volflag=th_qvx)")
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

@ -2449,7 +2449,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_qv th_spec_p u2abort u2ow uid unp_who ups_who zip_who"
zs = "forget_ip gid nrand tail_who th_qv th_qvx 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

@ -148,6 +148,7 @@ def vf_vmap() -> dict[str, str]:
"tail_who",
"tcolor",
"th_qv",
"th_qvx",
"th_spec_p",
"txt_eol",
"unlist",
@ -307,7 +308,8 @@ 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)",
"th_qv=40": "webp/jpg thumbnail quality (10~90)",
"th_qvx=40": "jxl 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

@ -52,9 +52,9 @@ class ThumbCli(object):
self.fmt_ffa = c["ffa"]
# defer args.th_ff_jpg, can change at runtime
d = next((x for x in self.args.th_dec if x in ("vips", "pil")), None)
self.can_webp = HAVE_WEBP or d == "vips"
self.can_jxl = HAVE_JXL or d == "vips"
nonpil = next((x for x in self.args.th_dec if x in ("vips", "ff")), None)
self.can_webp = HAVE_WEBP or nonpil
self.can_jxl = HAVE_JXL or nonpil
def log(self, msg: str, c: Union[int, str] = 0) -> None:
self.log_func("thumbcli", msg, c)
@ -104,18 +104,14 @@ class ThumbCli(object):
if sfmt == "w":
if (
self.args.th_no_webp
or (is_img and not self.can_webp)
or not self.can_webp
or (self.args.th_ff_jpg and (not is_img or preferred == "ff"))
):
sfmt = "j"
if sfmt == "x":
if (
self.args.th_no_jxl
or (is_img and not self.can_jxl)
or (self.args.th_ff_jpg and (not is_img or preferred == "ff"))
):
sfmt = "j"
if self.args.th_no_jxl or not self.can_jxl:
sfmt = "w"
vf_crop = dbv.flags["crop"]
vf_th3x = dbv.flags["th3x"]
@ -144,9 +140,12 @@ class ThumbCli(object):
tpath = thumb_path(histpath, rem, mtime, fmt, self.fmt_ffa)
tpaths = [tpath]
if fmt[:1] == "w" and fmt != "wav":
fmtc = fmt[:1]
if fmtc == "w" and fmt != "wav":
# also check for jpg (maybe webp is unavailable)
tpaths.append(tpath.rsplit(".", 1)[0] + ".jpg")
elif fmtc == "x":
tpaths.append(tpath.rsplit(".", 1)[0] + ".webp")
ret = None
abort = False

View file

@ -432,7 +432,7 @@ class ThumbSrv(object):
zs = "th_dec th_no_webp th_no_jpg"
for zs in zs.split(" "):
ret.append("%s(%s)\n" % (zs, getattr(self.args, zs)))
zs = "th_qv thsize th_spec_p convt"
zs = "th_qv th_qvx thsize th_spec_p convt"
for zs in zs.split(" "):
ret.append("%s(%s)\n" % (zs, vn.flags.get(zs)))
return "".join(ret)
@ -646,7 +646,8 @@ class ThumbSrv(object):
im.thumbnail(self.getres(vn, fmt))
fmts = ["RGB", "L"]
args = {"quality": vn.flags["th_qv"]}
zs = "th_qvx" if tpath.endswith(".jxl") else "th_qv"
args = {"quality": vn.flags[zs]}
if tpath.endswith(".webp"):
# quality 80 = pillow-default
@ -697,6 +698,10 @@ class ThumbSrv(object):
if tpath.endswith("jpg"):
qv = VIPS_JPG_Q[qv // 5]
args["optimize_coding"] = True
elif tpath.endswith("jxl"):
qv = vn.flags["th_qvx"]
# args["effort"] = 8
# `- not worth it; twice as slow, size drops 12%, no visual improvement unlike ffmpeg
img.write_to_file(tpath, Q=qv, strip=True, **args)
img.invalidate()
@ -787,11 +792,21 @@ class ThumbSrv(object):
]
# fmt: on
self._ffmpeg_im_o(tpath, vn, cmd)
def _ffmpeg_im_o(self, tpath: str, vn: VFS, cmd: list[bytes]) -> None:
if tpath.endswith(".jpg"):
cmd += [
b"-q:v",
FF_JPG_Q[vn.flags["th_qv"] // 5], # default=??
]
elif tpath.endswith(".jxl"):
cmd += [
b"-q:v",
unicode(vn.flags["th_qvx"]).encode("ascii"), # default=??
b"-effort:v",
b"8", # default=7, 1=fast, 9=max, 9~=8 but slower
]
else:
cmd += [
b"-q:v",
@ -996,21 +1011,7 @@ class ThumbSrv(object):
]
# fmt: on
if tpath.endswith(".jpg"):
cmd += [
b"-q:v",
FF_JPG_Q[vn.flags["th_qv"] // 5], # default=??
]
else:
cmd += [
b"-q:v",
unicode(vn.flags["th_qv"]).encode("ascii"), # default=75
b"-compression_level:v",
b"6", # default=4, 0=fast, 6=max
]
cmd += [fsenc(tpath)]
self._run_ff(cmd, vn, "convt")
self._ffmpeg_im_o(tpath, vn, cmd)
def conv_mp3(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
quality = self.args.q_mp3.lower()

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 th_qv 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 th_qvx 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"