| <!-- |
| -------------------------------------- |
| HTML QPA Image Viewer |
| -------------------------------------- |
| |
| Copyright (c) 2020 The Khronos Group Inc. |
| Copyright (c) 2020 Valve Corporation. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| --> |
| <html> |
| <head> |
| <meta charset="utf-8"/> |
| <title>Load PNGs from QPA output</title> |
| <style> |
| body { |
| background: white; |
| text-align: left; |
| font-family: sans-serif; |
| } |
| h1 { |
| margin-top: 2ex; |
| } |
| h2 { |
| font-size: large; |
| } |
| figure { |
| display: flex; |
| flex-direction: column; |
| |
| /* Taken from https://stackoverflow.com/a/25709375. A grid pattern |
| so that images are easier to see with transparency. */ |
| background: |
| linear-gradient(-90deg, rgba(0,0,0,.05) 1px, transparent 1px), |
| linear-gradient(rgba(0,0,0,.05) 1px, transparent 1px), |
| linear-gradient(-90deg, rgba(0, 0, 0, .04) 1px, transparent 1px), |
| linear-gradient(rgba(0,0,0,.04) 1px, transparent 1px), |
| linear-gradient(transparent 3px, #f2f2f2 3px, #f2f2f2 78px, transparent 78px), |
| linear-gradient(-90deg, #aaa 1px, transparent 1px), |
| linear-gradient(-90deg, transparent 3px, #f2f2f2 3px, #f2f2f2 78px, transparent 78px), |
| linear-gradient(#aaa 1px, transparent 1px), |
| #f2f2f2; |
| background-size: |
| 4px 4px, |
| 4px 4px, |
| 80px 80px, |
| 80px 80px, |
| 80px 80px, |
| 80px 80px, |
| 80px 80px, |
| 80px 80px; |
| } |
| img { |
| margin-left: 1ex; |
| margin-right: 1ex; |
| margin-bottom: 1ex; |
| /* Attempt to zoom images using the nearest-neighbor scaling |
| algorithm. */ |
| image-rendering: pixelated; |
| image-rendering: crisp-edges; |
| /* Border around images. */ |
| border: 1px solid darkgrey; |
| } |
| button { |
| margin: 1ex; |
| border: none; |
| border-radius: .5ex; |
| padding: 1ex; |
| background-color: steelblue; |
| color: white; |
| font-size: large; |
| } |
| button:hover { |
| opacity: .8; |
| } |
| #clearimagesbutton,#cleartextbutton { |
| background-color: seagreen; |
| } |
| select { |
| font-size: large; |
| padding: 1ex; |
| border-radius: .5ex; |
| border: 1px solid darkgrey; |
| } |
| select:hover { |
| opacity: .8; |
| } |
| .loadoption { |
| text-align: center; |
| margin: 1ex; |
| padding: 2ex; |
| border: 1px solid darkgrey; |
| border-radius: 1ex; |
| } |
| #options { |
| display: flex; |
| flex-wrap: wrap; |
| } |
| #qpatext { |
| display: block; |
| min-width: 80ex; |
| max-width: 132ex; |
| min-height: 25ex; |
| max-height: 25ex; |
| margin: 1ex auto; |
| } |
| #fileselector { |
| display: none; |
| } |
| #zoomandclear { |
| margin: 2ex; |
| } |
| #images { |
| margin: 2ex; |
| display: flex; |
| flex-direction: column; |
| } |
| .imagesblock { |
| display: flex; |
| flex-wrap: wrap; |
| } |
| .pathheader { |
| font-family: monospace; |
| font-size: large; |
| } |
| .subtext { |
| font-family: monospace; |
| font-size: smaller; |
| } |
| </style> |
| </head> |
| <body> |
| <h1>Load PNGs from QPA output</h1> |
| |
| <div id="options"> |
| <div class="loadoption"> |
| <h2>Option 1: Load local QPA files</h2> |
| <!-- The file selector text cannot be changed, so we use a hidden selector trick. --> |
| <button id="fileselectorbutton">📂 Load files</button> |
| <input id="fileselector" type="file" multiple> |
| </div> |
| |
| <div class="loadoption"> |
| <h2>Option 2: Paste QPA text or text extract containing <Image> elements below and click "Load images"</h2> |
| <textarea id="qpatext"></textarea> |
| <button id="loadimagesbutton">📃 Load images</button> |
| <button id="cleartextbutton">♻ Clear text</button> |
| </div> |
| </div> |
| |
| <div id="zoomandclear"> |
| 🔎 Image zoom |
| <select id="zoomselect"> |
| <option value="1" selected>1x</option> |
| <option value="2">2x</option> |
| <option value="4">4x</option> |
| <option value="6">6x</option> |
| <option value="8">8x</option> |
| <option value="16">16x</option> |
| <option value="32">32x</option> |
| </select> |
| <button id="clearimagesbutton">♻ Clear images</button> |
| </div> |
| |
| <div id="images"></div> |
| |
| <script> |
| // Returns zoom factor as a number. |
| var getSelectedZoom = function () { |
| return new Number(document.getElementById("zoomselect").value); |
| } |
| |
| // Scales a given image with the selected zoom factor. |
| var scaleSingleImage = function (img) { |
| var factor = getSelectedZoom(); |
| img.style.width = (img.naturalWidth * factor) + "px"; |
| img.style.height = (img.naturalHeight * factor) + "px"; |
| } |
| |
| // Rescales all <img> elements in the page. Used after changing the selected zoom. |
| var rescaleImages = function () { |
| var imageList = document.getElementsByTagName("img"); |
| for (var i = 0; i < imageList.length; i++) { |
| scaleSingleImage(imageList[i]) |
| } |
| } |
| |
| // Removes everything contained in the images <div>. |
| var clearImages = function () { |
| var imagesNode = document.getElementById("images"); |
| while (imagesNode.hasChildNodes()) { |
| imagesNode.removeChild(imagesNode.lastChild); |
| } |
| } |
| |
| // Clears textarea text. |
| var clearText = function() { |
| document.getElementById("qpatext").value = ""; |
| } |
| |
| // Returns a properly sized image with the given base64-encoded PNG data. |
| var createImage = function (pngData, imageName, imageFormat, imageDimensions, imageDescription) { |
| var imageContainer = document.createElement("figure"); |
| if (imageName.length > 0) { |
| var newFileNameHeader = document.createElement("figcaption"); |
| newFileNameHeader.textContent = imageName; |
| newFileNameHeader.style.fontWeight = "bold"; |
| newFileNameHeader.style.textAlign = "center"; |
| |
| if (imageDescription.length > 0) { |
| var newDescription = document.createElement("span"); |
| newDescription.className = "subtext"; |
| newDescription.textContent = decodeURI(imageDescription).replace(/'/g,'\'').replace(/"/g,'"'); |
| |
| newFileNameHeader.appendChild(document.createElement("br")); |
| newFileNameHeader.appendChild(newDescription); |
| } |
| |
| if (imageFormat.length > 0 || imageDimensions.length > 0) { |
| var newSubText = document.createElement("span"); |
| newSubText.className = "subtext"; |
| |
| newSubText.textContent = "("; |
| if (imageDimensions.length > 0) |
| newSubText.textContent += imageDimensions; |
| if (imageFormat.length > 0) { |
| if (imageDimensions.length > 0) |
| newSubText.textContent += " "; |
| newSubText.textContent += imageFormat; |
| } |
| newSubText.textContent += ")"; |
| |
| newFileNameHeader.appendChild(document.createElement("br")); |
| newFileNameHeader.appendChild(newSubText); |
| } |
| imageContainer.appendChild(newFileNameHeader); |
| } |
| |
| var newImage = document.createElement("img"); |
| newImage.src = "data:image/png;base64," + pngData; |
| newImage.style.alignSelf = "center"; |
| newImage.onload = (function () { |
| // Grab the image for the callback. We need to wait until |
| // the image has been properly loaded to access its |
| // naturalWidth and naturalHeight properties, needed for |
| // scaling. |
| var cbImage = newImage; |
| return function () { |
| scaleSingleImage(cbImage); |
| }; |
| })(); |
| imageContainer.appendChild(newImage); |
| return imageContainer; |
| } |
| |
| // Returns a new h3 header with the given file name. |
| var createFileNameHeader = function (fileName) { |
| var newHeader = document.createElement("h3"); |
| newHeader.textContent = fileName; |
| return newHeader; |
| } |
| |
| // Returns a new image block to contain images from a file. |
| var createImagesBlock = function () { |
| var imagesBlock = document.createElement("div"); |
| imagesBlock.className = "imagesblock"; |
| return imagesBlock; |
| } |
| |
| // Returns a new test case header. |
| var createTestCaseHeader = function (testCasePath) { |
| var header = document.createElement("h4"); |
| header.textContent = testCasePath; |
| header.className = "pathheader"; |
| return header; |
| } |
| |
| // Processes a single test case from the given text string. Creates |
| // a list of images in the given images block, as found in the |
| // text. |
| var processTestCase = function(textString, imagesBlock) { |
| // [\s\S] is a match-anything regexp like the dot, except it |
| // also matches newlines. Ideally, browsers would need to widely |
| // support the "dotall" regexp modifier, but that's not the case |
| // yet and this does the trick. |
| // Group 1 are the image element properties, if any. |
| // Group 2 is the base64 PNG data. |
| var imageRegexp = /<Image\b(.*?)>([\s\S]*?)<\/Image>/g; |
| var imageNameRegexp = /\bName="(.*?)"/; |
| var imageFormatRegexp = /\bFormat="(.*?)"/; |
| var imageWidthRegexp = /\bWidth="(.*?)"/; |
| var imageHeightRegexp = /\bHeight="(.*?)"/; |
| var imageDescRegexp = /\bDescription="(.*?)"/; |
| |
| var result; |
| |
| var innerResult; |
| var imageName; |
| var imageFormat; |
| var imageDimensions = ""; |
| var imageDescription; |
| |
| while ((result = imageRegexp.exec(textString)) !== null) { |
| innerResult = result[1].match(imageNameRegexp); |
| imageName = ((innerResult !== null) ? innerResult[1] : ""); |
| |
| innerResult = result[1].match(imageFormatRegexp); |
| imageFormat = ((innerResult !== null) ? innerResult[1] : ""); |
| |
| innerResult = result[1].match(imageWidthRegexp); |
| var imageWidth = ((innerResult !== null) ? innerResult[1] : ""); |
| |
| innerResult = result[1].match(imageHeightRegexp); |
| var imageHeight = ((innerResult !== null) ? innerResult[1] : ""); |
| |
| if ((imageWidth.length > 0) && (imageHeight.length > 0)) |
| imageDimensions = imageWidth + "x" + imageHeight; |
| |
| innerResult = result[1].match(imageDescRegexp); |
| imageDescription = ((innerResult !== null) ? innerResult[1] : ""); |
| |
| // Blanks need to be removed from the base64 string. |
| var pngData = result[2].replace(/\s+/g, ""); |
| imagesBlock.appendChild(createImage(pngData, imageName, imageFormat, imageDimensions, imageDescription)); |
| } |
| } |
| |
| var getTestCaseResultRegexp = function () { |
| return new RegExp(/#beginTestCaseResult\s([^\n]+)\n([\S\s]*?)#endTestCaseResult/g); |
| } |
| |
| // Processes a chunk of QPA text from the given file name. Creates |
| // the file name header, the test case header and a list of images |
| // in the images <div>, as found in the text. |
| var processText = function(textString, fileName) { |
| var imagesNode = document.getElementById("images"); |
| if (fileName.length > 0) { |
| var newHeader = createFileNameHeader(fileName); |
| imagesNode.appendChild(newHeader); |
| } |
| |
| var imagesDiv = document.createElement("div"); |
| var testCaseResultRegexp = getTestCaseResultRegexp(); |
| var result; |
| |
| while ((result = testCaseResultRegexp.exec(textString)) !== null) { |
| var testCasePathHeader = createTestCaseHeader(result[1]); |
| imagesDiv.appendChild(testCasePathHeader); |
| |
| var imagesBlock = createImagesBlock(); |
| processTestCase(result[2], imagesBlock); |
| imagesDiv.appendChild(imagesBlock); |
| } |
| |
| imagesNode.appendChild(imagesDiv); |
| } |
| |
| // Loads images from the text in the text area. |
| var loadImages = function () { |
| var textString = document.getElementById("qpatext").value; |
| var testRE = getTestCaseResultRegexp(); |
| |
| // Full case being pasted. |
| if (testRE.test(textString)) { |
| processText(textString, "<Pasted Text>"); |
| } |
| else { |
| // Excerpt from an unknown test case. |
| var imagesNode = document.getElementById("images"); |
| var fileNameHeader = createFileNameHeader("<Pasted Text>"); |
| imagesNode.appendChild(fileNameHeader); |
| var imagesDiv = document.createElement("div"); |
| var imagesBlock = createImagesBlock(); |
| var testCasePathHeader = createTestCaseHeader("<Unknown test case>"); |
| imagesDiv.appendChild(testCasePathHeader); |
| processTestCase(textString, imagesBlock); |
| imagesDiv.appendChild(imagesBlock); |
| imagesNode.appendChild(imagesDiv); |
| } |
| } |
| |
| // Loads images from the files in the file selector. |
| var handleFileSelect = function (evt) { |
| var files = evt.target.files; |
| for (var i = 0; i < files.length; i++) { |
| // Creates a reader per file. |
| var reader = new FileReader(); |
| // Grab the needed objects to use them after the file has |
| // been read, in order to process its contents and add |
| // images, if found, in the images <div>. |
| reader.onload = (function () { |
| var cbFileName = files[i].name; |
| var cbReader = reader; |
| return function () { |
| processText(cbReader.result, cbFileName); |
| }; |
| })(); |
| // Reads file contents. This will trigger the event above. |
| reader.readAsText(files[i]); |
| } |
| } |
| |
| // File selector trick: click on the selector when clicking on the |
| // custom button. |
| var clickFileSelector = function () { |
| document.getElementById("fileselector").click(); |
| } |
| |
| // Clears selected files to be able to select them again if needed. |
| var clearSelectedFiles = function() { |
| document.getElementById("fileselector").value = ""; |
| } |
| |
| // Set event handlers for interactive elements in the page. |
| document.getElementById("fileselector").onclick = clearSelectedFiles; |
| document.getElementById("fileselector").addEventListener("change", handleFileSelect, false); |
| document.getElementById("fileselectorbutton").onclick = clickFileSelector; |
| document.getElementById("loadimagesbutton").onclick = loadImages; |
| document.getElementById("cleartextbutton").onclick = clearText; |
| document.getElementById("zoomselect").onchange = rescaleImages; |
| document.getElementById("clearimagesbutton").onclick = clearImages; |
| </script> |
| </body> |
| </html> |