diff --git a/contrib/plugins/graft-thumbs.js b/contrib/plugins/graft-thumbs.js index 65046e68..20d5abd4 100644 --- a/contrib/plugins/graft-thumbs.js +++ b/contrib/plugins/graft-thumbs.js @@ -36,7 +36,7 @@ for (var a = 0; a < files.length; a++) { var file = files[a], - is_pic = /\.(jpe?g|png|gif|webp)$/i.exec(file.vp), + is_pic = /\.(jpe?g|png|gif|webp|jxl)$/i.exec(file.vp), is_audio = re_au_all.exec(file.vp), basename = file.vp.replace(/\.[^\.]+$/, ""), entry = pairs[basename]; diff --git a/copyparty/__main__.py b/copyparty/__main__.py index 672948aa..7653452d 100644 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1668,7 +1668,7 @@ def add_logging(ap): ap2.add_argument("--log-htp", action="store_true", help="debug: print http-server threadpool scaling") ap2.add_argument("--ihead", metavar="HEADER", type=u, action='append', help="print request \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all") ap2.add_argument("--ohead", metavar="HEADER", type=u, action='append', help="print response \033[33mHEADER\033[0m; [\033[32m*\033[0m]=all") - ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|[?&]th=[wjp]|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m") + ap2.add_argument("--lf-url", metavar="RE", type=u, default=r"^/\.cpr/|[?&]th=[xwjp]|/\.(_|ql_|DS_Store$|localized$)", help="dont log URLs matching regex \033[33mRE\033[0m") ap2.add_argument("--scan-st-r", metavar="SEC", type=float, default=0.1, help="fs-indexing: wait \033[33mSEC\033[0m between each status-message") ap2.add_argument("--scan-pr-r", metavar="SEC", type=float, default=10, help="fs-indexing: wait \033[33mSEC\033[0m between each 'progress:' message") ap2.add_argument("--scan-pr-s", metavar="MiB", type=float, default=1, help="fs-indexing: say 'file: ' when a file larger than \033[33mMiB\033[0m is about to be hashed") @@ -1706,6 +1706,7 @@ def add_thumbnail(ap): 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") + ap2.add_argument("--th-no-jxl", action="store_true", help="disable jpeg-xl output") ap2.add_argument("--th-ff-jpg", action="store_true", help="force jpg output for video thumbs (avoids issues on some FFmpeg builds)") ap2.add_argument("--th-ff-swr", action="store_true", help="use swresample instead of soxr for audio thumbs (faster, lower accuracy, avoids issues on some FFmpeg builds)") ap2.add_argument("--th-poke", metavar="SEC", type=int, default=300, help="activity labeling cooldown -- avoids doing keepalive pokes (updating the mtime) on thumbnail folders more often than \033[33mSEC\033[0m seconds") diff --git a/copyparty/sutil.py b/copyparty/sutil.py index d4422007..4df6e54e 100644 --- a/copyparty/sutil.py +++ b/copyparty/sutil.py @@ -95,7 +95,7 @@ def enthumb( if not thp: raise Exception() - ext = "jpg" if fmt == "j" else "webp" if fmt == "w" else fmt + ext = "jpg" if fmt == "j" else "webp" if fmt == "w" else "jxl" if fmt == "x" else fmt sz = bos.path.getsize(thp) st: os.stat_result = f["st"] ts = st.st_mtime diff --git a/copyparty/th_cli.py b/copyparty/th_cli.py index 39cd9ab2..5297b6b9 100644 --- a/copyparty/th_cli.py +++ b/copyparty/th_cli.py @@ -8,7 +8,7 @@ import stat from .__init__ import TYPE_CHECKING from .authsrv import VFS from .bos import bos -from .th_srv import EXTS_AC, HAVE_WEBP, thumb_path +from .th_srv import EXTS_AC, HAVE_WEBP, HAVE_JXL, thumb_path from .util import Cooldown, Pebkac if True: # pylint: disable=using-constant-test @@ -20,7 +20,7 @@ if TYPE_CHECKING: IOERROR = "reading the file was denied by the server os; either due to filesystem permissions, selinux, apparmor, or similar:\n%r" -IMG_EXTS = set(["webp", "jpg", "png"]) +IMG_EXTS = set(["webp", "jpg", "png", "jxl"]) class ThumbCli(object): @@ -54,6 +54,7 @@ class ThumbCli(object): # 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" def log(self, msg: str, c: Union[int, str] = 0) -> None: self.log_func("thumbcli", msg, c) @@ -94,7 +95,7 @@ class ThumbCli(object): if rem.startswith(".hist/th/") and rem.split(".")[-1] in IMG_EXTS: return os.path.join(ptop, rem) - if fmt[:1] in "jw" and fmt != "wav": + if fmt[:1] in "jwx" and fmt != "wav": sfmt = fmt[:1] if sfmt == "j" and self.args.th_no_jpg: @@ -108,6 +109,14 @@ class ThumbCli(object): ): 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" + vf_crop = dbv.flags["crop"] vf_th3x = dbv.flags["th3x"] diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index d2ede61c..9e85aab0 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -49,8 +49,9 @@ HAVE_PILF = False HAVE_HEIF = False HAVE_AVIF = False HAVE_WEBP = False +HAVE_JXL = False -EXTS_TH = set(["jpg", "webp", "png"]) +EXTS_TH = set(["jpg", "webp", "jxl", "png"]) EXTS_AC = set(["opus", "owa", "caf", "mp3", "flac", "wav"]) EXTS_SPEC_SAFE = set("aif aiff flac mp3 opus wav".split()) @@ -132,6 +133,20 @@ try: except: pass + try: + if os.environ.get("PRTY_NO_PIL_JXL"): + raise Exception() + + try: + import pillow_jxl + except ImportError: + pass + + Image.new("RGB", (2, 2)).save(BytesIO(), format="jxl") + HAVE_JXL = True + except: + pass + try: if os.environ.get("PRTY_NO_PIL_HEIF"): raise Exception() @@ -203,7 +218,7 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) - # spectrograms are never cropped; strip fullsize flag ext = rem.split(".")[-1].lower() - if ext in ffa and fmt[:2] in ("wf", "jf"): + if ext in ffa and fmt[:2] in ("wf", "jf", "xf"): fmt = fmt.replace("f", "") dcache = th_dir_cache @@ -225,7 +240,7 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str, ffa: set[str]) - cat = "ac" else: fc = fmt[:1] - fmt = "webp" if fc == "w" else "png" if fc == "p" else "jpg" + fmt = "webp" if fc == "w" else "png" if fc == "p" else "jxl" if fc == "x" else "jpg" cat = "th" return "%s/%s/%s/%s.%x.%s" % (histpath, cat, rd, fn, int(mtime), fmt) @@ -304,6 +319,10 @@ class ThumbSrv(object): for f in "webp".split(" "): self.fmt_pil.discard(f) + if not HAVE_JXL: + for f in "jxl".split(" "): + self.fmt_pil.discard(f) + self.thumbable: set[str] = set() if "pil" in self.args.th_dec: diff --git a/copyparty/web/baguettebox.js b/copyparty/web/baguettebox.js index c1668951..c8fbeecb 100644 --- a/copyparty/web/baguettebox.js +++ b/copyparty/web/baguettebox.js @@ -36,7 +36,7 @@ window.baguetteBox = (function () { touchFlag = false, // busy scrollCSS = ['', ''], scrollTimer = 0, - re_i = /^[^?]+\.(a?png|avif|bmp|gif|heif|jfif|jpe?g|svg|tiff?|webp)(\?|$)/i, + re_i = /^[^?]+\.(a?png|avif|bmp|gif|heif|jfif|jpe?g|jxl|svg|tiff?|webp)(\?|$)/i, re_v = /^[^?]+\.(webm|mkv|mp4|m4v|mov)(\?|$)/i, re_cbz = /^[^?]+\.(cbz)(\?|$)/i, cbz_pics = ["png", "jpg", "jpeg", "gif", "bmp", "tga", "tif", "tiff", "webp", "avif"], diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index eb6d2a65..17787ca1 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -1154,22 +1154,26 @@ function read_sbw() { onresize100.add(read_sbw, true); -var have_webp = sread('have_webp'); -(function () { - if (have_webp !== null) +function check_image_support(format, uri) { + var cached + = window['have_' + format] + = sread('have_' + format); + if (cached !== null) return; var img = new Image(); img.onload = function () { - have_webp = img.width > 0 && img.height > 0; - swrite('have_webp', 'ya'); + window['have_' + format] = img.width > 0 && img.height > 0; + swrite('have_' + format, 'ya'); }; img.onerror = function () { - have_webp = false; - swrite('have_webp', ''); + window['have_' + format] = false; + swrite('have_' + format, ''); }; - img.src = "data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=="; -})(); + img.src = uri; +} +check_image_support('webp', "data:image/webp;base64,UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA=="); +check_image_support('jxl', "data:image/jxl;base64,/woIAAAMABKIAgC4AF3lEgA="); function set_files_html(html) { @@ -5685,7 +5689,7 @@ var thegrid = (function () { }; function loadgrid() { - if (have_webp === null) + if (have_webp === null || have_jxl === null) return setTimeout(loadgrid, 50); r.setvis(); @@ -5738,7 +5742,11 @@ var thegrid = (function () { ihref = ext_th[ext] || ext_th[ext0]; } else if (r.thumbs) { - ihref = addq(ihref, 'th=' + (have_webp ? 'w' : 'j')); + ihref = addq(ihref, 'th=' + ( + have_jxl ? 'x' : + have_webp ? 'w' : + 'j' + )); if (!r.crop) ihref += 'f'; if (r.x3) diff --git a/docs/chungus.conf b/docs/chungus.conf index a8e34516..63385dd8 100644 --- a/docs/chungus.conf +++ b/docs/chungus.conf @@ -1015,7 +1015,7 @@ ohead: set-cooke # hint; default is unset # dont log URLs matching regex RE - lf-url: ^/\.cpr/|[?&]th=[wjp]|/\.(_|ql_|DS_Store$|localized$) # default + lf-url: ^/\.cpr/|[?&]th=[xwjp]|/\.(_|ql_|DS_Store$|localized$) # default ###000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ ###// admin panel options \\000000000000000000000000000000000000000000000000000000000000000000\ @@ -1091,6 +1091,9 @@ # disable webp output th-no-webp + # disable jpeg-xl output + th-no-jxl + # force jpg output for video thumbs (avoids issues on some FFmpeg builds) th-ff-jpg