client decides if thumbnails should be cropped or not

this carries some intentional side-effects; each thumbnail format will
now be stored in its own subfolder under .hist/th/ making cleanup more
effective (jpeg and webm are dropped separately)
This commit is contained in:
ed 2023-10-15 10:21:25 +00:00
parent 9b77c9ce7d
commit 2df76eb6e1
5 changed files with 40 additions and 26 deletions

View file

@ -1069,7 +1069,7 @@ def add_thumbnail(ap):
ap2.add_argument("--th-size", metavar="WxH", default="320x256", help="thumbnail res (volflag=thsize)")
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-no-crop", action="store_true", help="dynamic height; show full image (volflag=nocrop)")
ap2.add_argument("--th-no-crop", action="store_true", help="dynamic height; show full image by default (volflag=nocrop)")
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")

View file

@ -129,7 +129,7 @@ flagcats = {
"dathumb": "disables audio thumbnails (spectrograms)",
"dithumb": "disables image thumbnails",
"thsize": "thumbnail res; WxH",
"nocrop": "disable center-cropping",
"nocrop": "disable center-cropping by default",
"convt": "conversion timeout in seconds",
},
"handlers\n(better explained in --help-handlers)": {

View file

@ -3834,6 +3834,7 @@ class HttpCli(object):
"idx": e2d,
"itag": e2t,
"dsort": vf["sort"],
"dfull": "nocrop" in vf,
"u2ts": vf["u2ts"],
"lifetime": vn.flags.get("lifetime") or 0,
"frand": bool(vn.flags.get("rand")),
@ -3860,6 +3861,7 @@ 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"],
"themes": self.args.themes,
"turbolvl": self.args.turbo,

View file

@ -84,12 +84,13 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str) -> str:
# b64-lc = 38 = 1444
# base64 = 64 = 4096
rd, fn = vsplit(rem)
if rd:
h = hashlib.sha512(afsenc(rd)).digest()
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64
else:
rd = "top"
if not rd:
rd = "\ntop"
rd += "\n" + fmt
h = hashlib.sha512(afsenc(rd)).digest()
b64 = base64.urlsafe_b64encode(h).decode("ascii")[:24]
rd = "{}/{}/".format(b64[:2], b64[2:4]).lower() + b64
# could keep original filenames but this is safer re pathlen
h = hashlib.sha512(afsenc(fn)).digest()
@ -98,7 +99,8 @@ def thumb_path(histpath: str, rem: str, mtime: float, fmt: str) -> str:
if fmt in ("opus", "caf"):
cat = "ac"
else:
fmt = "webp" if fmt == "w" else "png" if fmt == "p" else "jpg"
fc = fmt[:1]
fmt = "webp" if fc == "w" else "png" if fc == "p" else "jpg"
cat = "th"
return "{}/{}/{}/{}.{:x}.{}".format(histpath, cat, rd, fn, int(mtime), fmt)
@ -118,7 +120,7 @@ class ThumbSrv(object):
self.stopping = False
self.nthr = max(1, self.args.th_mt)
self.q: Queue[Optional[tuple[str, str, VFS]]] = Queue(self.nthr * 4)
self.q: Queue[Optional[tuple[str, str, str, VFS]]] = Queue(self.nthr * 4)
for n in range(self.nthr):
Daemon(self.worker, "thumb-{}-{}".format(n, self.nthr))
@ -220,8 +222,8 @@ class ThumbSrv(object):
self.log("ptop [{}] not in {}".format(ptop, allvols), 3)
vn = self.asrv.vfs.all_aps[0][1]
self.q.put((abspath, tpath, vn))
self.log("conv {} \033[0m{}".format(tpath, abspath), c=6)
self.q.put((abspath, tpath, fmt, vn))
self.log("conv {} :{} \033[0m{}".format(tpath, fmt, abspath), c=6)
while not self.stopping:
with self.mutex:
@ -257,7 +259,7 @@ class ThumbSrv(object):
if not task:
break
abspath, tpath, vn = task
abspath, tpath, fmt, vn = task
ext = abspath.split(".")[-1].lower()
png_ok = False
funs = []
@ -290,7 +292,7 @@ class ThumbSrv(object):
for fun in funs:
try:
fun(abspath, ttpath, vn)
fun(abspath, ttpath, fmt, vn)
break
except Exception as ex:
msg = "{} could not create thumbnail of {}\n{}"
@ -324,7 +326,7 @@ class ThumbSrv(object):
with self.mutex:
self.nthr -= 1
def fancy_pillow(self, im: "Image.Image", vn: VFS) -> "Image.Image":
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)
r = max(*res) * 2
@ -341,7 +343,7 @@ class ThumbSrv(object):
if rot in rots:
im = im.transpose(rots[rot])
if "nocrop" in vn.flags:
if fmt.endswith("f"):
im.thumbnail(res, resample=Image.LANCZOS)
else:
iw, ih = im.size
@ -351,10 +353,10 @@ class ThumbSrv(object):
return im
def conv_pil(self, abspath: str, tpath: str, vn: VFS) -> None:
def conv_pil(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
with Image.open(fsenc(abspath)) as im:
try:
im = self.fancy_pillow(im, vn)
im = self.fancy_pillow(im, fmt, vn)
except Exception as ex:
self.log("fancy_pillow {}".format(ex), "90")
im.thumbnail(self.getres(vn))
@ -380,9 +382,9 @@ class ThumbSrv(object):
im.save(tpath, **args)
def conv_vips(self, abspath: str, tpath: str, vn: VFS) -> None:
def conv_vips(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
crops = ["centre", "none"]
if "nocrop" in vn.flags:
if fmt.endswith("f"):
crops = ["none"]
w, h = self.getres(vn)
@ -399,7 +401,7 @@ class ThumbSrv(object):
img.write_to_file(tpath, Q=40)
def conv_ffmpeg(self, abspath: str, tpath: str, vn: VFS) -> None:
def conv_ffmpeg(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if not ret:
return
@ -412,7 +414,7 @@ class ThumbSrv(object):
seek = [b"-ss", "{:.0f}".format(dur / 3).encode("utf-8")]
scale = "scale={0}:{1}:force_original_aspect_ratio="
if "nocrop" in vn.flags:
if fmt.endswith("f"):
scale += "decrease,setsar=1:1"
else:
scale += "increase,crop={0}:{1},setsar=1:1"
@ -497,7 +499,7 @@ class ThumbSrv(object):
self.log(t + txt, c=c)
raise sp.CalledProcessError(ret, (cmd[0], b"...", cmd[-1]))
def conv_waves(self, abspath: str, tpath: str, vn: VFS) -> None:
def conv_waves(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret:
raise Exception("not audio")
@ -525,7 +527,7 @@ class ThumbSrv(object):
cmd += [fsenc(tpath)]
self._run_ff(cmd, vn)
def conv_spec(self, abspath: str, tpath: str, vn: VFS) -> None:
def conv_spec(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
ret, _ = ffprobe(abspath, int(vn.flags["convt"] / 2))
if "ac" not in ret:
raise Exception("not audio")
@ -568,7 +570,7 @@ class ThumbSrv(object):
cmd += [fsenc(tpath)]
self._run_ff(cmd, vn)
def conv_opus(self, abspath: str, tpath: str, vn: VFS) -> None:
def conv_opus(self, abspath: str, tpath: str, fmt: str, vn: VFS) -> None:
if self.args.no_acode:
raise Exception("disabled in server config")

View file

@ -333,6 +333,7 @@ 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_zoom": "zoom",
"gt_chop": "chop",
"gt_sort": "sort by",
@ -804,6 +805,7 @@ 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_zoom": "zoom",
"gt_chop": "trim",
"gt_sort": "sorter",
@ -4265,7 +4267,8 @@ var thegrid = (function () {
gfiles.style.display = 'none';
gfiles.innerHTML = (
'<div id="ghead" class="ghead">' +
'<a href="#" class="tgl btn" id="gridsel" tt="' + L.gt_msel + '</a> <span>' + L.gt_zoom + ': ' +
'<a href="#" class="tgl btn" id="gridsel" tt="' + L.gt_msel + '</a> ' +
'<a href="#" class="tgl btn" id="gridfull" tt="' + L.gt_full + '</a> <span>' + L.gt_zoom + ': ' +
'<a href="#" class="btn" z="-1.2" tt="Hotkey: shift-A">&ndash;</a> ' +
'<a href="#" class="btn" z="1.2" tt="Hotkey: shift-D">+</a></span> <span>' + L.gt_chop + ': ' +
'<a href="#" class="btn" l="-1" tt="' + L.gt_c1 + '">&ndash;</a> ' +
@ -4515,6 +4518,9 @@ var thegrid = (function () {
if (!r.dirty)
return r.loadsel();
if (dfull != r.full && !sread('gridfull'))
bcfg_upd_ui('gridfull', r.full = dfull);
var html = [],
svgs = new Set(),
max_svgs = CHROME ? 500 : 5000,
@ -4532,6 +4538,8 @@ var thegrid = (function () {
if (r.thumbs) {
ihref += '?th=' + (have_webp ? 'w' : 'j');
if (r.full)
ihref += 'f'
if (href == "#")
ihref = SR + '/.cpr/ico/⏏️';
}
@ -4618,6 +4626,7 @@ var thegrid = (function () {
};
bcfg_bind(r, 'thumbs', 'thumbs', true, r.setdirty);
bcfg_bind(r, 'full', 'gridfull', false, r.setdirty);
bcfg_bind(r, 'sel', 'gridsel', false, r.loadsel);
bcfg_bind(r, 'en', 'griden', dgrid, function (v) {
v ? loadgrid() : r.setvis(true);
@ -5823,6 +5832,7 @@ var treectl = (function () {
res.files[a].tags = {};
read_dsort(res.dsort);
dfull = res.dfull;
srvinf = res.srvinf;
try {