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. Unfortunately, not supported under Firefox at the 46 time this text is being written. */ 47 image-rendering: pixelated; 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 { 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 </div> 129 </div> 130 131 <div id="zoomandclear"> 132 🔎 Image zoom 133 <select id="zoomselect"> 134 <option value="1" selected>1x</option> 135 <option value="2">2x</option> 136 <option value="4">4x</option> 137 <option value="8">8x</option> 138 <option value="16">16x</option> 139 <option value="32">32x</option> 140 </select> 141 <button id="clearimagesbutton">♻ Clear images</button> 142 </div> 143 144 <div id="images"></div> 145 146 <script> 147 // Returns zoom factor as a number. 148 var getSelectedZoom = function () { 149 return new Number(document.getElementById("zoomselect").value); 150 } 151 152 // Scales a given image with the selected zoom factor. 153 var scaleSingleImage = function (img) { 154 var factor = getSelectedZoom(); 155 img.style.width = (img.naturalWidth * factor) + "px"; 156 img.style.height = (img.naturalHeight * factor) + "px"; 157 } 158 159 // Rescales all <img> elements in the page. Used after changing the selected zoom. 160 var rescaleImages = function () { 161 var imageList = document.getElementsByTagName("img"); 162 for (var i = 0; i < imageList.length; i++) { 163 scaleSingleImage(imageList[i]) 164 } 165 } 166 167 // Removes everything contained in the images <div>. 168 var clearImages = function () { 169 var imagesNode = document.getElementById("images"); 170 while (imagesNode.hasChildNodes()) { 171 imagesNode.removeChild(imagesNode.lastChild); 172 } 173 } 174 175 // Returns a properly sized image with the given base64-encoded PNG data. 176 var createImage = function (pngData, imageName) { 177 var imageContainer = document.createElement("figure"); 178 if (imageName.length > 0) { 179 var newFileNameHeader = document.createElement("figcaption"); 180 newFileNameHeader.textContent = escape(imageName); 181 imageContainer.appendChild(newFileNameHeader); 182 } 183 var newImage = document.createElement("img"); 184 newImage.src = "data:image/png;base64," + pngData; 185 newImage.onload = (function () { 186 // Grab the image for the callback. We need to wait until 187 // the image has been properly loaded to access its 188 // naturalWidth and naturalHeight properties, needed for 189 // scaling. 190 var cbImage = newImage; 191 return function () { 192 scaleSingleImage(cbImage); 193 }; 194 })(); 195 imageContainer.appendChild(newImage); 196 return imageContainer; 197 } 198 199 // Returns a new h3 header with the given file name. 200 var createFileNameHeader = function (fileName) { 201 var newHeader = document.createElement("h3"); 202 newHeader.textContent = fileName; 203 return newHeader; 204 } 205 206 // Returns a new image block to contain images from a file. 207 var createImagesBlock = function () { 208 var imagesBlock = document.createElement("div"); 209 imagesBlock.className = "imagesblock"; 210 return imagesBlock; 211 } 212 213 // Processes a chunk of QPA text from the given file name. Creates 214 // the file name header and a list of images in the images <div>, as 215 // found in the text. 216 var processText = function(textString, fileName) { 217 var imagesNode = document.getElementById("images"); 218 var newHeader = createFileNameHeader(fileName); 219 imagesNode.appendChild(newHeader); 220 var imagesBlock = createImagesBlock(); 221 // [\s\S] is a match-anything regexp like the dot, except it 222 // also matches newlines. Ideally, browsers would need to widely 223 // support the "dotall" regexp modifier, but that's not the case 224 // yet and this does the trick. 225 // Group 1 are the image element properties, if any. 226 // Group 2 is the base64 PNG data. 227 var imageRegexp = /<Image\b(.*?)>([\s\S]*?)<\/Image>/g; 228 var imageNameRegexp = /\bName="(.*?)"/; 229 var result; 230 var innerResult; 231 var imageName; 232 while ((result = imageRegexp.exec(textString)) !== null) { 233 innerResult = result[1].match(imageNameRegexp); 234 imageName = ((innerResult !== null) ? innerResult[1] : ""); 235 // Blanks need to be removed from the base64 string. 236 var pngData = result[2].replace(/\s+/g, ""); 237 imagesBlock.appendChild(createImage(pngData, imageName)); 238 } 239 imagesNode.appendChild(imagesBlock); 240 } 241 242 // Loads images from the text in the text area. 243 var loadImages = function () { 244 processText(document.getElementById("qpatext").value, "<Pasted Text>"); 245 } 246 247 // Loads images from the files in the file selector. 248 var handleFileSelect = function (evt) { 249 var files = evt.target.files; 250 for (var i = 0; i < files.length; i++) { 251 // Creates a reader per file. 252 var reader = new FileReader(); 253 // Grab the needed objects to use them after the file has 254 // been read, in order to process its contents and add 255 // images, if found, in the images <div>. 256 reader.onload = (function () { 257 var cbFileName = files[i].name; 258 var cbReader = reader; 259 return function () { 260 processText(cbReader.result, cbFileName); 261 }; 262 })(); 263 // Reads file contents. This will trigger the event above. 264 reader.readAsText(files[i]); 265 } 266 } 267 268 // File selector trick: click on the selector when clicking on the 269 // custom button. 270 var clickFileSelector = function () { 271 document.getElementById("fileselector").click(); 272 } 273 274 // Clears selected files to be able to select them again if needed. 275 var clearSelectedFiles = function() { 276 document.getElementById("fileselector").value = ""; 277 } 278 279 // Set event handlers for interactive elements in the page. 280 document.getElementById("fileselector").onclick = clearSelectedFiles; 281 document.getElementById("fileselector").addEventListener("change", handleFileSelect, false); 282 document.getElementById("fileselectorbutton").onclick = clickFileSelector; 283 document.getElementById("loadimagesbutton").onclick = loadImages; 284 document.getElementById("zoomselect").onchange = rescaleImages; 285 document.getElementById("clearimagesbutton").onclick = clearImages; 286 </script> 287 </body> 288</html> 289