Created
May 14, 2023 21:48
-
-
Save pushpalroy/0423cd2ba1f1cdb5b0d7b8d364a85cf9 to your computer and use it in GitHub Desktop.
JS Interop of Flutter web with BlinkID In-browser SDK (JS library)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<base href="/"> | |
<title>Card Scan</title> | |
<link rel="stylesheet" type="text/css" href="/blinkid/style.css"> | |
<script src="https://cdn.jsdelivr.net/npm/@microblink/blinkid-in-browser-sdk@6.0.1/dist/blinkid-sdk.min.js"></script> | |
<script src="/blinkid/blinkid.js" defer></script> | |
</head> | |
<body> | |
<div id="screen-scanning" class="hidden"> | |
<video id="camera-feed" playsinline></video> | |
<!-- Recognition events will be drawn here. --> | |
<canvas id="camera-feedback"></canvas> | |
<div id="guides-section"> | |
<p id="camera-guides">Point the camera towards front side of a document.</p> | |
<p id="doc-side-guides">Scan the first side of the document.</p> | |
</div> | |
</div> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) Microblink Ltd. All rights reserved. | |
*/ | |
/** | |
* BlinkID In-browser SDK demo app which demonstrates how to: | |
* | |
* - Change default SDK settings | |
* - Scan front side of the identity document with web camera | |
* - Provide visual feedback to the end-user during the scan | |
* | |
* This is a modified version of the original example source available here: | |
* https://github.com/BlinkID/blinkid-in-browser/blob/master/examples/multi-side/javascript/app.js | |
*/ | |
// UI elements for scanning feedback | |
const cameraFeed = document.getElementById("camera-feed"); | |
const cameraFeedback = document.getElementById("camera-feedback"); | |
const drawContext = cameraFeedback.getContext("2d"); | |
const scanFeedback = document.getElementById("camera-guides"); | |
const docSideGuide = document.getElementById("doc-side-guides"); | |
/** | |
* Check browser support, customize settings and load WASM SDK. | |
*/ | |
function scanCard() { | |
// Check if browser has proper support for WebAssembly | |
if (!BlinkIDSDK.isBrowserSupported()) { | |
console.error("This browser is not supported!") | |
return; | |
} | |
// 1. It's possible to obtain a free trial license key on microblink.com | |
let licenseKey = "your-blinkid-license-key"; | |
// 2. Create instance of SDK load settings with your license key | |
const loadSettings = new BlinkIDSDK.WasmSDKLoadSettings(licenseKey); | |
// [OPTIONAL] Change default settings | |
// Show or hide hello message in browser console when WASM is successfully loaded | |
loadSettings.allowHelloMessage = true; | |
// In order to provide better UX, display progress bar while loading the SDK | |
//loadSettings.loadProgressCallback = (progress) => progressEl.value = progress; | |
// Set absolute location of the engine, i.e. WASM and support JS files | |
loadSettings.engineLocation = "/assets/blinkid_in_browser/resources"; | |
// Set absolute location of the worker file | |
loadSettings.workerLocation = "/assets/blinkid_in_browser/resources/BlinkIDWasmSDK.worker.min.js"; | |
// 3. Load SDK | |
BlinkIDSDK.loadWasmModule(loadSettings).then( | |
(sdk) => { | |
console.log("SDK is initialized!"); | |
startScan(sdk); | |
}, | |
(error) => { | |
console.error("Failed to load SDK!", error); | |
}); | |
} | |
/** | |
* Scan single side of identity document with web camera. | |
*/ | |
async function startScan(sdk) { | |
console.log("Starting to scan..."); | |
document.getElementById("screen-scanning")?.classList.remove("hidden"); | |
// 1. Create a recognizer objects which will be used to recognize single image or stream of images. | |
// | |
// BlinkID Multi-side Recognizer - scan ID documents on both sides | |
const multiSideGenericIDRecognizer = await BlinkIDSDK.createBlinkIdMultiSideRecognizer(sdk); | |
updateDocSideGuide("Scan the FRONT side") | |
// [OPTIONAL] Create a callbacks object that will receive recognition events, such as detected object location etc. | |
const callbacks = { | |
onQuadDetection: (quad) => drawQuad(quad), | |
onDetectionFailed: () => updateScanFeedback("Detection failed", true), | |
// This callback is required for multi-side experience. | |
onFirstSideResult: () => { | |
alert("Flip the document") | |
updateDocSideGuide("Scan the BACK side") | |
} | |
}; | |
// 2. Create a RecognizerRunner object which orchestrates the recognition with one or more | |
// recognizer objects. | |
const recognizerRunner = await BlinkIDSDK.createRecognizerRunner( | |
// SDK instance to use | |
sdk, | |
// List of recognizer objects that will be associated with created RecognizerRunner object | |
[multiSideGenericIDRecognizer], | |
// [OPTIONAL] Should recognition pipeline stop as soon as first recognizer in chain finished recognition | |
false, | |
// [OPTIONAL] Callbacks object that will receive recognition events | |
callbacks | |
); | |
// 3. Create a VideoRecognizer object and attach it to HTMLVideoElement that will be used for displaying the camera feed | |
const videoRecognizer = await BlinkIDSDK.VideoRecognizer.createVideoRecognizerFromCameraStream( | |
cameraFeed, recognizerRunner | |
); | |
// 4. Start the recognition and get results from callback | |
try { | |
videoRecognizer.startRecognition( | |
// 5. Obtain the results | |
async (recognitionState) => { | |
if (!videoRecognizer) { | |
return; | |
} | |
// Pause recognition before performing any async operation | |
videoRecognizer.pauseRecognition(); | |
if (recognitionState === BlinkIDSDK.RecognizerResultState.Empty) { | |
return; | |
} | |
const result = await multiSideGenericIDRecognizer.getResult(); | |
if (result.state === BlinkIDSDK.RecognizerResultState.Empty) { | |
return; | |
} | |
// Inform the user about results | |
// console.log("BlinkID Multi-side recognizer results", result); | |
// First name | |
const firstName = | |
result.firstName.latin || | |
result.firstName.cyrillic || | |
result.firstName.arabic || | |
result.mrz.secondaryID; | |
// Last name | |
const lastName = | |
result.lastName.latin || | |
result.lastName.cyrillic || | |
result.lastName.arabic || | |
result.mrz.primaryID; | |
const fullName = | |
result.fullName.latin || | |
result.fullName.cyrillic || | |
result.fullName.arabic || | |
`${result.mrz.secondaryID} ${result.mrz.primaryID}`; | |
// DOB | |
const dateOfBirth = { | |
year: result.dateOfBirth.year || result.mrz.dateOfBirth.year, | |
month: result.dateOfBirth.month || result.mrz.dateOfBirth.month, | |
day: result.dateOfBirth.day || result.mrz.dateOfBirth.day | |
}; | |
// License expiry date | |
const dateOfExpiry = { | |
year: result.dateOfExpiry.year || result.mrz.dateOfExpiry.year, | |
month: result.dateOfExpiry.month || result.mrz.dateOfExpiry.month, | |
day: result.dateOfExpiry.day || result.mrz.dateOfExpiry.day | |
}; | |
const derivedFullName = `${firstName} ${lastName}`.trim() || fullName; | |
// License ID | |
const licenseId = | |
result.documentNumber.latin || | |
result.documentNumber.cyrillic || | |
result.documentNumber.arabic || | |
result.viz.documentNumber; | |
// Address | |
const address = | |
result.address.latin || | |
result.address.cyrillic || | |
result.address.arabic || | |
result.viz.address; | |
// Country | |
const country = | |
result.classInfo.countryName || | |
result.classInfo.isoAlpha2CountryCode || | |
result.classInfo.isoAlpha3CountryCode; | |
// Country ID | |
const countryId = | |
result.classInfo.country; | |
// Region ID | |
const regionId = | |
result.classInfo.region; | |
// 6. Release all resources allocated on the WebAssembly heap and associated with camera stream | |
// Release browser resources associated with the camera stream | |
videoRecognizer?.releaseVideoFeed(); | |
// Release memory on WebAssembly heap used by the RecognizerRunner | |
recognizerRunner?.delete(); | |
// Release memory on WebAssembly heap used by the recognizer | |
multiSideGenericIDRecognizer?.delete(); | |
// Clear any leftovers drawn to canvas | |
clearDrawCanvas(); | |
// Hide scanning screen and show scan button again | |
document.getElementById("screen-scanning")?.classList.add("hidden"); | |
// Called when both the sides have completed scanning | |
window.parent.setDlScanResult(firstName, lastName, licenseId.toString(), dateOfBirth, | |
dateOfExpiry, address.toString(), country.toString(), countryId, regionId); | |
}); | |
} catch (error) { | |
console.error("Error during initialization of VideoRecognizer:", error); | |
window.parent.setDlScanError(error.toString()); | |
return; | |
} | |
} | |
/** | |
* Utility functions for drawing detected quadrilateral onto canvas. | |
*/ | |
function drawQuad(quad) { | |
clearDrawCanvas(); | |
// Based on detection status, show appropriate color and message | |
setupColor(quad); | |
setupMessage(quad); | |
applyTransform(quad.transformMatrix); | |
drawContext.beginPath(); | |
drawContext.moveTo(quad.topLeft.x, quad.topLeft.y); | |
drawContext.lineTo(quad.topRight.x, quad.topRight.y); | |
drawContext.lineTo(quad.bottomRight.x, quad.bottomRight.y); | |
drawContext.lineTo(quad.bottomLeft.x, quad.bottomLeft.y); | |
drawContext.closePath(); | |
drawContext.stroke(); | |
} | |
/** | |
* This function will make sure that coordinate system associated with detectionResult | |
* canvas will match the coordinate system of the image being recognized. | |
*/ | |
function applyTransform(transformMatrix) { | |
const canvasAR = cameraFeedback.width / cameraFeedback.height; | |
const videoAR = cameraFeed.videoWidth / cameraFeed.videoHeight; | |
let xOffset = 0; | |
let yOffset = 0; | |
let scaledVideoHeight = 0; | |
let scaledVideoWidth = 0; | |
if (canvasAR > videoAR) { | |
// pillarboxing: https://en.wikipedia.org/wiki/Pillarbox | |
scaledVideoHeight = cameraFeedback.height; | |
scaledVideoWidth = videoAR * scaledVideoHeight; | |
xOffset = (cameraFeedback.width - scaledVideoWidth) / 2.0; | |
} else { | |
// letterboxing: https://en.wikipedia.org/wiki/Letterboxing_(filming) | |
scaledVideoWidth = cameraFeedback.width; | |
scaledVideoHeight = scaledVideoWidth / videoAR; | |
yOffset = (cameraFeedback.height - scaledVideoHeight) / 2.0; | |
} | |
// first transform canvas for offset of video preview within the HTML video element (i.e. correct letterboxing or pillarboxing) | |
drawContext.translate(xOffset, yOffset); | |
// second, scale the canvas to fit the scaled video | |
drawContext.scale( | |
scaledVideoWidth / cameraFeed.videoWidth, | |
scaledVideoHeight / cameraFeed.videoHeight); | |
// finally, apply transformation from image coordinate system to | |
// https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setTransform | |
drawContext.transform( | |
transformMatrix[0], | |
transformMatrix[3], | |
transformMatrix[1], | |
transformMatrix[4], | |
transformMatrix[2], | |
transformMatrix[5]); | |
} | |
function clearDrawCanvas() { | |
cameraFeedback.width = cameraFeedback.clientWidth; | |
cameraFeedback.height = cameraFeedback.clientHeight; | |
drawContext.clearRect( | |
0, | |
0, | |
cameraFeedback.width, | |
cameraFeedback.height | |
); | |
} | |
function setupColor(displayable) { | |
let color = "#FFFF00FF"; | |
if (displayable.detectionStatus === 0) { | |
color = "#FF0000FF"; | |
} else if (displayable.detectionStatus === 1) { | |
color = "#00FF00FF"; | |
} | |
drawContext.fillStyle = color; | |
drawContext.strokeStyle = color; | |
drawContext.lineWidth = 8; | |
} | |
function setupMessage(displayable) { | |
switch (displayable.detectionStatus) { | |
case BlinkIDSDK.DetectionStatus.Fail: | |
updateScanFeedback("Scanning..."); | |
break; | |
case BlinkIDSDK.DetectionStatus.Success: | |
case BlinkIDSDK.DetectionStatus.FallbackSuccess: | |
updateScanFeedback("Detection successful"); | |
break; | |
case BlinkIDSDK.DetectionStatus.CameraAtAngle: | |
updateScanFeedback("Adjust the angle"); | |
break; | |
case BlinkIDSDK.DetectionStatus.CameraTooHigh: | |
updateScanFeedback("Move document closer"); | |
break; | |
case BlinkIDSDK.DetectionStatus.CameraTooNear: | |
case BlinkIDSDK.DetectionStatus.DocumentTooCloseToEdge: | |
case BlinkIDSDK.DetectionStatus.Partial: | |
updateScanFeedback("Move document farther"); | |
break; | |
default: | |
console.warn("Unhandled detection status!", displayable.detectionStatus); | |
} | |
} | |
let scanFeedbackLock = false; | |
/** | |
* The purpose of this function is to ensure that scan feedback message is | |
* visible for at least 1 second. | |
*/ | |
function updateScanFeedback(message, force) { | |
if (scanFeedbackLock && !force) { | |
return; | |
} | |
scanFeedbackLock = true; | |
scanFeedback.innerText = message; | |
window.setTimeout(() => scanFeedbackLock = false, 1000); | |
} | |
function updateDocSideGuide(message) { | |
docSideGuide.innerText = message; | |
} | |
// Run | |
scanCard(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:js' as js; | |
import 'dart:html' as http; | |
import 'dart:js_util'; | |
import 'package:flutter/cupertino.dart'; | |
/// Javascript - Dart InterOp API wrapper | |
class JSApi { | |
void jsCallDartFunction(String jsFunctionName, Function callback) { | |
debugPrint("JSApi function: window.$jsFunctionName"); | |
setProperty(http.window, jsFunctionName, js.allowInterop(callback)); | |
} | |
dynamic callJsFunction(Object method, [List<dynamic>? args]) { | |
return js.context.callMethod(method, args); | |
} | |
bool jsHasProperty(dynamic property) { | |
return js.context.hasProperty(property); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:html'; | |
import 'dart:ui' as ui; | |
import 'package:flutter/material.dart'; | |
import '../../internal/js/js_interop_interface.dart'; | |
class BlinkIdScanPage extends StatefulWidget { | |
const BlinkIdScanPage({super.key}); | |
@override | |
State<BlinkIdScanPage> createState() => _BlinkIdScanPageState(); | |
} | |
class _BlinkIdScanPageState extends State<BlinkIdScanPage> { | |
final IFrameElement _scannerIframeElement = IFrameElement(); | |
@override | |
void initState() { | |
super.initState(); | |
_setUpBlinkIdScanIframe(); | |
// Register your function here which will be called from JS | |
JSApi js = JSApi(); | |
js.jsCallDartFunction("setDlScanResult", ( | |
firstName, | |
lastName, | |
licenseNum, | |
birthDate, | |
expiryDate, | |
address, | |
country, | |
countryId, | |
regionId, | |
) { | |
// Get results from callback | |
debugPrint( | |
"$firstName, $lastName, $licenseNum, ${birthDate.day}/${birthDate.month}/${birthDate.year}, " | |
"${expiryDate.day}/${expiryDate.month}/${expiryDate.year}, $address, $country, $countryId, $regionId", | |
); | |
}); | |
} | |
@override | |
Widget build(BuildContext context) { | |
return SizedBox( | |
width: 800, | |
height: 1000, | |
child: HtmlElementView( | |
key: UniqueKey(), | |
viewType: 'iframeElement', | |
), | |
); | |
} | |
_setUpBlinkIdScanIframe() { | |
_scannerIframeElement.height = '1000'; | |
_scannerIframeElement.width = '800'; | |
_scannerIframeElement.style.border = 'none'; | |
_scannerIframeElement.src = "/blinkid/blinkid.html"; | |
// ignore: undefined_prefixed_name | |
ui.platformViewRegistry.registerViewFactory( | |
'iframeElement', | |
(int viewId) => _scannerIframeElement, | |
); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Copyright (c) Microblink Ltd. All rights reserved. | |
*/ | |
* | |
{ | |
box-sizing: border-box; | |
} | |
html, | |
body | |
{ | |
width: 100%; | |
height: 100%; | |
} | |
html | |
{ | |
margin: 0; | |
padding: 0; | |
font-size: 16px; | |
line-height: 24px; | |
font-family: sans-serif; | |
} | |
body | |
{ | |
display: flex; | |
min-height: 100%; | |
margin: 0; | |
justify-content: center; | |
align-items: center; | |
} | |
#loading | |
{ | |
display: block; | |
} | |
#view-landing, | |
#view-scan-from-camera, | |
#view-scan-from-file, | |
#view-results | |
{ | |
display: block; | |
width: 100%; | |
height: 100%; | |
} | |
/* Rules for better readability */ | |
img | |
{ | |
display: block; | |
width: 100%; | |
max-width: 320px; | |
height: auto; | |
} | |
video | |
{ | |
width: 100%; | |
} | |
textarea | |
{ | |
display: block; | |
} | |
/* Camera feedback */ | |
#screen-scanning | |
{ | |
position: relative; | |
} | |
#view-scan-from-camera | |
{ | |
position: relative; | |
} | |
#camera-feedback | |
{ | |
position: absolute; | |
top: 0; | |
left: 0; | |
right: 0; | |
bottom: 0; | |
width: 100%; | |
height: 100%; | |
} | |
#guides-section | |
{ | |
position: absolute; | |
padding: 8px; | |
top: 0; | |
left: 0; | |
right: 0; | |
text-align: center; | |
} | |
#camera-guides | |
{ | |
top: 0; | |
left: 0; | |
right: 0; | |
color: white; | |
text-align: center; | |
font-size: 18px; | |
} | |
#doc-side-guides | |
{ | |
top: 40px; | |
left: 0; | |
right: 0; | |
color: white; | |
text-align: center; | |
font-weight: bold; | |
font-size: 22px; | |
} | |
/* Auxiliary classes */ | |
.hidden | |
{ | |
display: none !important; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment