Создать APK бесплатно

JavaScript bridge API

Сканер QR-кодов и штрихкодов

Экран сканера Google одним промисом, AppMint.scanCode() — без разрешения на камеру, с разобранными URL, Wi-Fi, телефоном или контактом для обычных QR-кодов.

AppMint.scanCode помощник#

AppMint.scanCode(options)

Пример

Открывает экран сканера QR-кодов и штрихкодов Google и выдаёт отсканированное. Один вызов — без разрешения на камеру, без <video>, без собственного интерфейса.

Возвращает: Promise, который всегда выполняется: {ok: true, value, displayValue, format, type, …} или {ok: false, code, message}. Требуется: ничего включать не нужно. На телефоне должны быть Google Play services.

Любой код:

async function scan() {
  if (!(window.AppMint && AppMint.scanCode)) { alert('Open this page in the app to scan'); return; }
  var r = await AppMint.scanCode();
  if (!r.ok) {
    if (r.code === 'cancelled') return;                          // the user closed the scanner
    if (r.code === 'unavailable') alert('The scanner is not available on this phone');
    else if (r.code === 'busy') alert('A scan is already open');
    else alert('Scan failed: ' + r.message);                     // 'failed'
    return;
  }
  console.log(r.value, r.format, r.type);   // "TICKET-4821", "qr_code", "text"
}

Только некоторые форматы и возможность «ввести вручную» для повреждённых этикеток:

var r = await AppMint.scanCode({
  formats: ['ean_13', 'ean_8', 'upc_a', 'upc_e'],   // shop barcodes
  manualInput: true,                                // lets the user type the code
  autoZoom: true                                    // default true
});
if (r.ok) lookUpProduct(r.value);

Готовые части — присутствует только та часть, что соответствует r.type:

var r = await AppMint.scanCode({ formats: ['qr_code'] });
if (r.ok) {
  if (r.type === 'url')     openLink(r.url.url);                      // url: { url, title }
  if (r.type === 'wifi')    showWifi(r.wifi.ssid, r.wifi.password, r.wifi.encryption); // 'wpa'|'wep'|'open'
  if (r.type === 'phone')   callNumber(r.phone);                      // a string
  if (r.type === 'email')   writeMail(r.email.address, r.email.subject, r.email.body);
  if (r.type === 'sms')     writeSms(r.sms.phoneNumber, r.sms.message);
  if (r.type === 'geo')     showMap(r.geo.lat, r.geo.lng);
  if (r.type === 'contact') addContact(r.contact.name, r.contact.organization, r.contact.phones, r.contact.emails);
}

Примечания: названия форматов: qr_code, aztec, codabar, code_39, code_93, code_128, data_matrix, ean_8, ean_13, itf, pdf417, upc_a, upc_e (неизвестные названия игнорируются; без названий — все форматы). type — одно из: url, wifi, text, phone, email, sms, geo, contact, calendar, isbn, product, driver_license, unknown. Коды ошибок: cancelled (закрыто, в том числе кнопкой «Назад»), unavailable (нет Play services или не удалось загрузить сканер), busy (сканирование уже открыто), failed. Первое сканирование на телефоне может занять несколько секунд, пока Play services загружает сканер. Приложения, созданные с помощью ИИ, используют scanCode() из @/lib/appmintNative, который сканирует и в вебе.

AppMint.textRecognition помощник#

AppMint.textRecognition.recognize(image, {script}) / prepare(script, onProgress)

Пример

Читает текст на письменностях помимо латиницы: деванагари (хинди, маратхи, непальский …), китайский, японский и корейский — или 'auto', который пробует каждую письменность и оставляет результат с наибольшим числом символов. Та же опция работает и в стандартном детекторе: new TextDetector({ script: 'devanagari' }). Без этой опции всё читается как латиница, как и раньше.

Возвращает: recognize() → Promise с {script, text, blocks: [{text, box, corners, lines}]} (script = использованная письменность); при ошибке завершается с DOMException — NotSupportedError для неизвестной письменности или отсутствия Google Play services, иначе OperationError. prepare() → Promise с {script, status: 'ready'}. Требуется: Google Play services. Распознаватель каждой письменности загружается через Play services: письменности, названные в вашем коде, устанавливаются вместе с приложением, остальные — при первом использовании.

Текст на хинди с фотографии:

document.getElementById('photo').addEventListener('change', async function (e) {
  var file = e.target.files[0];
  if (!file || !(window.AppMint && AppMint.textRecognition)) return;
  try {
    var r = await AppMint.textRecognition.recognize(file, { script: 'devanagari' });
    document.getElementById('out').textContent = r.text;
  } catch (err) {
    alert('Could not read the photo: ' + err.name);
  }
});

Сначала загрузите модель письменности, с отображением прогресса — иначе при первом использовании на телефоне придётся ждать загрузки:

async function getReady() {
  var bar = document.getElementById('model-progress');
  await AppMint.textRecognition.prepare('japanese', function (s) {
    if (s.progress != null) bar.value = s.progress;      // 0..1 while 'downloading'
  });
  bar.hidden = true;
}

// The same states arrive as events, also when a recognize() call starts a download:
window.addEventListener('appmint:ocr-model', function (e) {
  console.log(e.detail.script, e.detail.status, e.detail.progress);   // 'downloading' | 'ready' | 'failed'
});

Смешанные документы — пусть приложение само выберет из ожидаемых вами письменностей:

var r = await AppMint.textRecognition.recognize(img, { script: 'auto', scripts: ['latin', 'chinese', 'korean'] });
console.log('read as ' + r.script + ': ' + r.text);

var lines = await new TextDetector({ script: 'korean' }).detect(img);   // standard shape, one entry per line

Примечания: scripts — список AppMint: latin, devanagari, chinese, japanese, korean. Для арабского, кириллицы, тайского и иврита распознавателя на устройстве нет. WebToApk.__ocrRecognizeWith(requestId, base64, optionsJson) — внутренний транспорт; используйте помощник.

__ocrAvailable мост#

window.WebToApk.__ocrAvailable(): Boolean

TextDetector / recognizeText(): Google Play services здесь (дом модуля OCR).

Пример

Внутренний транспорт для TextDetector: есть ли на этом телефоне Google Play services (откуда берётся распознаватель). Страницы используют стандартный API; никогда не вызывайте это напрямую — методы __ могут измениться без предупреждения.

Возвращает: true или false. Когда false, TextDetector не установлен вовсе. Требуется: ничего.

Используйте публичный API:

if ('TextDetector' in window) scanButton.hidden = false;

Выберите между сканированием и формой для ввода один раз, при запуске. Если на телефоне нет Google Play services, детектора просто нет, поэтому страница должна предложить другой способ:

function setUpReceiptEntry() {
  var canScan = typeof window.TextDetector === 'function';
  document.getElementById('scan-receipt').hidden = !canScan;
  document.getElementById('type-receipt').hidden = canScan;
  if (!canScan) document.getElementById('entry-hint').textContent =
    'Text scanning is not available on this phone. Type the total below.';
}
document.addEventListener('DOMContentLoaded', setUpReceiptEntry);

Существующий детектор в момент вызова всё равно может ответить, что распознавание сейчас недоступно. Различайте два вида сбоев по имени ошибки:

async function readLabel(img) {
  if (typeof window.TextDetector !== 'function') return null;
  try {
    var lines = await new TextDetector().detect(img);
    return lines.map(function (l) { return l.rawValue; }).join('\n');
  } catch (e) {
    if (e.name === 'NotSupportedError') showMessage('Text recognition is not available right now.');
    else showMessage('Could not read the label: ' + e.message);
    return null;
  }
}

Примечания: реализовано в оболочке приложения (TextRecognitionBridge.kt).

__ocrRecognize мост#

window.WebToApk.__ocrRecognize(requestId: String, base64: String)

Распознаёт текст в [base64] (PNG/JPEG); ответ приходит как `appmint:ocr {requestId, blocks|error}`.

Пример

Внутренний транспорт для TextDetector.detect(): распознаёт текст в PNG/JPEG. Страницы используют стандартный API; никогда не вызывайте это напрямую — методы __ могут измениться без предупреждения.

Возвращает: ничего; ответ приходит событием appmint:ocr {requestId, blocks:[{text, box, corners, lines}]} или {requestId, error} (unavailable, bad-image, failed: …). Требуется: ничего.

Используйте публичный API:

const lines = await new TextDetector().detect(document.getElementById('receipt'));
console.log(lines.map((l) => l.rawValue));

Прочитайте текст фотографии, которую выбрал пользователь. detect() принимает Blob/File, а также <img>, <canvas> или ImageData:

<input type="file" id="doc" accept="image/*" capture="environment">
<pre id="out"></pre>
<script>
document.getElementById('doc').addEventListener('change', async function (e) {
  var file = e.target.files[0];
  if (!file || typeof window.TextDetector !== 'function') return;
  try {
    var lines = await new TextDetector().detect(file);
    document.getElementById('out').textContent = lines.map(function (l) { return l.rawValue; }).join('\n');
  } catch (err) {
    document.getElementById('out').textContent = 'Could not read the photo (' + err.name + ')';
  }
});
</script>

Обведите каждую распознанную строку поверх изображения. boundingBox задан в собственных пикселях изображения, поэтому масштабируйте его под отображаемый размер. Изображение должно полностью загрузиться, иначе detect() завершается с ошибкой InvalidStateError:

async function highlightLines(img, overlay) {
  if (!img.complete) await new Promise(function (r) { img.onload = r; });
  var lines = await new TextDetector().detect(img);
  var sx = img.clientWidth / img.naturalWidth, sy = img.clientHeight / img.naturalHeight;
  overlay.innerHTML = '';
  lines.forEach(function (l) {
    var b = l.boundingBox, box = document.createElement('div');
    box.className = 'ocr-box';
    box.title = l.rawValue;
    box.style.cssText = 'position:absolute;left:' + b.x * sx + 'px;top:' + b.y * sy +
      'px;width:' + b.width * sx + 'px;height:' + b.height * sy + 'px';
    overlay.appendChild(box);
  });
}

Достаньте сумму из чека. Изображение с другого источника, которое «портит» canvas, завершается ошибкой SecurityError, поэтому отдавайте картинку с собственного origin приложения:

async function findTotal(img) {
  try {
    var lines = await new TextDetector().detect(img);
    var hit = lines.find(function (l) { return /total/i.test(l.rawValue); });
    var m = hit && hit.rawValue.match(/(\d+[.,]\d{2})/);
    return m ? parseFloat(m[1].replace(',', '.')) : null;
  } catch (e) {
    if (e.name === 'SecurityError') console.warn('Image is from another origin; load it from the app instead.');
    else console.warn('OCR failed:', e.name, e.message);
    return null;
  }
}

Примечания: реализовано в оболочке приложения (TextRecognitionBridge.kt).

__ocrRecognizeWith мост#

window.WebToApk.__ocrRecognizeWith(requestId: String, base64: String, optionsJson: String)

[__ocrRecognize] с параметрами [optionsJson] `{script: 'latin'|'devanagari'|'chinese'|'japanese'|'korean'|'auto', scripts?}`; ответ `appmint:ocr` также называет использованное письмо `script`.

Пример

Описано вместе с AppMint.textRecognition — пример там показывает и этот.

Читает текст на письменностях помимо латиницы: деванагари (хинди, маратхи, непальский …), китайский, японский и корейский — или 'auto', который пробует каждую письменность и оставляет результат с наибольшим числом символов. Та же опция работает и в стандартном детекторе: new TextDetector({ script: 'devanagari' }). Без этой опции всё читается как латиница, как и раньше.

Возвращает: recognize() → Promise с {script, text, blocks: [{text, box, corners, lines}]} (script = использованная письменность); при ошибке завершается с DOMException — NotSupportedError для неизвестной письменности или отсутствия Google Play services, иначе OperationError. prepare() → Promise с {script, status: 'ready'}. Требуется: Google Play services. Распознаватель каждой письменности загружается через Play services: письменности, названные в вашем коде, устанавливаются вместе с приложением, остальные — при первом использовании.

Текст на хинди с фотографии:

document.getElementById('photo').addEventListener('change', async function (e) {
  var file = e.target.files[0];
  if (!file || !(window.AppMint && AppMint.textRecognition)) return;
  try {
    var r = await AppMint.textRecognition.recognize(file, { script: 'devanagari' });
    document.getElementById('out').textContent = r.text;
  } catch (err) {
    alert('Could not read the photo: ' + err.name);
  }
});

Сначала загрузите модель письменности, с отображением прогресса — иначе при первом использовании на телефоне придётся ждать загрузки:

async function getReady() {
  var bar = document.getElementById('model-progress');
  await AppMint.textRecognition.prepare('japanese', function (s) {
    if (s.progress != null) bar.value = s.progress;      // 0..1 while 'downloading'
  });
  bar.hidden = true;
}

// The same states arrive as events, also when a recognize() call starts a download:
window.addEventListener('appmint:ocr-model', function (e) {
  console.log(e.detail.script, e.detail.status, e.detail.progress);   // 'downloading' | 'ready' | 'failed'
});

Смешанные документы — пусть приложение само выберет из ожидаемых вами письменностей:

var r = await AppMint.textRecognition.recognize(img, { script: 'auto', scripts: ['latin', 'chinese', 'korean'] });
console.log('read as ' + r.script + ': ' + r.text);

var lines = await new TextDetector({ script: 'korean' }).detect(img);   // standard shape, one entry per line

Примечания: scripts — список AppMint: latin, devanagari, chinese, japanese, korean. Для арабского, кириллицы, тайского и иврита распознавателя на устройстве нет. WebToApk.__ocrRecognizeWith(requestId, base64, optionsJson) — внутренний транспорт; используйте помощник.

prepareTextRecognition мост#

window.WebToApk.prepareTextRecognition(script: String): String

Заранее загружает распознаватель текста для [script]. Возвращает «checking» (затем `appmint:ocr-model {script, status: 'downloading'|'ready'|'failed', progress?}`), «unsupported-script» или «unavailable» (нет Google Play services).

Пример

Заранее загружает на устройство распознаватель текста для одной письменности, чтобы первый recognize() не ждал. Страницы обычно вызывают AppMint.textRecognition.prepare(script), который это оборачивает.

Возвращает: строку сразу — "checking" (результат придёт позже событиями appmint:ocr-model {script, status: 'downloading' | 'ready' | 'failed', progress?, error?}), "unsupported-script" или "unavailable" (нет Google Play services). 'auto' подготавливает все письменности. Требуется: Google Play services.

if (window.WebToApk && typeof window.WebToApk.prepareTextRecognition === 'function') {
  window.addEventListener('appmint:ocr-model', function (e) {
    if (e.detail.script !== 'chinese') return;
    if (e.detail.status === 'downloading') console.log('downloading', Math.round((e.detail.progress || 0) * 100) + '%');
    if (e.detail.status === 'ready') document.getElementById('scan').disabled = false;
    if (e.detail.status === 'failed') console.warn(e.detail.error);
  });
  var state = window.WebToApk.prepareTextRecognition('chinese');
  if (state !== 'checking') console.warn('Chinese text recognition: ' + state);
}

Подготовьте, затем сканируйте через обёртку:

async function readSign(canvas) {
  if (!(window.AppMint && AppMint.textRecognition)) return null;
  try {
    await AppMint.textRecognition.prepare('japanese');
    return await AppMint.textRecognition.recognize(canvas, { script: 'japanese' });
  } catch (err) {
    console.warn('Text recognition:', err.name, err.message);   // e.g. NotSupportedError without Play services
    return null;
  }
}

Неподдерживаемые письменности и отсутствие Play services сообщаются в возвращаемом значении:

const W = window.WebToApk;
if (W && typeof W.prepareTextRecognition === 'function') {
  const state = W.prepareTextRecognition('arabic');
  if (state === 'unsupported-script') showMessage('Arabic text cannot be read on the phone.');
  else if (state === 'unavailable') showMessage('Text recognition needs Google Play services.');
}

Примечания: письменности: latin, devanagari, chinese, japanese, korean, auto. Письменность, уже имеющаяся на телефоне, сразу отвечает ready.

scanCode мост#

window.WebToApk.scanCode(optionsJson: String, callbackId: String)

Открывает сканер кодов Google; [optionsJson] `{formats?, autoZoom?, manualInput?}`. Завершает промис AppMint.scanCode [callbackId].

Пример

Внутренняя часть AppMint.scanCode() — страницы используют этот помощник на Promise, который открывает сканер QR-кодов и штрихкодов Google и возвращает результат.

Возвращает: напрямую ничего; результат завершает промис AppMint.scanCode(). Требуется: ничего включать не нужно (разрешение на камеру не требуется). На телефоне должны быть Google Play services.

Публичный API:

async function scanTicket() {
  if (!(window.AppMint && AppMint.scanCode)) { alert('Open this page in the app to scan'); return; }
  var r = await AppMint.scanCode({ formats: ['qr_code'] });
  if (r.ok) checkTicket(r.value);                  // r.format, r.type, r.displayValue…
  else if (r.code !== 'cancelled') alert('Scan failed (' + r.code + ')');
}

Подключитесь к сети Wi-Fi по напечатанному QR-коду. Код Wi-Fi возвращается с type: 'wifi' и объектом wifi:

async function scanWifi() {
  if (!(window.AppMint && AppMint.scanCode)) return;
  var r = await AppMint.scanCode({ formats: ['qr_code'] });
  if (!r.ok) { if (r.code !== 'cancelled') alert('Scan failed (' + r.code + ')'); return; }
  if (r.type !== 'wifi') { alert('That is not a Wi-Fi code.'); return; }
  showWifiCard(r.wifi.ssid, r.wifi.password, r.wifi.encryption);   // encryption: 'wpa' | 'wep' | 'open'
}

Найдите товар по штрихкоду из магазина, позволив пользователю ввести цифры, если этикетка повреждена (manualInput). Первое сканирование на телефоне может загрузить сканер, и без сети это завершается ошибкой unavailable:

async function scanProduct() {
  if (!(window.AppMint && AppMint.scanCode)) { showManualEntry(); return; }
  var r = await AppMint.scanCode({ formats: ['ean_13', 'ean_8', 'upc_a', 'upc_e'], manualInput: true });
  if (r.ok) return lookUpProduct(r.value, r.format);            // e.g. '4006381333931', 'ean_13'
  if (r.code === 'busy') return;                                  // a scan is already open
  if (r.code === 'unavailable') alert('The scanner is not available: ' + r.message);
  else if (r.code !== 'cancelled') alert('Scan failed (' + r.code + ')');
}

Откройте то, на что указывает QR-код, по его расшифрованному type:

async function scanAndOpen() {
  var r = await AppMint.scanCode({ formats: ['qr_code'], autoZoom: true });
  if (!r.ok) return;
  if (r.type === 'url') location.href = r.url.url;
  else if (r.type === 'phone') location.href = 'tel:' + r.phone;
  else if (r.type === 'email') location.href = 'mailto:' + r.email.address;
  else if (r.type === 'geo') showOnMap(r.geo.lat, r.geo.lng);
  else if (r.type === 'contact') saveContact(r.contact.name, r.contact.phones, r.contact.emails);
  else showText(r.displayValue);
}

Примечания: низкоуровневый метод принимает optionsJson ({formats?, autoZoom?, manualInput?} в виде JSON-строки) и callbackId; ответ доставляется во внутреннюю таблицу ответов помощника, поэтому страница, вызывающая его напрямую, ответа не получит. Одновременно идёт только одно сканирование — второй вызов вернёт code: 'busy'. Все поля результата и коды ошибок см. в AppMint.scanCode.