libjxl

FORK: libjxl patches used on blog
git clone https://git.neptards.moe/blog/libjxl.git
Log | Files | Refs | Submodules | README | LICENSE

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>