diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 80fe68a4..d5ce2020 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -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") diff --git a/copyparty/authsrv.py b/copyparty/authsrv.py index e0b17c8e..2c97a8fc 100644 --- a/copyparty/authsrv.py +++ b/copyparty/authsrv.py @@ -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]) diff --git a/copyparty/cfg.py b/copyparty/cfg.py index b24947d0..13b2c8b9 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -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", diff --git a/copyparty/th_cli.py b/copyparty/th_cli.py index 889e1c59..2b860f4c 100644 --- a/copyparty/th_cli.py +++ b/copyparty/th_cli.py @@ -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 diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 08758b2f..3cfdd3c9 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -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() diff --git a/tests/util.py b/tests/util.py index 4346eae9..7c0b6de1 100644 --- a/tests/util.py +++ b/tests/util.py @@ -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"