1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
|
// Real minified blob worker: both thumbnail sizes, animated originals,
// bounded concurrency and graceful fallback, with no page-thread encoding.
import assert from 'node:assert/strict';
import puppeteer from './puppeteer.js';
const bundle = await Bun.build({ entrypoints: ['src/image-thumbnails.js'], target: 'browser', minify: true });
assert(bundle.success);
const js = await bundle.outputs[0].text();
const server = Bun.serve({ port: 0, fetch: r => new URL(r.url).pathname === '/worker-client.js'
? new Response(js, { headers: { 'Content-Type': 'text/javascript' } })
: new Response('<script type="module">import {makeImageThumbnails} from "/worker-client.js";window.thumbnails=makeImageThumbnails;</script>', { headers: { 'Content-Type': 'text/html' } }) });
const browser = await puppeteer.launch({ executablePath: '/usr/bin/google-chrome', headless: true, args: ['--no-sandbox'] });
try {
const page = await browser.newPage();
await page.goto(server.url.href); await page.waitForFunction(() => window.thumbnails);
const result = await page.evaluate(async () => {
const canvas = new OffscreenCanvas(3000,2000), ctx=canvas.getContext('2d');
ctx.fillStyle='red';ctx.fillRect(0,0,3000,2000);ctx.fillStyle='blue';ctx.fillRect(500,0,2000,2000);
const blob=await canvas.convertToBlob({type:'image/png'});
const decode = createImageBitmap, encode = OffscreenCanvas.prototype.convertToBlob;
let mainCalls=0, pending=0, maxPending=0;
const RealWorker = Worker;
window.Worker = class extends RealWorker {
constructor(...args) { super(...args); this.addEventListener('message',()=>pending--); }
postMessage(...args) { pending++;maxPending=Math.max(maxPending,pending);super.postMessage(...args); }
};
window.createImageBitmap = () => {mainCalls++;throw Error('main-thread decode');};
OffscreenCanvas.prototype.convertToBlob = () => {mainCalls++;throw Error('main-thread encode');};
const [big, tiles, bad, recovered] = await Promise.all([
thumbnails(blob,{width:1100,tileSize:360}), thumbnails(blob,{tileSize:360}),
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 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,
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(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]);
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');
} finally { await browser.close();server.stop(true); }
|