1<!-- 2-------------------------------------- 3HTML QPA Image Viewer 4-------------------------------------- 5 6Copyright (c) 2020 The Khronos Group Inc. 7Copyright (c) 2020 Valve Corporation. 8 9Licensed under the Apache License, Version 2.0 (the "License"); 10you may not use this file except in compliance with the License. 11You may obtain a copy of the License at 12 13http://www.apache.org/licenses/LICENSE-2.0 14 15Unless required by applicable law or agreed to in writing, software 16distributed under the License is distributed on an "AS IS" BASIS, 17WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 18See the License for the specific language governing permissions and 19limitations under the License. 20--> 21<html> 22 <head> 23 <meta charset="utf-8"/> 24 <title>Load PNGs from QPA output</title> 25 <style> 26 body { 27 background: white; 28 text-align: left; 29 font-family: sans-serif; 30 } 31 h1 { 32 margin-top: 2ex; 33 } 34 h2 { 35 font-size: large; 36 } 37 figure { 38 display: flex; 39 flex-direction: column; 40 41 /* Taken from https://stackoverflow.com/a/25709375. A grid pattern 42 so that images are easier to see with transparency. */ 43 background: 44 linear-gradient(-90deg, rgba(0,0,0,.05) 1px, transparent 1px), 45 linear-gradient(rgba(0,0,0,.05) 1px, transparent 1px), 46 linear-gradient(-90deg, rgba(0, 0, 0, .04) 1px, transparent 1px), 47 linear-gradient(rgba(0,0,0,.04) 1px, transparent 1px), 48 linear-gradient(transparent 3px, #f2f2f2 3px, #f2f2f2 78px, transparent 78px), 49 linear-gradient(-90deg, #aaa 1px, transparent 1px), 50 linear-gradient(-90deg, transparent 3px, #f2f2f2 3px, #f2f2f2 78px, transparent 78px), 51 linear-gradient(#aaa 1px, transparent 1px), 52 #f2f2f2; 53 background-size: 54 4px 4px, 55 4px 4px, 56 80px 80px, 57 80px 80px, 58 80px 80px, 59 80px 80px, 60 80px 80px, 61 80px 80px; 62 } 63 img { 64 margin-left: 1ex; 65 margin-right: 1ex; 66 margin-bottom: 1ex; 67 /* Attempt to zoom images using the nearest-neighbor scaling 68 algorithm. */ 69 image-rendering: pixelated; 70 image-rendering: crisp-edges; 71 /* Border around images. */ 72 border: 1px solid darkgrey; 73 } 74 button { 75 margin: 1ex; 76 border: none; 77 border-radius: .5ex; 78 padding: 1ex; 79 background-color: steelblue; 80 color: white; 81 font-size: large; 82 } 83 button:hover { 84 opacity: .8; 85 } 86 #clearimagesbutton,#cleartextbutton { 87 background-color: seagreen; 88 } 89 select { 90 font-size: large; 91 padding: 1ex; 92 border-radius: .5ex; 93 border: 1px solid darkgrey; 94 } 95 select:hover { 96 opacity: .8; 97 } 98 .loadoption { 99 text-align: center; 100 margin: 1ex; 101 padding: 2ex; 102 border: 1px solid darkgrey; 103 border-radius: 1ex; 104 } 105 #options { 106 display: flex; 107 flex-wrap: wrap; 108 } 109 #qpatext { 110 display: block; 111 min-width: 80ex; 112 max-width: 132ex; 113 min-height: 25ex; 114 max-height: 25ex; 115 margin: 1ex auto; 116 } 117 #fileselector { 118 display: none; 119 } 120 #zoomandclear { 121 margin: 2ex; 122 } 123 #images { 124 margin: 2ex; 125 display: flex; 126 flex-direction: column; 127 } 128 .imagesblock { 129 display: flex; 130 flex-wrap: wrap; 131 } 132 .pathheader { 133 font-family: monospace; 134 font-size: large; 135 } 136 .subtext { 137 font-family: monospace; 138 font-size: smaller; 139 } 140 </style> 141 </head> 142 <body> 143 <h1>Load PNGs from QPA output</h1> 144 145 <div id="options"> 146 <div class="loadoption"> 147 <h2>Option 1: Load local QPA files</h2> 148 <!-- The file selector text cannot be changed, so we use a hidden selector trick. --> 149 <button id="fileselectorbutton">📂 Load files</button> 150 <input id="fileselector" type="file" multiple> 151 </div> 152 153 <div class="loadoption"> 154 <h2>Option 2: Paste QPA text or text extract containing <Image> elements below and click "Load images"</h2> 155 <textarea id="qpatext"></textarea> 156 <button id="loadimagesbutton">📃 Load images</button> 157 <button id="cleartextbutton">♻ Clear text</button> 158 </div> 159 </div> 160 161 <div id="zoomandclear"> 162 🔎 Image zoom 163 <select id="zoomselect"> 164 <option value="1" selected>1x</option> 165 <option value="2">2x</option> 166 <option value="4">4x</option> 167 <option value="6">6x</option> 168 <option value="8">8x</option> 169 <option value="16">16x</option> 170 <option value="32">32x</option> 171 </select> 172 <button id="clearimagesbutton">♻ Clear images</button> 173 </div> 174 175 <div id="images"></div> 176 177 <script> 178 // Returns zoom factor as a number. 179 var getSelectedZoom = function () { 180 return new Number(document.getElementById("zoomselect").value); 181 } 182 183 // Scales a given image with the selected zoom factor. 184 var scaleSingleImage = function (img) { 185 var factor = getSelectedZoom(); 186 img.style.width = (img.naturalWidth * factor) + "px"; 187 img.style.height = (img.naturalHeight * factor) + "px"; 188 } 189 190 // Rescales all <img> elements in the page. Used after changing the selected zoom. 191 var rescaleImages = function () { 192 var imageList = document.getElementsByTagName("img"); 193 for (var i = 0; i < imageList.length; i++) { 194 scaleSingleImage(imageList[i]) 195 } 196 } 197 198 // Removes everything contained in the images <div>. 199 var clearImages = function () { 200 var imagesNode = document.getElementById("images"); 201 while (imagesNode.hasChildNodes()) { 202 imagesNode.removeChild(imagesNode.lastChild); 203 } 204 } 205 206 // Clears textarea text. 207 var clearText = function() { 208 document.getElementById("qpatext").value = ""; 209 } 210 211 // Returns a properly sized image with the given base64-encoded PNG data. 212 var createImage = function (pngData, imageName, imageFormat, imageDimensions, imageDescription) { 213 var imageContainer = document.createElement("figure"); 214 if (imageName.length > 0) { 215 var newFileNameHeader = document.createElement("figcaption"); 216 newFileNameHeader.textContent = imageName; 217 newFileNameHeader.style.fontWeight = "bold"; 218 newFileNameHeader.style.textAlign = "center"; 219 220 if (imageDescription.length > 0) { 221 var newDescription = document.createElement("span"); 222 newDescription.className = "subtext"; 223 newDescription.textContent = decodeURI(imageDescription).replace(/'/g,'\'').replace(/"/g,'"'); 224 225 newFileNameHeader.appendChild(document.createElement("br")); 226 newFileNameHeader.appendChild(newDescription); 227 } 228 229 if (imageFormat.length > 0 || imageDimensions.length > 0) { 230 var newSubText = document.createElement("span"); 231 newSubText.className = "subtext"; 232 233 newSubText.textContent = "("; 234 if (imageDimensions.length > 0) 235 newSubText.textContent += imageDimensions; 236 if (imageFormat.length > 0) { 237 if (imageDimensions.length > 0) 238 newSubText.textContent += " "; 239 newSubText.textContent += imageFormat; 240 } 241 newSubText.textContent += ")"; 242 243 newFileNameHeader.appendChild(document.createElement("br")); 244 newFileNameHeader.appendChild(newSubText); 245 } 246 imageContainer.appendChild(newFileNameHeader); 247 } 248 249 var newImage = document.createElement("img"); 250 newImage.src = "data:image/png;base64," + pngData; 251 newImage.style.alignSelf = "center"; 252 newImage.onload = (function () { 253 // Grab the image for the callback. We need to wait until 254 // the image has been properly loaded to access its 255 // naturalWidth and naturalHeight properties, needed for 256 // scaling. 257 var cbImage = newImage; 258 return function () { 259 scaleSingleImage(cbImage); 260 }; 261 })(); 262 imageContainer.appendChild(newImage); 263 return imageContainer; 264 } 265 266 // Returns a new h3 header with the given file name. 267 var createFileNameHeader = function (fileName) { 268 var newHeader = document.createElement("h3"); 269 newHeader.textContent = fileName; 270 return newHeader; 271 } 272 273 // Returns a new image block to contain images from a file. 274 var createImagesBlock = function () { 275 var imagesBlock = document.createElement("div"); 276 imagesBlock.className = "imagesblock"; 277 return imagesBlock; 278 } 279 280 // Returns a new test case header. 281 var createTestCaseHeader = function (testCasePath) { 282 var header = document.createElement("h4"); 283 header.textContent = testCasePath; 284 header.className = "pathheader"; 285 return header; 286 } 287 288 // Processes a single test case from the given text string. Creates 289 // a list of images in the given images block, as found in the 290 // text. 291 var processTestCase = function(textString, imagesBlock) { 292 // [\s\S] is a match-anything regexp like the dot, except it 293 // also matches newlines. Ideally, browsers would need to widely 294 // support the "dotall" regexp modifier, but that's not the case 295 // yet and this does the trick. 296 // Group 1 are the image element properties, if any. 297 // Group 2 is the base64 PNG data. 298 var imageRegexp = /<Image\b(.*?)>([\s\S]*?)<\/Image>/g; 299 var imageNameRegexp = /\bName="(.*?)"/; 300 var imageFormatRegexp = /\bFormat="(.*?)"/; 301 var imageWidthRegexp = /\bWidth="(.*?)"/; 302 var imageHeightRegexp = /\bHeight="(.*?)"/; 303 var imageDescRegexp = /\bDescription="(.*?)"/; 304 305 var result; 306 307 var innerResult; 308 var imageName; 309 var imageFormat; 310 var imageDimensions = ""; 311 var imageDescription; 312 313 while ((result = imageRegexp.exec(textString)) !== null) { 314 innerResult = result[1].match(imageNameRegexp); 315 imageName = ((innerResult !== null) ? innerResult[1] : ""); 316 317 innerResult = result[1].match(imageFormatRegexp); 318 imageFormat = ((innerResult !== null) ? innerResult[1] : ""); 319 320 innerResult = result[1].match(imageWidthRegexp); 321 var imageWidth = ((innerResult !== null) ? innerResult[1] : ""); 322 323 innerResult = result[1].match(imageHeightRegexp); 324 var imageHeight = ((innerResult !== null) ? innerResult[1] : ""); 325 326 if ((imageWidth.length > 0) && (imageHeight.length > 0)) 327 imageDimensions = imageWidth + "x" + imageHeight; 328 329 innerResult = result[1].match(imageDescRegexp); 330 imageDescription = ((innerResult !== null) ? innerResult[1] : ""); 331 332 // Blanks need to be removed from the base64 string. 333 var pngData = result[2].replace(/\s+/g, ""); 334 imagesBlock.appendChild(createImage(pngData, imageName, imageFormat, imageDimensions, imageDescription)); 335 } 336 } 337 338 var getTestCaseResultRegexp = function () { 339 return new RegExp(/#beginTestCaseResult\s([^\n]+)\n([\S\s]*?)#endTestCaseResult/g); 340 } 341 342 // Processes a chunk of QPA text from the given file name. Creates 343 // the file name header, the test case header and a list of images 344 // in the images <div>, as found in the text. 345 var processText = function(textString, fileName) { 346 var imagesNode = document.getElementById("images"); 347 if (fileName.length > 0) { 348 var newHeader = createFileNameHeader(fileName); 349 imagesNode.appendChild(newHeader); 350 } 351 352 var imagesDiv = document.createElement("div"); 353 var testCaseResultRegexp = getTestCaseResultRegexp(); 354 var result; 355 356 while ((result = testCaseResultRegexp.exec(textString)) !== null) { 357 var testCasePathHeader = createTestCaseHeader(result[1]); 358 imagesDiv.appendChild(testCasePathHeader); 359 360 var imagesBlock = createImagesBlock(); 361 processTestCase(result[2], imagesBlock); 362 imagesDiv.appendChild(imagesBlock); 363 } 364 365 imagesNode.appendChild(imagesDiv); 366 } 367 368 // Loads images from the text in the text area. 369 var loadImages = function () { 370 var textString = document.getElementById("qpatext").value; 371 var testRE = getTestCaseResultRegexp(); 372 373 // Full case being pasted. 374 if (testRE.test(textString)) { 375 processText(textString, "<Pasted Text>"); 376 } 377 else { 378 // Excerpt from an unknown test case. 379 var imagesNode = document.getElementById("images"); 380 var fileNameHeader = createFileNameHeader("<Pasted Text>"); 381 imagesNode.appendChild(fileNameHeader); 382 var imagesDiv = document.createElement("div"); 383 var imagesBlock = createImagesBlock(); 384 var testCasePathHeader = createTestCaseHeader("<Unknown test case>"); 385 imagesDiv.appendChild(testCasePathHeader); 386 processTestCase(textString, imagesBlock); 387 imagesDiv.appendChild(imagesBlock); 388 imagesNode.appendChild(imagesDiv); 389 } 390 } 391 392 // Loads images from the files in the file selector. 393 var handleFileSelect = function (evt) { 394 var files = evt.target.files; 395 for (var i = 0; i < files.length; i++) { 396 // Creates a reader per file. 397 var reader = new FileReader(); 398 // Grab the needed objects to use them after the file has 399 // been read, in order to process its contents and add 400 // images, if found, in the images <div>. 401 reader.onload = (function () { 402 var cbFileName = files[i].name; 403 var cbReader = reader; 404 return function () { 405 processText(cbReader.result, cbFileName); 406 }; 407 })(); 408 // Reads file contents. This will trigger the event above. 409 reader.readAsText(files[i]); 410 } 411 } 412 413 // File selector trick: click on the selector when clicking on the 414 // custom button. 415 var clickFileSelector = function () { 416 document.getElementById("fileselector").click(); 417 } 418 419 // Clears selected files to be able to select them again if needed. 420 var clearSelectedFiles = function() { 421 document.getElementById("fileselector").value = ""; 422 } 423 424 // Set event handlers for interactive elements in the page. 425 document.getElementById("fileselector").onclick = clearSelectedFiles; 426 document.getElementById("fileselector").addEventListener("change", handleFileSelect, false); 427 document.getElementById("fileselectorbutton").onclick = clickFileSelector; 428 document.getElementById("loadimagesbutton").onclick = loadImages; 429 document.getElementById("cleartextbutton").onclick = clearText; 430 document.getElementById("zoomselect").onchange = rescaleImages; 431 document.getElementById("clearimagesbutton").onclick = clearImages; 432 </script> 433 </body> 434</html> 435