manual_decode_demo.html (9953B)
1 <html> 2 <head> 3 <link rel="icon" type="image/x-icon" href="favicon.ico"> 4 <style> 5 #log p { 6 margin: 0; 7 } 8 </style> 9 </head> 10 <body> 11 <div id="log" style="padding:2px; border: solid 1px #000; background-color: #ccc; margin:2px; height: 8em; font-family: monospace; overflow-y: auto; font-size: 8px;"></div> 12 <script> 13 // WASM module. 14 let jxlModule = null; 15 // Flag; if true, then HDR color space / 16 bit output is supported. 16 let hdrCanvas = false; 17 18 // Add message to "console". 19 let addMessage = (text, color) => { 20 let log = document.getElementById('log'); 21 let message = document.createElement('p'); 22 message.style = 'color: ' + color + ';'; 23 message.textContent = text; 24 log.append(message); 25 log.scrollTop = log.scrollHeight; 26 } 27 28 // Callback from WASM module when it becomes available. 29 let onLoadJxlModule = (module) => { 30 jxlModule = module; 31 addMessage('WASM module loaded', 'black'); 32 onJxlModuleReady(); 33 }; 34 35 // Check if multi-threading is supported (i.e. SharedArrayBuffer is allowed). 36 let probeMutlithreading = () => { 37 try { 38 new SharedArrayBuffer(); 39 return true; 40 } catch (ex) { 41 addMessage('Installing Service Worker, please wait...', 'orange'); 42 return false; 43 } 44 }; 45 46 // Check if HDR features are enabled. 47 let probeHdr = () => { 48 addMessage('Probing HDR features', 'black'); 49 try { 50 let tmpCanvas = document.createElement('canvas'); 51 tmpCanvas.width = 1; 52 tmpCanvas.height = 1; 53 let ctx = tmpCanvas.getContext('2d', {colorSpace: 'rec2100-pq', pixelFormat: 'float16'}); 54 // make it fail on firefox... 55 ctx.getContextAttributes(); 56 addMessage('HDR canvas supported', 'green'); 57 return true; 58 } catch (ex) { 59 addMessage(ex, 'red'); 60 addMessage('Are Blink experiments enabled? about://flags/#enable-experimental-web-platform-features', 'blue'); 61 return false; 62 } 63 }; 64 65 // "main" method executed after page is loaded; all scripts are "synchronous" elements, 66 // so it is guaranted that script elements are loaded and executed. 67 let onDomContentLoaded = () => { 68 if (!probeMutlithreading()) return; 69 hdrCanvas = probeHdr(); 70 JxlDecoderModule().then(onLoadJxlModule); 71 }; 72 73 // Pass next chunk to decoder and interprets result. 74 let processInput = (img, chunkLen) => { 75 let response = { 76 wantFlush: false, 77 copyPixels: false, 78 error: false, 79 } 80 do { 81 let t0 = performance.now(); 82 let result = jxlModule._jxlProcessInput(img.decoder, img.buffer, chunkLen); 83 let t1 = performance.now(); 84 let tProcessing = t1 - t0; 85 // addMessage('Processed chunk in ' + tProcessing + 'ms', 'blue'); 86 img.totalProcessing += tProcessing; 87 // addMessage('Process result: ' + result, 'green'); 88 if (result === 2) { 89 addMessage('Needs more input', 'gray'); 90 } else if (result === 0) { 91 // addMessage('Image ready', 'gray'); 92 response.wantFlush = false; 93 response.copyPixels = true; 94 } else if (result === 1) { 95 if (img.wantProgressive) { 96 addMessage('DC ready', 'gray'); 97 response.wantFlush = true; 98 response.copyPixels = true; 99 } else { 100 // addMessage('Skipping DC flush', 'gray'); 101 chunkLen = 0; 102 continue; 103 } 104 } else { 105 addMessage('Processing error', 'red'); 106 img.broken = true; 107 response.error = true; 108 break; 109 } 110 break; 111 } while (true); 112 return response; 113 } 114 115 // Decode chunk and present results (dump to canvas). 116 let processChunk = (img, chunkLen) => { 117 let result = processInput(img, chunkLen); 118 if (result.error) return; 119 120 if (result.wantFlush) { 121 let t2 = performance.now(); 122 let flushResult = jxlModule._jxlFlush(img.decoder); 123 let t3 = performance.now(); 124 let tFlushing = t3 - t2; 125 addMessage('Flush result: ' + flushResult, 'gray'); 126 img.totalFlushing += tFlushing; 127 } 128 129 if (!result.copyPixels) return; 130 131 let w = jxlModule.HEAP32[img.decoder >> 2]; 132 let h = jxlModule.HEAP32[(img.decoder + 4) >> 2]; 133 let pixelData = jxlModule.HEAP32[(img.decoder + 8) >> 2]; 134 if (!img.canvas) { 135 img.canvas = document.createElement('canvas'); 136 img.canvas.width = w; 137 img.canvas.height = h; 138 img.canvas.style = 'width:100%'; 139 // TODO(eustas): postpone until really flushed 140 document.body.appendChild(img.canvas); 141 let ctxOptions = {colorSpace: img.colorSpace, pixelFormat: 'float16'}; 142 let pixelOptions = {colorSpace: img.colorSpace, storageFormat: 'uint16'}; 143 if (img.wantSdr) { 144 ctxOptions = null; 145 pixelOptions = null; 146 } 147 img.canvasCtx = img.canvas.getContext('2d', ctxOptions); 148 img.pixels = img.canvasCtx.getImageData(0, 0, w, h, pixelOptions); 149 } 150 151 let src = null; 152 let start = pixelData; 153 if (img.wantSdr) { 154 src = new Uint8Array(jxlModule.HEAP8.buffer); 155 } else { 156 src = new Uint16Array(jxlModule.HEAP8.buffer); 157 start = start >> 1; 158 } 159 let end = start + w * h * 4; 160 img.pixels.data.set(src.slice(start, end)); 161 img.canvasCtx.putImageData(img.pixels, 0, 0); 162 }; 163 164 const BUF_LEN = 150 * 1024; 165 166 // Image data cache for benchmarking. 167 let fullImage = new Uint8Array(0); 168 169 // Callback for fetch data. 170 let onChunk = (img, chunk) => { 171 if (chunk.done) { 172 addMessage('Read finished | total processing: ' + img.totalProcessing.toFixed(1) + 'ms | total flushing ' + img.totalFlushing.toFixed(1) + 'ms', 'black'); 173 cleanup(img); 174 img.onComplete(img); 175 return; 176 } 177 if (img.broken) return; 178 179 if (!img.decoder) { 180 let decoder = jxlModule._jxlCreateInstance(img.wantSdr, img.displayNits); 181 if (decoder < 4) { 182 img.broken = true; 183 cleanup(img); 184 addMessage('Failed to create decoder instance', 'red'); 185 return; 186 } 187 img.decoder = decoder; 188 img.buffer = jxlModule._malloc(BUF_LEN); 189 } 190 191 // addMessage('Received chunk: ' + chunk.value.length, 'gray'); 192 let newFullImage = new Uint8Array(fullImage.length + chunk.value.length); 193 newFullImage.set(fullImage); 194 newFullImage.set(chunk.value, fullImage.length); 195 fullImage = newFullImage; 196 197 let offset = 0; 198 while (offset < chunk.value.length) { 199 let delta = chunk.value.length - offset; 200 if (delta > BUF_LEN) delta = BUF_LEN; 201 jxlModule.HEAP8.set(chunk.value.slice(offset, offset + delta), img.buffer); 202 offset += delta; 203 processChunk(img, delta); 204 if (img.broken) { 205 return; 206 } 207 } 208 209 // Break the promise chain. 210 setTimeout(img.proceed, 0); 211 }; 212 213 // Read next chunk; NB: used to break promise chain. 214 let proceed = (img) => { 215 img.reader.read().then(img.onChunk, img.onReadError); 216 }; 217 218 // Release (in-module) memory resources. 219 let cleanup = (img) => { 220 if (img.decoder) { 221 jxlModule._jxlDestroyInstance(img.decoder); 222 img.decoder = 0; 223 } 224 if (img.buffer) { 225 jxlModule._free(img.buffer); 226 img.buffer = 0; 227 } 228 }; 229 230 // Report error and cleanup. 231 let onReadError = (img, error) => { 232 img.broken = true; 233 cleanup(img); 234 addMessage('Read failed: ' + error, 'red'); 235 }; 236 237 // On successful fetch start. 238 let onResponse = (img, response) => { 239 if (!response.ok) { 240 addMessage('Fetch failed: ' + response.status + ' (' + response.statusText + ')'); 241 return; 242 } 243 // Alas, not supported by fetch: 244 // let reader = response.body.getReader({mode: "byob"}); 245 img.reader = response.body.getReader(); 246 247 img.proceed(); 248 }; 249 250 // On image decoding completion. 251 let onComplete = (img) => { 252 if (!img.runBenchmark) return; 253 254 let buffer = jxlModule._malloc(fullImage.length); 255 jxlModule.HEAP8.set(fullImage, buffer); 256 img.buffer = buffer; 257 let results = []; 258 259 for (let i = 0; i < img.runBenchmark; ++i) { 260 img.totalProcessing = 0; 261 img.decoder = jxlModule._jxlCreateInstance(img.wantSdr, img.displayNits); 262 processChunk(img, fullImage.length); 263 jxlModule._jxlDestroyInstance(img.decoder); 264 results.push(img.totalProcessing); 265 //addMessage('Decoding time: ' + img.totalProcessing + 'ms', 'black'); 266 } 267 268 results.sort(); 269 addMessage('Min decoding time: ' + results[0].toFixed(3) + 'ms', 'black'); 270 addMessage('Median decoding time: ' + results[results.length >> 1].toFixed(3) + 'ms', 'black'); 271 addMessage('Max decoding time: ' + results[results.length - 1].toFixed(3) + 'ms', 'black'); 272 273 jxlModule._free(buffer); 274 }; 275 276 // Fill cookie object template. 277 let makeImg = () => { 278 return { 279 name: '', 280 colorSpace: 'rec2100-pq', 281 wantSdr: false, 282 displayNits: 100, 283 broken: false, 284 decoder: 0, 285 canvas: null, 286 canvasCtx: null, 287 pixels: null, 288 buffer: 0, 289 wantProgressive: false, 290 onlyDecode: false, 291 totalProcessing: 0, 292 totalFlushing: 0, 293 runBenchmark: 0, 294 onChunk: () => {}, 295 onReadError: () => {}, 296 proceed: () => {}, 297 onComplete: () => {}, 298 }; 299 } 300 301 // Parse URL query and run image decoding / benchmarking. 302 let onJxlModuleReady = () => { 303 let params = (new URL(document.location)).searchParams; 304 const images = ['image00.jxl', 'image01.jxl']; 305 let imgIdx = (params.get('img') | 0) % images.length; 306 let imgName = images[imgIdx]; 307 308 let colorSpace = params.get('colorSpace') || 'srgb'; 309 let wantSdr = params.get('wantSdr') == 'true'; 310 let displayNits = parseInt(params.get('displayNits') || '0'); 311 let runBenchmark = parseInt(params.get('runBenchmark') || '0'); 312 313 if (!hdrCanvas) { 314 colorSpace = 'srgb-linear'; 315 displayNits = displayNits || 100; 316 wantSdr = true; 317 } 318 319 addMessage('Color-space: "' + colorSpace + '", tone-map to SDR: ' + wantSdr + ', displayNits: ' + (displayNits || 'n/a'), 'black'); 320 321 let img = makeImg(); 322 img.name = imgName; 323 img.colorSpace = colorSpace; 324 img.wantSdr = wantSdr; 325 img.displayNits = displayNits; 326 img.onChunk = onChunk.bind(null, img); 327 img.onReadError = onReadError.bind(null, img); 328 img.proceed = proceed.bind(null, img); 329 img.onComplete = onComplete.bind(null, img); 330 img.runBenchmark = runBenchmark; 331 332 fetch(new Request(imgName, {cache: "no-store"})).then(onResponse.bind(null, img)); 333 }; 334 335 document.addEventListener('DOMContentLoaded', onDomContentLoaded); 336 </script> 337 338 <script src="jxl_decoder.js"></script> 339 </body> 340 </html>