From 33f41f3e615d9676a07ab9895fa03273eb8497ee Mon Sep 17 00:00:00 2001 From: ed Date: Sun, 18 Feb 2024 13:04:22 +0000 Subject: [PATCH] add hi-res thumbs (togglebtn/servercfg) --- copyparty/__main__.py | 4 ++- copyparty/cfg.py | 6 ++-- copyparty/httpcli.py | 6 ++-- copyparty/ico.py | 2 +- copyparty/th_srv.py | 29 ++++++++++------- copyparty/web/browser.js | 70 ++++++++++++++++++++++++++++++---------- tests/util.py | 4 ++- 7 files changed, 85 insertions(+), 36 deletions(-) diff --git a/copyparty/__main__.py b/copyparty/__main__.py index f4faa5ce..60bb6919 100755 --- a/copyparty/__main__.py +++ b/copyparty/__main__.py @@ -1170,7 +1170,8 @@ def add_thumbnail(ap): 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=float, default=60, help="conversion timeout in seconds (volflag=convt)") ap2.add_argument("--th-ram-max", metavar="GB", type=float, default=6, help="max memory usage (GiB) permitted by thumbnailer; not very accurate") - ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image by default (client can override in UI) (volflag=nocrop)") + 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[32mfy\033[0m]=crop, [\033[32mfn\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[32mfy\033[0m]=yes, [\033[32mfn\033[0m]=no, [\033[32mfy\033[0m]=force-yes, [\033[32mfn\033[0m]=force-no (volflag=th3x)") ap2.add_argument("--th-dec", metavar="LIBS", default="vips,pil,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") @@ -1430,6 +1431,7 @@ def main(argv: Optional[list[str]] = None) -> None: deprecated: list[tuple[str, str]] = [ ("--salt", "--warksalt"), ("--hdr-au-usr", "--idp-h-usr"), + ("--th-no-crop", "--th-crop=n"), ] for dk, nk in deprecated: idx = -1 diff --git a/copyparty/cfg.py b/copyparty/cfg.py index cc4f77a1..10e921e8 100644 --- a/copyparty/cfg.py +++ b/copyparty/cfg.py @@ -20,7 +20,6 @@ def vf_bmap() -> dict[str, str]: "no_thumb": "dthumb", "no_vthumb": "dvthumb", "no_athumb": "dathumb", - "th_no_crop": "nocrop", } for k in ( "dotsrch", @@ -56,6 +55,8 @@ def vf_vmap() -> dict[str, str]: "re_maxage": "scan", "th_convt": "convt", "th_size": "thsize", + "th_crop": "crop", + "th_x3": "th3x", } for k in ( "dbd", @@ -172,7 +173,8 @@ flagcats = { "dathumb": "disables audio thumbnails (spectrograms)", "dithumb": "disables image thumbnails", "thsize": "thumbnail res; WxH", - "nocrop": "disable center-cropping by default", + "crop": "center-cropping (y/n/fy/fn)", + "th3x": "3x resolution (y/n/fy/fn)", "convt": "conversion timeout in seconds", }, "handlers\n(better explained in --help-handlers)": { diff --git a/copyparty/httpcli.py b/copyparty/httpcli.py index 153a6763..fc12be4f 100644 --- a/copyparty/httpcli.py +++ b/copyparty/httpcli.py @@ -3973,7 +3973,8 @@ class HttpCli(object): "idx": e2d, "itag": e2t, "dsort": vf["sort"], - "dfull": "nocrop" in vf, + "dcrop": vf["crop"], + "dth3x": vf["th3x"], "u2ts": vf["u2ts"], "lifetime": vn.flags.get("lifetime") or 0, "frand": bool(vn.flags.get("rand")), @@ -4000,8 +4001,9 @@ class HttpCli(object): "sb_md": "" if "no_sb_md" in vf else (vf.get("md_sbf") or "y"), "readme": readme, "dgrid": "grid" in vf, - "dfull": "nocrop" in vf, "dsort": vf["sort"], + "dcrop": vf["crop"], + "dth3x": vf["th3x"], "themes": self.args.themes, "turbolvl": self.args.turbo, "u2j": self.args.u2j, diff --git a/copyparty/ico.py b/copyparty/ico.py index 00da00dd..28537d25 100644 --- a/copyparty/ico.py +++ b/copyparty/ico.py @@ -31,7 +31,7 @@ class Ico(object): w = 100 h = 30 - if not self.args.th_no_crop and as_thumb: + if "n" in self.args.th_crop and as_thumb: sw, sh = self.args.th_size.split("x") h = int(100.0 / (float(sw) / float(sh))) w = 100 diff --git a/copyparty/th_srv.py b/copyparty/th_srv.py index 10ab1223..3a45aaf3 100644 --- a/copyparty/th_srv.py +++ b/copyparty/th_srv.py @@ -97,8 +97,8 @@ 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 in ("wf", "jf"): - fmt = fmt[:1] + if ext in ffa and fmt[:2] in ("wf", "jf"): + fmt = fmt.replace("f", "") rd += "\n" + fmt h = hashlib.sha512(afsenc(rd)).digest() @@ -200,9 +200,10 @@ class ThumbSrv(object): with self.mutex: return not self.nthr - def getres(self, vn: VFS) -> tuple[int, int]: + def getres(self, vn: VFS, fmt: str) -> tuple[int, int]: + mul = 3 if "3" in fmt else 1 w, h = vn.flags["thsize"].split("x") - return int(w), int(h) + return int(w) * mul, int(h) * mul def get(self, ptop: str, rem: str, mtime: float, fmt: str) -> Optional[str]: histpath = self.asrv.vfs.histtab.get(ptop) @@ -364,7 +365,7 @@ class ThumbSrv(object): def fancy_pillow(self, im: "Image.Image", fmt: str, vn: VFS) -> "Image.Image": # exif_transpose is expensive (loads full image + unconditional copy) - res = self.getres(vn) + res = self.getres(vn, fmt) r = max(*res) * 2 im.thumbnail((r, r), resample=Image.LANCZOS) try: @@ -379,7 +380,7 @@ class ThumbSrv(object): if rot in rots: im = im.transpose(rots[rot]) - if fmt.endswith("f"): + if "f" in fmt: im.thumbnail(res, resample=Image.LANCZOS) else: iw, ih = im.size @@ -396,7 +397,7 @@ class ThumbSrv(object): im = self.fancy_pillow(im, fmt, vn) except Exception as ex: self.log("fancy_pillow {}".format(ex), "90") - im.thumbnail(self.getres(vn)) + im.thumbnail(self.getres(vn, fmt)) fmts = ["RGB", "L"] args = {"quality": 40} @@ -422,10 +423,10 @@ class ThumbSrv(object): def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None: self.wait4ram(0.2, tpath) crops = ["centre", "none"] - if fmt.endswith("f"): + if "f" in fmt: crops = ["none"] - w, h = self.getres(vn) + w, h = self.getres(vn, fmt) kw = {"height": h, "size": "down", "intent": "relative"} for c in crops: @@ -454,12 +455,12 @@ class ThumbSrv(object): seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")] scale = "scale={0}:{1}:force_original_aspect_ratio=" - if fmt.endswith("f"): + if "f" in fmt: scale += "decrease,setsar=1:1" else: scale += "increase,crop={0}:{1},setsar=1:1" - res = self.getres(vn) + res = self.getres(vn, fmt) bscale = scale.format(*list(res)).encode("utf-8") # fmt: off cmd = [ @@ -594,7 +595,11 @@ class ThumbSrv(object): need = 0.2 + dur / coeff self.wait4ram(need, tpath) - fc = "[0:a:0]aresample=48000{},showspectrumpic=s=640x512,crop=780:544:70:50[o]" + fc = "[0:a:0]aresample=48000{},showspectrumpic=s=" + if "3" in fmt: + fc += "1280x1024,crop=1420:1056:70:48[o]" + else: + fc += "640x512,crop=780:544:70:48[o]" if self.args.th_ff_swr: fco = ":filter_size=128:cutoff=0.877" diff --git a/copyparty/web/browser.js b/copyparty/web/browser.js index 2d49f450..7d71d34a 100644 --- a/copyparty/web/browser.js +++ b/copyparty/web/browser.js @@ -349,7 +349,8 @@ var Ls = { "tvt_edit": "open file in text editor$NHotkey: E\">✏️ edit", "gt_msel": "enable file selection; ctrl-click a file to override$N$N<em>when active: doubleclick a file / folder to open it</em>$N$NHotkey: S\">multiselect", - "gt_full": "show uncropped thumbnails\">full", + "gt_crop": "center-crop thumbnails\">crop", + "gt_3x": "hi-res thumbnails\">3x", "gt_zoom": "zoom", "gt_chop": "chop", "gt_sort": "sort by", @@ -844,7 +845,8 @@ var Ls = { "tvt_edit": "redigér filen$NSnarvei: E\">✏️ endre", "gt_msel": "markér filer istedenfor å åpne dem; ctrl-klikk filer for å overstyre$N$N<em>når aktiv: dobbelklikk en fil / mappe for å åpne</em>$N$NSnarvei: S\">markering", - "gt_full": "ikke beskjær bildene\">full", + "gt_crop": "beskjær ikonene så de passer bedre\">✂", + "gt_3x": "høyere oppløsning på ikoner\">3x", "gt_zoom": "zoom", "gt_chop": "trim", "gt_sort": "sorter", @@ -4515,7 +4517,9 @@ var thegrid = (function () { gfiles.innerHTML = ( '
' + ' ' + '+ ' + L.gt_chop + ': ' + ' ' + @@ -4530,7 +4534,7 @@ var thegrid = (function () { lfiles.parentNode.insertBefore(gfiles, lfiles); var r = { - 'sz': clamp(fcfg_get('gridsz', 10), 4, 40), + 'sz': clamp(fcfg_get('gridsz', 10), 4, 80), 'ln': clamp(icfg_get('gridln', 3), 1, 7), 'isdirty': true, 'bbox': null @@ -4593,10 +4597,10 @@ var thegrid = (function () { r.setdirty = function () { r.dirty = true; - if (r.en) { + if (r.en) loadgrid(); - } - r.setvis(); + else + r.setvis(); }; function setln(v) { @@ -4616,7 +4620,7 @@ var thegrid = (function () { function setsz(v) { if (v !== undefined) { - r.sz = clamp(v, 4, 40); + r.sz = clamp(v, 4, 80); swrite('gridsz', r.sz); setTimeout(r.tippen, 20); } @@ -4624,6 +4628,7 @@ var thegrid = (function () { document.documentElement.style.setProperty('--grid-sz', r.sz + 'em'); } catch (ex) { } + aligngriditems(); } setsz(); @@ -4776,8 +4781,11 @@ var thegrid = (function () { if (!r.dirty) return r.loadsel(); - if (dfull != r.full && !sread('gridfull')) - bcfg_upd_ui('gridfull', r.full = dfull); + if (dcrop.startsWith('f') || !sread('gridcrop')) + bcfg_upd_ui('gridcrop', r.crop = ('y' == dcrop.slice(-1))); + + if (dth3x.startsWith('f') || !sread('grid3x')) + bcfg_upd_ui('grid3x', r.x3 = ('y' == dth3x.slice(-1))); var html = [], svgs = new Set(), @@ -4796,8 +4804,10 @@ var thegrid = (function () { if (r.thumbs) { ihref += '?th=' + (have_webp ? 'w' : 'j'); - if (r.full) - ihref += 'f' + if (!r.crop) + ihref += 'f'; + if (r.x3) + ihref += '3'; if (href == "#") ihref = SR + '/.cpr/ico/' + (ref == 'moar' ? '++' : 'exit'); } @@ -4833,7 +4843,7 @@ var thegrid = (function () { html.push('' + ao.innerHTML + ''); } ebi('ggrid').innerHTML = html.join('\n'); @@ -4884,8 +4894,29 @@ var thegrid = (function () { })[0]; }; + r.set_crop = function (en) { + if (!dcrop.startsWith('f')) + return r.setdirty(); + + r.crop = dcrop.startsWith('y'); + bcfg_upd_ui('gridcrop', r.crop); + if (r.crop != en) + toast.warn(10, L.ul_btnlk); + }; + + r.set_x3 = function (en) { + if (!dth3x.startsWith('f')) + return r.setdirty(); + + r.x3 = dth3x.startsWith('y'); + bcfg_upd_ui('grid3x', r.x3); + if (r.x3 != en) + toast.warn(10, L.ul_btnlk); + }; + bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty); - bcfg_bind(r, 'full', 'gridfull', false, r.setdirty); + bcfg_bind(r, 'crop', 'gridcrop', !dcrop.endsWith('n'), r.set_crop); + bcfg_bind(r, 'x3', 'grid3x', dth3x.endsWith('y'), r.set_x3); bcfg_bind(r, 'sel', 'gridsel', false, r.loadsel); bcfg_bind(r, 'en', 'griden', dgrid, function (v) { v ? loadgrid() : r.setvis(true); @@ -5575,11 +5606,15 @@ function aligngriditems() { if (/b/.test(themen + '')) totalgapwidth *= 2.8; + var val, st = ebi('ggrid').style; + if (((griditemcount * em2px) * gridsz) + totalgapwidth < gridwidth) { - ebi('ggrid').style.justifyContent = 'left'; + val = 'left'; } else { - ebi('ggrid').style.justifyContent = treectl.hidden ? 'center' : 'space-between'; + val = treectl.hidden ? 'center' : 'space-between'; } + if (st.justifyContent != val) + st.justifyContent = val; } onresize100.add(aligngriditems); @@ -6110,7 +6145,8 @@ var treectl = (function () { res.files[a].tags = {}; read_dsort(res.dsort); - dfull = res.dfull; + dcrop = res.dcrop; + dth3x = res.dth3x; srvinf = res.srvinf; try { diff --git a/tests/util.py b/tests/util.py index 3f0967ee..25a9c03b 100644 --- a/tests/util.py +++ b/tests/util.py @@ -110,7 +110,7 @@ class Cfg(Namespace): def __init__(self, a=None, v=None, c=None, **ka0): ka = {} - ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw q rand smb srch_dbg stats th_no_crop vague_403 vc ver xdev xlink xvol" + ex = "daw dav_auth dav_inf dav_mac dav_rt e2d e2ds e2dsa e2t e2ts e2tsr e2v e2vu e2vp ed emp exp force_js getmod grid hardlink ih ihead magic never_symlink nid nih no_acode no_athumb no_dav no_dedup no_del no_dupe no_lifetime no_logues no_mv no_readme no_robots no_sb_md no_sb_lg no_scandir no_tarcmp no_thumb no_vthumb no_zip nrand nw q rand smb srch_dbg stats th_x3 vague_403 vc ver xdev xlink xvol" ka.update(**{k: False for k in ex.split()}) ex = "dotpart dotsrch no_dhash no_fastboot no_rescan no_sendfile no_voldump re_dhash plain_ip" @@ -156,7 +156,9 @@ class Cfg(Namespace): s_wr_sz=512 * 1024, sort="href", srch_hits=99999, + th_crop="y", th_size="320x256", + th_x3="n", u2sort="s", u2ts="c", unpost=600,