Skip to content

Instantly share code, notes, and snippets.

@pushpalroy
Created May 14, 2023 21:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pushpalroy/0423cd2ba1f1cdb5b0d7b8d364a85cf9 to your computer and use it in GitHub Desktop.
Save pushpalroy/0423cd2ba1f1cdb5b0d7b8d364a85cf9 to your computer and use it in GitHub Desktop.
JS Interop of Flutter web with BlinkID In-browser SDK (JS library)
<!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>
/**
* 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();
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);
}
}
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,
);
}
}
/**
* 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