From cc84dd0c6c852b46b309079a5a90a54080fc049c Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Thu, 22 Oct 2026 10:12:49 -0700 Subject: Images grid in justified rows too: tiles in each picture's own shape (the tile thumbnail is no longer a square crop: 360px tall, its middle cut to 1:2–2:1; vmedia pic.webp?s=row for hosts without CORS; tile cache v2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- tools/image-thumbnails-test.js | 19 ++++++++++++------- tools/media-grid-dups-test.js | 12 ++++++++++++ 2 files changed, 24 insertions(+), 7 deletions(-) (limited to 'tools') diff --git a/tools/image-thumbnails-test.js b/tools/image-thumbnails-test.js index ede7e11..6ece275 100644 --- a/tools/image-thumbnails-test.js +++ b/tools/image-thumbnails-test.js @@ -1,4 +1,4 @@ -// Real minified blob worker: both thumbnail sizes, animated originals, +// Real minified blob worker: both thumbnail sizes (the tile in the picture's own shape), animated originals, // bounded concurrency and graceful fallback, with no page-thread encoding. import assert from 'node:assert/strict'; import puppeteer from './puppeteer.js'; @@ -30,25 +30,30 @@ try { thumbnails(new Blob(['broken'],{type:'image/jpeg'}),{width:1100}), thumbnails(blob,{width:1100}), ]); const media = await decode(big.media), tile = await decode(big.tile); - const square = new OffscreenCanvas(360,360);square.getContext('2d').drawImage(tile,0,0); - const pixel=[...square.getContext('2d').getImageData(0,180,1,1).data]; + const whole = new OffscreenCanvas(tile.width,tile.height);whole.getContext('2d').drawImage(tile,0,0); + const pixel=[...whole.getContext('2d').getImageData(tile.width>>1,180,1,1).data], edge=[...whole.getContext('2d').getImageData(2,180,1,1).data]; + // a panorama past 2:1 keeps its middle, 2:1 + const pano=new OffscreenCanvas(4000,1000);pano.getContext('2d').fillStyle='green';pano.getContext('2d').fillRect(0,0,4000,1000); + const panoTile=await decode((await thumbnails(await encode.call(pano,{type:'image/png'}),{tileSize:360})).tile); const smallCanvas=new OffscreenCanvas(32,20);smallCanvas.getContext('2d'); const smallBlob=await encode.call(smallCanvas,{type:'image/png'}); const small=await thumbnails(smallBlob,{width:1100,tileSize:360}); const smallTile=await decode(small.tile); const gif=new Blob([Uint8Array.from(atob('R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'),c=>c.charCodeAt(0))],{type:'image/gif'}); const animated=await thumbnails(gif,{width:1100,tileSize:360}); - const out={mainCalls,maxPending,big:[big.width,big.height],media:[media.width,media.height],tile:[tile.width,tile.height],pixel, + const out={mainCalls,maxPending,big:[big.width,big.height],media:[media.width,media.height],tile:[tile.width,tile.height],pixel,edge,pano:[panoTile.width,panoTile.height], tileOnly:!tiles.media&&!!tiles.tile,bad,recovered:!!recovered.media,smallOriginal:small.media===null,smallTile:[smallTile.width,smallTile.height], animated: animated && [animated.width,animated.height,animated.media,animated.tile]}; media.close();tile.close();smallTile.close();return out; }); console.log(result); assert.equal(result.mainCalls,0);assert.equal(result.maxPending,1); - assert.deepEqual(result.big,[3000,2000]);assert.deepEqual(result.media,[1100,733]);assert.deepEqual(result.tile,[360,360]); - assert(result.pixel[2]>200&&result.pixel[0]<30,'tile crops the center'); + assert.deepEqual(result.big,[3000,2000]);assert.deepEqual(result.media,[1100,733]);assert.deepEqual(result.tile,[540,360]); + // the grid's justified rows: a tile in the picture's own shape, 360 tall, uncut up to 2:1 + assert(result.pixel[2]>200&&result.pixel[0]<30&&result.edge[0]>200,'tile keeps the whole picture'); + assert.deepEqual(result.pano,[720,360]); assert(result.tileOnly&&result.recovered&&result.smallOriginal);assert.equal(result.bad,null); - assert.deepEqual(result.smallTile,[20,20]);assert.deepEqual(result.animated,[1,1,null,null]); + assert.deepEqual(result.smallTile,[32,20]);assert.deepEqual(result.animated,[1,1,null,null]); const fallback=await browser.newPage();await fallback.goto(server.url.href);await fallback.waitForFunction(()=>window.thumbnails); assert.equal(await fallback.evaluate(async()=>{window.Worker=class {constructor(){throw Error('blocked');}};return thumbnails(new Blob(['x']),{width:1100});}),null); console.log('✓ thumbnails stay off the page thread; sizes, crop, originals, queue, bad-image recovery and worker fallback pass'); diff --git a/tools/media-grid-dups-test.js b/tools/media-grid-dups-test.js index eeb105e..948e727 100644 --- a/tools/media-grid-dups-test.js +++ b/tools/media-grid-dups-test.js @@ -18,6 +18,8 @@ const clip = (name, size) => { return f; }; const CLIPS = { wide: clip('wide', '320x180'), again: clip('wide', '320x180'), other: clip('wide', '320x180'), tall: clip('tall', '180x320'), last: clip('last', '240x240') }; +const pic = (size) => Bun.spawnSync(['ffmpeg', '-loglevel', 'error', '-f', 'lavfi', '-i', 'testsrc=size=' + size, '-frames:v', '1', '-f', 'image2', '-c:v', 'png', 'pipe:1']).stdout; +const picWide = pic('640x360'), picTall = pic('360x640'); const SK = generateSecretKey(), AUTHOR = getPublicKey(SK); const now = Math.floor(Date.now() / 1000); const html = await buildHtml({ minify: true, pwa: false }); @@ -30,6 +32,8 @@ const server = Bun.serve({ port: 0, fetch: (req) => { const a = +range[1], b = range[2] ? Math.min(+range[2], f.size - 1) : f.size - 1; return new Response(f.slice(a, b + 1), { status: 206, headers: { 'content-type': 'video/webm', 'accept-ranges': 'bytes', 'content-range': 'bytes ' + a + '-' + b + '/' + f.size, 'content-length': String(b - a + 1) } }); } + const pm = /^\/pic-(wide|tall)\.png$/.exec(path); + if (pm) return new Response(pm[1] === 'wide' ? picWide : picTall, { headers: { 'content-type': 'image/png', 'access-control-allow-origin': '*' } }); if (path === '/dead.webm') return new Response('gone', { status: 404 }); if (path === '/still.png') return new Response(png, { headers: { 'content-type': 'image/png' } }); if (path.startsWith('/punks') || path === '/verify-worker.js') return new Response(Bun.file('dist' + path)); @@ -46,6 +50,9 @@ const seeded = [ post('Transylvania lightning conference 2026 ' + U('again'), U('again'), 200), post('Transylvania lightning conference 2026 ' + U('other'), U('other'), 300), post('the last clip ' + U('last'), U('last'), 400), + // pictures, one wide and one tall, the post saying nothing of their size + finalizeEvent({ kind: 1, created_at: now - 120, tags: [], content: 'a wide view ' + server.url.origin + '/pic-wide.png' }, SK), + finalizeEvent({ kind: 1, created_at: now - 130, tags: [], content: 'a tall view ' + server.url.origin + '/pic-tall.png' }, SK), // a tall one: out of a wide screen's grid until "All shapes" finalizeEvent({ kind: 1, created_at: now - 450, tags: [['imeta', 'url ' + U('tall'), 'm video/webm', 'dim 180x320']], content: 'standing up ' + U('tall') }, SK), // a clip its host has since deleted (it says so: 404) — no blank tile for it @@ -115,6 +122,11 @@ try { const tallT = R.rows.flatMap((r) => r.ks.map((k, i) => ({ k, ar: r.ars[i] }))).find((x) => x.k === 'tall.webm'); check('…in justified rows: every tile its clip\'s shape (the tall one tall, the wide ones wide)', R.justified && tallT && Math.abs(tallT.ar - 0.5625) < 0.03 && R.rows.flatMap((r) => r.ks.map((k, i) => ({ k, ar: r.ars[i] }))).filter((x) => x.k !== 'tall.webm' && x.k !== 'last.webm').every((x) => Math.abs(x.ar - 16 / 9) < 0.03), JSON.stringify(R)); check('…one height across each row', R.rows.every((r) => Math.max(...r.hs) - Math.min(...r.hs) <= 1), JSON.stringify(R.rows.map((r) => r.hs))); + // the Images grid: justified rows too, each picture in its own shape + await page.evaluate(() => document.querySelector('.media-mode[data-mode="images"]').click()); + const picAr = (name) => page.evaluate((n) => { const t = [...document.querySelectorAll('.media-grid .media-tile')].find((x) => x.getAttribute('data-key').endsWith(n)); if (!t) return null; const r = t.getBoundingClientRect(); return +(r.width / r.height).toFixed(2); }, name); + check('the Images grid lays pictures out in justified rows, each its own shape', await waitFor(() => { const g = document.querySelector('.media-grid.justified'); if (!g) return false; const ar = (n) => { const t = [...g.querySelectorAll('.media-tile')].find((x) => x.getAttribute('data-key').endsWith(n)); if (!t) return 0; const r = t.getBoundingClientRect(); return r.width / r.height; }; return Math.abs(ar('pic-wide.png') - 16 / 9) < 0.05 && Math.abs(ar('pic-tall.png') - 9 / 16) < 0.05; }, null, 10000), + JSON.stringify({ wide: await picAr('pic-wide.png'), tall: await picAr('pic-tall.png') })); check('no page errors', errs.length === 0, errs.slice(0, 2).join(' | ')); } finally { await browser.close(); -- cgit v1.2.3