• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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">&#x1F4C2; 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 &lt;Image&gt; elements below and click "Load images"</h2>
155                <textarea id="qpatext"></textarea>
156                <button id="loadimagesbutton">&#x1F4C3; Load images</button>
157                <button id="cleartextbutton">&#x267B; Clear text</button>
158            </div>
159        </div>
160
161        <div id="zoomandclear">
162            &#x1F50E; 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">&#x267B; 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(/&apos;/g,'\'').replace(/&quot;/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