├── ocr_results_viewer.wasm ├── readme.md ├── qtlogo.svg ├── ocr_results_viewer.html └── qtloader.js /ocr_results_viewer.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stereomatchingkiss/ocr_results_viewer/main/ocr_results_viewer.wasm -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | This project is for demo purpose only, not for commercial purpose. 2 | You can access this page by the url "https://stereomatchingkiss.github.io/ocr_results_viewer/ocr_results_viewer.html" 3 | -------------------------------------------------------------------------------- /qtlogo.svg: -------------------------------------------------------------------------------- 1 | 2 | 12 | 14 | 15 | 17 | image/svg+xml 18 | 20 | 21 | 22 | 23 | 24 | 28 | 32 | 36 | 40 | 41 | -------------------------------------------------------------------------------- /ocr_results_viewer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | ocr_results_viewer 13 | 23 | 24 | 25 |
26 |
27 | 28 | Qt for WebAssembly: ocr_results_viewer 29 |
30 | 31 |
32 |
33 | 34 | 35 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /qtloader.js: -------------------------------------------------------------------------------- 1 | /**************************************************************************** 2 | ** 3 | ** Copyright (C) 2018 The Qt Company Ltd. 4 | ** Contact: https://www.qt.io/licensing/ 5 | ** 6 | ** This file is part of the plugins of the Qt Toolkit. 7 | ** 8 | ** $QT_BEGIN_LICENSE:GPL$ 9 | ** Commercial License Usage 10 | ** Licensees holding valid commercial Qt licenses may use this file in 11 | ** accordance with the commercial license agreement provided with the 12 | ** Software or, alternatively, in accordance with the terms contained in 13 | ** a written agreement between you and The Qt Company. For licensing terms 14 | ** and conditions see https://www.qt.io/terms-conditions. For further 15 | ** information use the contact form at https://www.qt.io/contact-us. 16 | ** 17 | ** GNU General Public License Usage 18 | ** Alternatively, this file may be used under the terms of the GNU 19 | ** General Public License version 3 or (at your option) any later version 20 | ** approved by the KDE Free Qt Foundation. The licenses are as published by 21 | ** the Free Software Foundation and appearing in the file LICENSE.GPL3 22 | ** included in the packaging of this file. Please review the following 23 | ** information to ensure the GNU General Public License requirements will 24 | ** be met: https://www.gnu.org/licenses/gpl-3.0.html. 25 | ** 26 | ** $QT_END_LICENSE$ 27 | ** 28 | ****************************************************************************/ 29 | 30 | // QtLoader provides javascript API for managing Qt application modules. 31 | // 32 | // QtLoader provides API on top of Emscripten which supports common lifecycle 33 | // tasks such as displaying placeholder content while the module downloads, 34 | // handing application exits, and checking for browser wasm support. 35 | // 36 | // There are two usage modes: 37 | // * Managed: QtLoader owns and manages the HTML display elements like 38 | // the loader and canvas. 39 | // * External: The embedding HTML page owns the display elements. QtLoader 40 | // provides event callbacks which the page reacts to. 41 | // 42 | // Managed mode usage: 43 | // 44 | // var config = { 45 | // containerElements : [$("container-id")]; 46 | // } 47 | // var qtLoader = QtLoader(config); 48 | // qtLoader.loadEmscriptenModule("applicationName"); 49 | // 50 | // External mode.usage: 51 | // 52 | // var config = { 53 | // canvasElements : [$("canvas-id")], 54 | // showLoader: function() { 55 | // loader.style.display = 'block' 56 | // canvas.style.display = 'hidden' 57 | // }, 58 | // showCanvas: function() { 59 | // loader.style.display = 'hidden' 60 | // canvas.style.display = 'block' 61 | // return canvas; 62 | // } 63 | // } 64 | // var qtLoader = QtLoader(config); 65 | // qtLoader.loadEmscriptenModule("applicationName"); 66 | // 67 | // Config keys 68 | // 69 | // containerElements : [container-element, ...] 70 | // One or more HTML elements. QtLoader will display loader elements 71 | // on these while loading the applicaton, and replace the loader with a 72 | // canvas on load complete. 73 | // canvasElements : [canvas-element, ...] 74 | // One or more canvas elements. 75 | // showLoader : function(status, containerElement) 76 | // Optional loading element constructor function. Implement to create 77 | // a custom loading screen. This function may be called multiple times, 78 | // while preparing the application binary. "status" is a string 79 | // containing the loading sub-status, and may be either "Downloading", 80 | // or "Compiling". The browser may be using streaming compilation, in 81 | // which case the wasm module is compiled during downloading and the 82 | // there is no separate compile step. 83 | // showCanvas : function(containerElement) 84 | // Optional canvas constructor function. Implement to create custom 85 | // canvas elements. 86 | // showExit : function(crashed, exitCode, containerElement) 87 | // Optional exited element constructor function. 88 | // showError : function(crashed, exitCode, containerElement) 89 | // Optional error element constructor function. 90 | // 91 | // path : 92 | // Prefix path for wasm file, realative to the loading HMTL file. 93 | // restartMode : "DoNotRestart", "RestartOnExit", "RestartOnCrash" 94 | // Controls whether the application should be reloaded on exits. The default is "DoNotRestart" 95 | // restartType : "RestartModule", "ReloadPage" 96 | // restartLimit : 97 | // Restart attempts limit. The default is 10. 98 | // stdoutEnabled : 99 | // stderrEnabled : 100 | // environment : 101 | // key-value environment variable pairs. 102 | // 103 | // QtLoader object API 104 | // 105 | // webAssemblySupported : bool 106 | // webGLSupported : bool 107 | // canLoadQt : bool 108 | // Reports if WebAssembly and WebGL are supported. These are requirements for 109 | // running Qt applications. 110 | // loadEmscriptenModule(applicationName) 111 | // Loads the application from the given emscripten javascript module file and wasm file 112 | // status 113 | // One of "Created", "Loading", "Running", "Exited". 114 | // crashed 115 | // Set to true if there was an unclean exit. 116 | // exitCode 117 | // main()/emscripten_force_exit() return code. Valid on status change to 118 | // "Exited", iff crashed is false. 119 | // exitText 120 | // Abort/exit message. 121 | // addCanvasElement 122 | // Add canvas at run-time. Adds a corresponding QScreen, 123 | // removeCanvasElement 124 | // Remove canvas at run-time. Removes the corresponding QScreen. 125 | // resizeCanvasElement 126 | // Signals to the application that a canvas has been resized. 127 | // setFontDpi 128 | // Sets the logical font dpi for the application. 129 | 130 | 131 | var Module = {} 132 | 133 | function QtLoader(config) 134 | { 135 | function webAssemblySupported() { 136 | return typeof WebAssembly !== "undefined" 137 | } 138 | 139 | function webGLSupported() { 140 | // We expect that WebGL is supported if WebAssembly is; however 141 | // the GPU may be blacklisted. 142 | try { 143 | var canvas = document.createElement("canvas"); 144 | return !!(window.WebGLRenderingContext && (canvas.getContext("webgl") || canvas.getContext("experimental-webgl"))); 145 | } catch (e) { 146 | return false; 147 | } 148 | } 149 | 150 | function canLoadQt() { 151 | // The current Qt implementation requires WebAssembly (asm.js is not in use), 152 | // and also WebGL (there is no raster fallback). 153 | return webAssemblySupported() && webGLSupported(); 154 | } 155 | 156 | function removeChildren(element) { 157 | while (element.firstChild) element.removeChild(element.firstChild); 158 | } 159 | 160 | function createCanvas() { 161 | var canvas = document.createElement("canvas"); 162 | canvas.className = "QtCanvas"; 163 | canvas.style.height = "100%"; 164 | canvas.style.width = "100%"; 165 | 166 | // Set contentEditable in order to enable clipboard events; hide the resulting focus frame. 167 | canvas.contentEditable = true; 168 | canvas.style.outline = "0px solid transparent"; 169 | canvas.style.caretColor = "transparent"; 170 | canvas.style.cursor = "default"; 171 | 172 | return canvas; 173 | } 174 | 175 | // Set default state handler functions and create canvases if needed 176 | if (config.containerElements !== undefined) { 177 | 178 | config.canvasElements = config.containerElements.map(createCanvas); 179 | 180 | config.showError = config.showError || function(errorText, container) { 181 | removeChildren(container); 182 | var errorTextElement = document.createElement("text"); 183 | errorTextElement.className = "QtError" 184 | errorTextElement.innerHTML = errorText; 185 | return errorTextElement; 186 | } 187 | 188 | config.showLoader = config.showLoader || function(loadingState, container) { 189 | removeChildren(container); 190 | var loadingText = document.createElement("text"); 191 | loadingText.className = "QtLoading" 192 | loadingText.innerHTML = '

${loadingState}...

'; 193 | return loadingText; 194 | }; 195 | 196 | config.showCanvas = config.showCanvas || function(canvas, container) { 197 | removeChildren(container); 198 | } 199 | 200 | config.showExit = config.showExit || function(crashed, exitCode, container) { 201 | if (!crashed) 202 | return undefined; 203 | 204 | removeChildren(container); 205 | var fontSize = 54; 206 | var crashSymbols = ["\u{1F615}", "\u{1F614}", "\u{1F644}", "\u{1F928}", "\u{1F62C}", 207 | "\u{1F915}", "\u{2639}", "\u{1F62E}", "\u{1F61E}", "\u{1F633}"]; 208 | var symbolIndex = Math.floor(Math.random() * crashSymbols.length); 209 | var errorHtml = ` ${crashSymbols[symbolIndex]} ` 210 | var errorElement = document.createElement("text"); 211 | errorElement.className = "QtExit" 212 | errorElement.innerHTML = errorHtml; 213 | return errorElement; 214 | } 215 | } 216 | 217 | config.restartMode = config.restartMode || "DoNotRestart"; 218 | config.restartLimit = config.restartLimit || 10; 219 | 220 | if (config.stdoutEnabled === undefined) config.stdoutEnabled = true; 221 | if (config.stderrEnabled === undefined) config.stderrEnabled = true; 222 | 223 | // Make sure config.path is defined and ends with "/" if needed 224 | if (config.path === undefined) 225 | config.path = ""; 226 | if (config.path.length > 0 && !config.path.endsWith("/")) 227 | config.path = config.path.concat("/"); 228 | 229 | if (config.environment === undefined) 230 | config.environment = {}; 231 | 232 | var publicAPI = {}; 233 | publicAPI.webAssemblySupported = webAssemblySupported(); 234 | publicAPI.webGLSupported = webGLSupported(); 235 | publicAPI.canLoadQt = canLoadQt(); 236 | publicAPI.canLoadApplication = canLoadQt(); 237 | publicAPI.status = undefined; 238 | publicAPI.loadEmscriptenModule = loadEmscriptenModule; 239 | publicAPI.addCanvasElement = addCanvasElement; 240 | publicAPI.removeCanvasElement = removeCanvasElement; 241 | publicAPI.resizeCanvasElement = resizeCanvasElement; 242 | publicAPI.setFontDpi = setFontDpi; 243 | publicAPI.fontDpi = fontDpi; 244 | 245 | restartCount = 0; 246 | 247 | function fetchResource(filePath) { 248 | var fullPath = config.path + filePath; 249 | return fetch(fullPath).then(function(response) { 250 | if (!response.ok) { 251 | self.error = response.status + " " + response.statusText + " " + response.url; 252 | setStatus("Error"); 253 | return Promise.reject(self.error) 254 | } else { 255 | return response; 256 | } 257 | }); 258 | } 259 | 260 | function fetchText(filePath) { 261 | return fetchResource(filePath).then(function(response) { 262 | return response.text(); 263 | }); 264 | } 265 | 266 | function fetchThenCompileWasm(response) { 267 | return response.arrayBuffer().then(function(data) { 268 | self.loaderSubState = "Compiling"; 269 | setStatus("Loading") // trigger loaderSubState udpate 270 | return WebAssembly.compile(data); 271 | }); 272 | } 273 | 274 | function fetchCompileWasm(filePath) { 275 | return fetchResource(filePath).then(function(response) { 276 | if (typeof WebAssembly.compileStreaming !== "undefined") { 277 | self.loaderSubState = "Downloading/Compiling"; 278 | setStatus("Loading"); 279 | return WebAssembly.compileStreaming(response).catch(function(error) { 280 | // compileStreaming may/will fail if the server does not set the correct 281 | // mime type (application/wasm) for the wasm file. Fall back to fetch, 282 | // then compile in this case. 283 | return fetchThenCompileWasm(response); 284 | }); 285 | } else { 286 | // Fall back to fetch, then compile if compileStreaming is not supported 287 | return fetchThenCompileWasm(response); 288 | } 289 | }); 290 | } 291 | 292 | function loadEmscriptenModule(applicationName) { 293 | 294 | // Loading in qtloader.js goes through four steps: 295 | // 1) Check prerequisites 296 | // 2) Download resources 297 | // 3) Configure the emscripten Module object 298 | // 4) Start the emcripten runtime, after which emscripten takes over 299 | 300 | // Check for Wasm & WebGL support; set error and return before downloading resources if missing 301 | if (!webAssemblySupported()) { 302 | self.error = "Error: WebAssembly is not supported" 303 | setStatus("Error"); 304 | return; 305 | } 306 | if (!webGLSupported()) { 307 | self.error = "Error: WebGL is not supported" 308 | setStatus("Error"); 309 | return; 310 | } 311 | 312 | // Continue waiting if loadEmscriptenModule() is called again 313 | if (publicAPI.status == "Loading") 314 | return; 315 | self.loaderSubState = "Downloading"; 316 | setStatus("Loading"); 317 | 318 | // Fetch emscripten generated javascript runtime 319 | var emscriptenModuleSource = undefined 320 | var emscriptenModuleSourcePromise = fetchText(applicationName + ".js").then(function(source) { 321 | emscriptenModuleSource = source 322 | }); 323 | 324 | // Fetch and compile wasm module 325 | var wasmModule = undefined; 326 | var wasmModulePromise = fetchCompileWasm(applicationName + ".wasm").then(function (module) { 327 | wasmModule = module; 328 | }); 329 | 330 | // Wait for all resources ready 331 | Promise.all([emscriptenModuleSourcePromise, wasmModulePromise]).then(function(){ 332 | completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule); 333 | }).catch(function(error) { 334 | self.error = error; 335 | setStatus("Error"); 336 | }); 337 | } 338 | 339 | function completeLoadEmscriptenModule(applicationName, emscriptenModuleSource, wasmModule) { 340 | 341 | // The wasm binary has been compiled into a module during resource download, 342 | // and is ready to be instantiated. Define the instantiateWasm callback which 343 | // emscripten will call to create the instance. 344 | Module.instantiateWasm = function(imports, successCallback) { 345 | WebAssembly.instantiate(wasmModule, imports).then(function(instance) { 346 | successCallback(instance, wasmModule); 347 | }, function(error) { 348 | self.error = error; 349 | setStatus("Error"); 350 | }); 351 | return {}; 352 | }; 353 | 354 | Module.locateFile = Module.locateFile || function(filename) { 355 | return config.path + filename; 356 | }; 357 | 358 | // Attach status callbacks 359 | Module.setStatus = Module.setStatus || function(text) { 360 | // Currently the only usable status update from this function 361 | // is "Running..." 362 | if (text.startsWith("Running")) 363 | setStatus("Running"); 364 | }; 365 | Module.monitorRunDependencies = Module.monitorRunDependencies || function(left) { 366 | // console.log("monitorRunDependencies " + left) 367 | }; 368 | 369 | // Attach standard out/err callbacks. 370 | Module.print = Module.print || function(text) { 371 | if (config.stdoutEnabled) 372 | console.log(text) 373 | }; 374 | Module.printErr = Module.printErr || function(text) { 375 | // Filter out OpenGL getProcAddress warnings. Qt tries to resolve 376 | // all possible function/extension names at startup which causes 377 | // emscripten to spam the console log with warnings. 378 | if (text.startsWith !== undefined && text.startsWith("bad name in getProcAddress:")) 379 | return; 380 | 381 | if (config.stderrEnabled) 382 | console.log(text) 383 | }; 384 | 385 | // Error handling: set status to "Exited", update crashed and 386 | // exitCode according to exit type. 387 | // Emscripten will typically call printErr with the error text 388 | // as well. Note that emscripten may also throw exceptions from 389 | // async callbacks. These should be handled in window.onerror by user code. 390 | Module.onAbort = Module.onAbort || function(text) { 391 | publicAPI.crashed = true; 392 | publicAPI.exitText = text; 393 | setStatus("Exited"); 394 | }; 395 | Module.quit = Module.quit || function(code, exception) { 396 | if (exception.name == "ExitStatus") { 397 | // Clean exit with code 398 | publicAPI.exitText = undefined 399 | publicAPI.exitCode = code; 400 | } else { 401 | publicAPI.exitText = exception.toString(); 402 | publicAPI.crashed = true; 403 | } 404 | setStatus("Exited"); 405 | }; 406 | 407 | // Set environment variables 408 | Module.preRun = Module.preRun || [] 409 | Module.preRun.push(function() { 410 | for (var [key, value] of Object.entries(config.environment)) { 411 | ENV[key.toUpperCase()] = value; 412 | } 413 | }); 414 | 415 | Module.mainScriptUrlOrBlob = new Blob([emscriptenModuleSource], {type: 'text/javascript'}); 416 | 417 | Module.qtCanvasElements = config.canvasElements; 418 | 419 | config.restart = function() { 420 | 421 | // Restart by reloading the page. This will wipe all state which means 422 | // reload loops can't be prevented. 423 | if (config.restartType == "ReloadPage") { 424 | location.reload(); 425 | } 426 | 427 | // Restart by readling the emscripten app module. 428 | ++self.restartCount; 429 | if (self.restartCount > config.restartLimit) { 430 | self.error = "Error: This application has crashed too many times and has been disabled. Reload the page to try again." 431 | setStatus("Error"); 432 | return; 433 | } 434 | loadEmscriptenModule(applicationName); 435 | }; 436 | 437 | publicAPI.exitCode = undefined; 438 | publicAPI.exitText = undefined; 439 | publicAPI.crashed = false; 440 | 441 | // Finally evaluate the emscripten application script, which will 442 | // reference the global Module object created above. 443 | self.eval(emscriptenModuleSource); // ES5 indirect global scope eval 444 | } 445 | 446 | function setErrorContent() { 447 | if (config.containerElements === undefined) { 448 | if (config.showError !== undefined) 449 | config.showError(self.error); 450 | return; 451 | } 452 | 453 | for (container of config.containerElements) { 454 | var errorElement = config.showError(self.error, container); 455 | container.appendChild(errorElement); 456 | } 457 | } 458 | 459 | function setLoaderContent() { 460 | if (config.containerElements === undefined) { 461 | if (config.showLoader !== undefined) 462 | config.showLoader(self.loaderSubState); 463 | return; 464 | } 465 | 466 | for (container of config.containerElements) { 467 | var loaderElement = config.showLoader(self.loaderSubState, container); 468 | container.appendChild(loaderElement); 469 | } 470 | } 471 | 472 | function setCanvasContent() { 473 | if (config.containerElements === undefined) { 474 | if (config.showCanvas !== undefined) 475 | config.showCanvas(); 476 | return; 477 | } 478 | 479 | for (var i = 0; i < config.containerElements.length; ++i) { 480 | var container = config.containerElements[i]; 481 | var canvas = config.canvasElements[i]; 482 | config.showCanvas(canvas, container); 483 | container.appendChild(canvas); 484 | } 485 | } 486 | 487 | function setExitContent() { 488 | 489 | // publicAPI.crashed = true; 490 | 491 | if (publicAPI.status != "Exited") 492 | return; 493 | 494 | if (config.containerElements === undefined) { 495 | if (config.showExit !== undefined) 496 | config.showExit(publicAPI.crashed, publicAPI.exitCode); 497 | return; 498 | } 499 | 500 | if (!publicAPI.crashed) 501 | return; 502 | 503 | for (container of config.containerElements) { 504 | var loaderElement = config.showExit(publicAPI.crashed, publicAPI.exitCode, container); 505 | if (loaderElement !== undefined) 506 | container.appendChild(loaderElement); 507 | } 508 | } 509 | 510 | var committedStatus = undefined; 511 | function handleStatusChange() { 512 | if (publicAPI.status != "Loading" && committedStatus == publicAPI.status) 513 | return; 514 | committedStatus = publicAPI.status; 515 | 516 | if (publicAPI.status == "Error") { 517 | setErrorContent(); 518 | } else if (publicAPI.status == "Loading") { 519 | setLoaderContent(); 520 | } else if (publicAPI.status == "Running") { 521 | setCanvasContent(); 522 | } else if (publicAPI.status == "Exited") { 523 | if (config.restartMode == "RestartOnExit" || 524 | config.restartMode == "RestartOnCrash" && publicAPI.crashed) { 525 | committedStatus = undefined; 526 | config.restart(); 527 | } else { 528 | setExitContent(); 529 | } 530 | } 531 | 532 | // Send status change notification 533 | if (config.statusChanged) 534 | config.statusChanged(publicAPI.status); 535 | } 536 | 537 | function setStatus(status) { 538 | if (status != "Loading" && publicAPI.status == status) 539 | return; 540 | publicAPI.status = status; 541 | 542 | window.setTimeout(function() { handleStatusChange(); }, 0); 543 | } 544 | 545 | function addCanvasElement(element) { 546 | if (publicAPI.status == "Running") 547 | Module.qtAddCanvasElement(element); 548 | else 549 | console.log("Error: addCanvasElement can only be called in the Running state"); 550 | } 551 | 552 | function removeCanvasElement(element) { 553 | if (publicAPI.status == "Running") 554 | Module.qtRemoveCanvasElement(element); 555 | else 556 | console.log("Error: removeCanvasElement can only be called in the Running state"); 557 | } 558 | 559 | function resizeCanvasElement(element) { 560 | if (publicAPI.status == "Running") 561 | Module.qtResizeCanvasElement(element); 562 | } 563 | 564 | function setFontDpi(dpi) { 565 | Module.qtFontDpi = dpi; 566 | if (publicAPI.status == "Running") 567 | Module.qtSetFontDpi(dpi); 568 | } 569 | 570 | function fontDpi() { 571 | return Module.qtFontDpi; 572 | } 573 | 574 | setStatus("Created"); 575 | 576 | return publicAPI; 577 | } 578 | --------------------------------------------------------------------------------