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 img { 42 margin-right: 1ex; 43 margin-bottom: 1ex; 44 /* Attempt to zoom images using the nearest-neighbor scaling 45 algorithm. */ 46 image-rendering: pixelated; 47 image-rendering: crisp-edges; 48 /* Use a black background color for images in case some pixels 49 are transparent to some degree. In the worst case, the image 50 could appear to be missing. */ 51 background: black; 52 } 53 button { 54 margin: 1ex; 55 border: none; 56 border-radius: .5ex; 57 padding: 1ex; 58 background-color: steelblue; 59 color: white; 60 font-size: large; 61 } 62 button:hover { 63 opacity: .8; 64 } 65 #clearimagesbutton,#cleartextbutton { 66 background-color: seagreen; 67 } 68 select { 69 font-size: large; 70 padding: 1ex; 71 border-radius: .5ex; 72 border: 1px solid darkgrey; 73 } 74 select:hover { 75 opacity: .8; 76 } 77 .loadoption { 78 text-align: center; 79 margin: 1ex; 80 padding: 2ex; 81 border: 1px solid darkgrey; 82 border-radius: 1ex; 83 } 84 #options { 85 display: flex; 86 flex-wrap: wrap; 87 } 88 #qpatext { 89 display: block; 90 min-width: 80ex; 91 max-width: 132ex; 92 min-height: 25ex; 93 max-height: 25ex; 94 margin: 1ex auto; 95 } 96 #fileselector { 97 display: none; 98 } 99 #zoomandclear { 100 margin: 2ex; 101 } 102 #images { 103 margin: 2ex; 104 display: flex; 105 flex-direction: column; 106 } 107 .imagesblock { 108 display: flex; 109 flex-wrap: wrap; 110 } 111 </style> 112 </head> 113 <body> 114 <h1>Load PNGs from QPA output</h1> 115 116 <div id="options"> 117 <div class="loadoption"> 118 <h2>Option 1: Load local QPA files</h2> 119 <!-- The file selector text cannot be changed, so we use a hidden selector trick. --> 120 <button id="fileselectorbutton">📂 Load files</button> 121 <input id="fileselector" type="file" multiple> 122 </div> 123 124 <div class="loadoption"> 125 <h2>Option 2: Paste QPA text or text extract containing <Image> elements below and click "Load images"</h2> 126 <textarea id="qpatext"></textarea> 127 <button id="loadimagesbutton">📃 Load images</button> 128 <button id="cleartextbutton">♻ Clear text</button> 129 </div> 130 </div> 131 132 <div id="zoomandclear"> 133 🔎 Image zoom 134 <select id="zoomselect"> 135 <option value="1" selected>1x</option> 136 <option value="2">2x</option> 137 <option value="4">4x</option> 138 <option value="8">8x</option> 139 <option value="16">16x</option> 140 <option value="32">32x</option> 141 </select> 142 <button id="clearimagesbutton">♻ Clear images</button> 143 </div> 144 145 <div id="images"></div> 146 147 <script> 148 // Returns zoom factor as a number. 149 var getSelectedZoom = function () { 150 return new Number(document.getElementById("zoomselect").value); 151 } 152 153 // Scales a given image with the selected zoom factor. 154 var scaleSingleImage = function (img) { 155 var factor = getSelectedZoom(); 156 img.style.width = (img.naturalWidth * factor) + "px"; 157 img.style.height = (img.naturalHeight * factor) + "px"; 158 } 159 160 // Rescales all <img> elements in the page. Used after changing the selected zoom. 161 var rescaleImages = function () { 162 var imageList = document.getElementsByTagName("img"); 163 for (var i = 0; i < imageList.length; i++) { 164 scaleSingleImage(imageList[i]) 165 } 166 } 167 168 // Removes everything contained in the images <div>. 169 var clearImages = function () { 170 var imagesNode = document.getElementById("images"); 171 while (imagesNode.hasChildNodes()) { 172 imagesNode.removeChild(imagesNode.lastChild); 173 } 174 } 175 176 // Clears textarea text. 177 var clearText = function() { 178 document.getElementById("qpatext").value = ""; 179 } 180 181 // Returns a properly sized image with the given base64-encoded PNG data. 182 var createImage = function (pngData, imageName) { 183 var imageContainer = document.createElement("figure"); 184 if (imageName.length > 0) { 185 var newFileNameHeader = document.createElement("figcaption"); 186 newFileNameHeader.textContent = escape(imageName); 187 imageContainer.appendChild(newFileNameHeader); 188 } 189 var newImage = document.createElement("img"); 190 newImage.src = "data:image/png;base64," + pngData; 191 newImage.onload = (function () { 192 // Grab the image for the callback. We need to wait until 193 // the image has been properly loaded to access its 194 // naturalWidth and naturalHeight properties, needed for 195 // scaling. 196 var cbImage = newImage; 197 return function () { 198 scaleSingleImage(cbImage); 199 }; 200 })(); 201 imageContainer.appendChild(newImage); 202 return imageContainer; 203 } 204 205 // Returns a new h3 header with the given file name. 206 var createFileNameHeader = function (fileName) { 207 var newHeader = document.createElement("h3"); 208 newHeader.textContent = fileName; 209 return newHeader; 210 } 211 212 // Returns a new image block to contain images from a file. 213 var createImagesBlock = function () { 214 var imagesBlock = document.createElement("div"); 215 imagesBlock.className = "imagesblock"; 216 return imagesBlock; 217 } 218 219 // Processes a chunk of QPA text from the given file name. Creates 220 // the file name header and a list of images in the images <div>, as 221 // found in the text. 222 var processText = function(textString, fileName) { 223 var imagesNode = document.getElementById("images"); 224 var newHeader = createFileNameHeader(fileName); 225 imagesNode.appendChild(newHeader); 226 var imagesBlock = createImagesBlock(); 227 // [\s\S] is a match-anything regexp like the dot, except it 228 // also matches newlines. Ideally, browsers would need to widely 229 // support the "dotall" regexp modifier, but that's not the case 230 // yet and this does the trick. 231 // Group 1 are the image element properties, if any. 232 // Group 2 is the base64 PNG data. 233 var imageRegexp = /<Image\b(.*?)>([\s\S]*?)<\/Image>/g; 234 var imageNameRegexp = /\bName="(.*?)"/; 235 var result; 236 var innerResult; 237 var imageName; 238 while ((result = imageRegexp.exec(textString)) !== null) { 239 innerResult = result[1].match(imageNameRegexp); 240 imageName = ((innerResult !== null) ? innerResult[1] : ""); 241 // Blanks need to be removed from the base64 string. 242 var pngData = result[2].replace(/\s+/g, ""); 243 imagesBlock.appendChild(createImage(pngData, imageName)); 244 } 245 imagesNode.appendChild(imagesBlock); 246 } 247 248 // Loads images from the text in the text area. 249 var loadImages = function () { 250 processText(document.getElementById("qpatext").value, "<Pasted Text>"); 251 } 252 253 // Loads images from the files in the file selector. 254 var handleFileSelect = function (evt) { 255 var files = evt.target.files; 256 for (var i = 0; i < files.length; i++) { 257 // Creates a reader per file. 258 var reader = new FileReader(); 259 // Grab the needed objects to use them after the file has 260 // been read, in order to process its contents and add 261 // images, if found, in the images <div>. 262 reader.onload = (function () { 263 var cbFileName = files[i].name; 264 var cbReader = reader; 265 return function () { 266 processText(cbReader.result, cbFileName); 267 }; 268 })(); 269 // Reads file contents. This will trigger the event above. 270 reader.readAsText(files[i]); 271 } 272 } 273 274 // File selector trick: click on the selector when clicking on the 275 // custom button. 276 var clickFileSelector = function () { 277 document.getElementById("fileselector").click(); 278 } 279 280 // Clears selected files to be able to select them again if needed. 281 var clearSelectedFiles = function() { 282 document.getElementById("fileselector").value = ""; 283 } 284 285 // Set event handlers for interactive elements in the page. 286 document.getElementById("fileselector").onclick = clearSelectedFiles; 287 document.getElementById("fileselector").addEventListener("change", handleFileSelect, false); 288 document.getElementById("fileselectorbutton").onclick = clickFileSelector; 289 document.getElementById("loadimagesbutton").onclick = loadImages; 290 document.getElementById("cleartextbutton").onclick = clearText; 291 document.getElementById("zoomselect").onchange = rescaleImages; 292 document.getElementById("clearimagesbutton").onclick = clearImages; 293 </script> 294 </body> 295</html> 296