How to Build an ISBN Barcode Scanner in HTML5

The International Standard Book Number (ISBN) is a numeric commercial book identifier.1 It is a special kind of the EAN-13 barcode format for books. It may also include an EAN-5 add-on which indicates the price info.

example

We can use ISBN for book management, inventory tracking, retails, etc. In this article, we are going to build an ISBN barcode scanner in HTML5. It uses Dynamsoft Barcode Reader to scan barcodes and can work as a progressive web app.

Here is a list of what the web app does:

  • Scan ISBN barcode with add-on support.
  • Query the book’s info through the OpenLibrary’s API.
  • Get the book’s price via its add-on code and calculate the total price of scanned books.
  • Export the data to a CSV file.
  • Save the data into IndexedDB and load the data from IndexedDB.

There is also a video showing the scanning process:

Build an ISBN Barcode Scanner

Follow the steps to build an ISBN barcode scanner.

New HTML with a Basic Layout

Create a new HTML file with the following content.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ISBN Barcode Scanner Sample</title>
    <style>
      .scanner {
        display: none;
        position: absolute;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
      }
      
      .scanner.active {
        display: block;
      }

      .app {
        display: flex;
        flex-direction: column;
        width: calc(100% - 40px);
        padding-left: 20px;
        padding-right: 20px;
      }

      .title {
        align-self: center;
      }

      .inputContainer {
        display: flex;
        justify-content: space-between;
        min-height: 30px;
        width: 100%;
        flex-wrap: wrap;
      }

      .barcodeInput {
        flex-basis: 50%;
        flex-grow: 1;
        min-width: 100px;
      }

      .scanButton {
        flex-basis: 20%;
        flex-grow: 1;
      }

      .insertButton {
        flex-basis: 20%;
        flex-grow: 1;
      }

      .fullwidth {
        width: 100%;
      }

      table {
        border-collapse: collapse;
      }

      table, th, td {
        border: 1px solid black;
      }

      .resultContainer {
        padding-top: 1em;
      }
    </style>
  </head>
  <body>
    <div class="app">
      <h2 class="title">ISBN Barcode Scanner</h2>
      <div class="inputContainer">
        <input type="text" class="barcodeInput"/>
        <button class="scanButton">Scan</button>
        <button class="insertButton">Insert</button>
      </div>
      <div>
        <label for="queryTitle">Query title:
        <input type="checkbox" id="queryTitle" checked/></label>
        <span id="status"></span>
      </div>
      <div class="resultContainer fullwidth">
        <table class="results fullwidth">
          <tr>
            <th>ISBN</th>
            <th>Title</th>
            <th>Price</th>
            <th>Action</th>
          </tr>
        </table>
      </div>
      <div>
        <div>Total price: <span id="total"></span></div>
        <button class="downloadButton">Download CSV</button>
        <div>
          <button class="saveButton">Save to IndexedDB</button>
          <button class="loadButton">Load from IndexedDB</button>
        </div>
      </div>
      <div class="scanner"></div>
    </div>
    <script>
    </script>
  </body>
</html>

It has an input for users to enter the ISBN barcode info. The user can click “Scan” to scan the ISBN barcode and click “Insert” to insert the book’s info (ISBN, title, price) into the table. It will calculate the total price. The data can be exported to a CSV and saved to IndexedDB for persistent storage.

Test the layout on codepen:

See the Pen Untitled by Lihang Xu (@xulihang) on CodePen.

Next, let’s implement the app with JavaScript.

Implement the ISBN Barcode Scanning

  1. Include Dynamsoft Barcode Reader and Dynamsoft Camera Enhancer.

    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-javascript-barcode@9.4.0-iv11082320/dist/dbr.js"></script><!-- a version supporting EAN-5 add-on -->
    <script src="https://cdn.jsdelivr.net/npm/dynamsoft-camera-enhancer@3.3.1/dist/dce.js"></script>
    
  2. Initialize the barcode reader and the camera enhancer. Bind the UI element to the scanner container. A license is needed to use the barcode reader. You can apply for a license here.

    let reader;
    let enhancer;
    async function init(){
      updateStatus("Initializing...");
      Dynamsoft.DBR.BarcodeScanner.license = 'DLS2eyJoYW5kc2hha2VDb2RlIjoiMjAwMDAxLTE2NDk4Mjk3OTI2MzUiLCJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSIsInNlc3Npb25QYXNzd29yZCI6IndTcGR6Vm05WDJrcEQ5YUoifQ=='; // one-day trial
      reader = await Dynamsoft.DBR.BarcodeScanner.createInstance();
      await useEAN13Template();
      enhancer = await Dynamsoft.DCE.CameraEnhancer.createInstance();
      enhancer.on("played", (playCallbackInfo) => {
        console.log("camera started");
      });
      updateStatus("");
      await enhancer.setUIElement(Dynamsoft.DCE.CameraEnhancer.defaultUIElementURL);
      setScanRegion();
      let container = document.getElementsByClassName("scanner")[0];
      container.appendChild(enhancer.getUIElement());
    }
    
    function updateStatus(info){
      document.getElementById("status").innerText = info;
    }
    

    A scan region is set so that only a limited region of the camera frame will be used for reading barcodes.

    function setScanRegion(){
      enhancer.setScanRegion({
        regionLeft:0,
        regionTop:25,
        regionRight:100,
        regionBottom:55,
        regionMeasuredByPercentage: 1
      });
    }
    

    The runtime settings of the barcode reader is modified for reading ISBN barcodes. It is defined in a JSON template.

    async function useEAN13Template() {
      await reader.initRuntimeSettingsWithString(`
      {
        "FormatSpecification": {
          "EnableAddOnCode": 1,
          "Name": "defaultFormatParameterForAllBarcodeFormat"
        },
        "ImageParameter": {
          "BarcodeFormatIds": ["BF_EAN_13"],
          "BarcodeFormatIds_2": ["BF2_NULL"],
          "ExpectedBarcodesCount": 1,
          "FormatSpecificationNameArray": [
            "defaultFormatParameterForAllBarcodeFormat"
          ],
          "Name": "default",
          "Timeout": 3000
        },
        "Version": "3.0"
      }`);
    };
    
  3. Define the startScan function for the “Scan” button.

    function startScan(){
      if (!enhancer || !reader) {
        alert("Please wait for the initialization of Dynamsoft Barcode Reader");
        return;
      }
      document.getElementsByClassName("scanner")[0].classList.add("active");
      enhancer.open(true); //start the camera
    }
    
  4. Set an interval to read barcodes from the camera frames.

    let interval;
    let processing = false;
    function startProcessingLoop(isBarcode){
      stopProcessingLoop();
      interval = setInterval(captureAndDecode,100); // read barcodes
    }
    
    function stopProcessingLoop(){
      if (interval) {
        clearInterval(interval);
        interval = undefined;
      }
      processing = false;
    }
    
    async function captureAndDecode() {
      if (!enhancer || !reader) {
        return
      }
      if (enhancer.isOpen() === false) {
        return;
      }
      if (processing == true) {
        return;
      }
      processing = true; // set processing to true so that the next frame will be skipped if the processing has not completed.
      let frame = enhancer.getFrame();
      if (frame) {  
        let results = await reader.decode(frame);
        console.log(results);
        if (results.length > 0) {
          const result = results[0];
          document.getElementsByClassName("barcodeInput")[0].value = result.barcodeText;
          stopScan();
        }
        processing = false;
      }
    };
       
    function stopScan(){
      stopProcessingLoop();
      enhancer.close(true);
      document.getElementsByClassName("scanner")[0].classList.remove("active");
    }
    
  5. In the played event, start processing frames to get barcodes.

    enhancer.on("played", (playCallbackInfo) => { //triggered when the camera starts (change of resolution and camera will cause the camera to restart)
      startProcessingLoop();
    });
    

Insert the Book into the Table

  1. Get the price from the add-on. The first digit of the add-on indicates the currency. If it’s set to 0 or 1 the price is stated in GBP (£). 5 is US$, 6 is Canadian $, 3 is Australian $ and 4 is New Zealand $. The four following digits represent the price multiplied by 100.

    async function insert(){
      let parts = barcodeText.split("-") 
      let ISBN = parts[0];
      let addon = "";
      let price = "";
      if (parts.length = 2) { //has addon
        addon = barcodeText.split("-")[1];
        price = getPrice(addon);
      }
    }
       
    function getPrice(addon){
      let price = addon.substring(1,addon.length);
      price = price / 100;
      return price;
    }
    
  2. Get the title via the OpenLibrary’s API.

    async function insert(){
      let title = "";
      let jsonStr = await queryDetails(ISBN);
      let jsonObj = JSON.parse(jsonStr);
      if ("title" in jsonObj) {
        title = jsonObj["title"];
      }
    }
    
    function queryDetails(isbn){
      return new Promise(function (resolve, reject) {
        let URL = 'https://openlibrary.org/isbn/'+isbn+'.json';
        console.log(URL);
        let xhr = new XMLHttpRequest();
        xhr.open('GET', URL);
        xhr.onreadystatechange = function(){
          if(xhr.readyState === 4){
            resolve(xhr.responseText);
          }
        }
        xhr.onerror = function(){
          reject("error");
        }
        xhr.send();
      });
    }
    
  3. Insert all the book info as a row into the table.

    async function insert(barcodeText) {
      if (barcodeText) {
        let parts = barcodeText.split("-") 
        let ISBN = parts[0];
        let title = "";
        let addon = "";
        let price = "";
        if (parts.length = 2) { //has addon
          addon = barcodeText.split("-")[1];
          price = getPrice(addon);
        }
        if (document.getElementById("queryTitle").checked) {
          try {
            let jsonStr = await queryDetails(ISBN);
            let jsonObj = JSON.parse(jsonStr);
            if ("title" in jsonObj) {
              title = jsonObj["title"];
            }
          } catch (error) {
            console.log(error);
          }
        }
        let rowValues = [ISBN,title,price];
        insertRow(rowValues);
      }
    }
          
    function insertRow(rowValues){
      let table = document.getElementsByClassName("results")[0];
      let tr = document.createElement("tr");
      for (let index = 0; index < rowValues.length; index++) {
        const value = rowValues[index];
        let td = document.createElement("td");
        td.innerText = value;
        tr.appendChild(td)
      }
      let td = document.createElement("td");
      let deleteButton = document.createElement("button");
      deleteButton.innerText = "Delete";
      deleteButton.addEventListener("click",function(){
        tr.parentElement.removeChild(tr);
        calculateTotalPrice();
      });
      td.appendChild(deleteButton);
      tr.appendChild(td);
      table.appendChild(tr);
    }
    
  4. Calculate the total price if the table is changed.

    const priceColumnIndex = 2;
    function calculateTotalPrice(){
      let table = document.getElementsByClassName("results")[0];
      let rows = table.getElementsByTagName("tr");
      let total = 0;
      for (let index = 1; index < rows.length; index++) {
        const row = rows[index];
        const cells = row.getElementsByTagName("td");
        const price = parseFloat(cells[priceColumnIndex].innerText);
        total = total + price;
      }
      document.getElementById("total").innerText = total;
    }
    

Export the Data into a CSV File

Use the following code to build the content of a CSV file and download it.

function downloadCSV() {
  let csv = getDataTable(true);
  let csv_string = csv.join('\n');
  // Download it
  let filename = 'out.csv';
  let link = document.createElement('a');
  link.style.display = 'none';
  link.setAttribute('target', '_blank');
  link.setAttribute('href', 'data:text/csv;charset=utf-8,' + encodeURIComponent(csv_string));
  link.setAttribute('download', filename);
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

function getDataTable(toCSV, separator = ','){
  // Select rows from table
  let rows = document.getElementsByTagName('tr');
  // Construct table
  let table = [];
  for (let i = 0; i < rows.length; i++) {
    let row = [], cols = rows[i].querySelectorAll('td, th');
    for (let j = 0; j < cols.length - 1; j++) { //do not include the last column
      if (toCSV) {
        // Clean innertext to remove multiple spaces and jumpline (break csv)
        let data = cols[j].innerText.replace(/(\r\n|\n|\r)/gm, '').replace(/(\s\s)/gm, ' ')
        // Escape double-quote with double-double-quote (see https://stackoverflow.com/questions/17808511/properly-escape-a-double-quote-in-csv)
        data = data.replace(/"/g, '""');
        // Push escaped string
        row.push('"' + data + '"');
      }else{
        let data = cols[j].innerText;
        row.push(data);
      }            
    }
    if (toCSV) {
      table.push(row.join(separator));
    }else{
      table.push(row);
    }
  }
  return table;
}

Save the Data into IndexedDB

IndexedDB is a low-level API for client-side storage of significant amounts of structured data. It is ideal for storing persistent data like the table of books. For ease of use, we are going to use the localForage library.

Save the table’s data into IndexedDB:

function saveToIndexedDB(){
  let table = getDataTable();
  localforage.setItem('ISBN', table).then(function (value) {
    alert("Saved");
  }).catch(function(err) {
    alert(err);
  });
}

Load the table’s data from IndexedDB:

function loadFromIndexedDB(){
  localforage.getItem('ISBN').then(function (value) {
    loadDataToTable(value);
    calculateTotalPrice();
  }).catch(function(err) {
    alert(err);
  });
}

function loadDataToTable(table){
  resetTable();
  for (let index = 1; index < table.length; index++) {
    const row = table[index];
    insertRow(row);
  }
}

function resetTable(){
  let table = document.getElementsByClassName("results")[0];
  while (table.getElementsByTagName("tr").length > 1) {
    let rows = table.getElementsByTagName("tr");
    table.removeChild(rows[rows.length-1]);
  }
}

Make the App Work as a Progressive Web App

We can take a step further to make the app a progressive web app so that it can be installed and work offline. We can find a guide on how to do this here.

  1. Create a new manifest file named app.webmanifest.

    {
      "name": "ISBN Barcode Reader Progressive Web App",
      "short_name": "ISBN Reader",
      "description": "A progressive web app to scan ISBN barcodes using Dynamsoft Barcode Reader.",
      "icons": [
        {
          "src": "icon128.png",
          "sizes": "128x128",
          "type": "image/png"
        }
      ],
      "start_url": "index.html",
      "display": "standalone",
      "background_color": "purple"
    }
    
  2. Link it in the HTML:

    <link rel="manifest" href="app.webmanifest" />
    
  3. Create a service worker to cache static files and files requested.

    const cacheName = "ISBNReader";
    const appShellFiles = [
      "index.html",
      "icon128.png",
    ];
    const contentToCache = appShellFiles;
    
    self.addEventListener("install", (e) => {
      console.log("[Service Worker] Install");
      e.waitUntil(
        (async () => {
          const cache = await caches.open(cacheName);
          console.log("[Service Worker] Caching all: app shell and content");
          await cache.addAll(contentToCache);
        })()
      );
    });
    
    self.addEventListener("fetch", (e) => {
      e.respondWith(
        (async () => {
          const r = await caches.match(e.request);
          console.log(`[Service Worker] Fetching resource: ${e.request.url}`);
          if (r) {
            return r;
          }
          const response = await fetch(e.request);
          const cache = await caches.open(cacheName);
          console.log(`[Service Worker] Caching new resource: ${e.request.url}`);
          cache.put(e.request, response.clone());
          return response;
        })()
      );
    });
    
  4. Register the service worker in the index file.

    if ("serviceWorker" in navigator) {
      navigator.serviceWorker.register("sw.js").then(function (registration) {
        console.log('ServiceWorker registration successful with scope: ', registration.scope);
      }, function (err) {
        console.log('ServiceWorker registration failed: ', err);
      }).catch(function (err) {
        console.log(err);
      });
    }
    

All right, we’ve now finished the ISBN barcode scanner. You can check out the online demo to have a try.

Source Code

You can find the source code of the demo in the following repo: https://github.com/tony-xlh/ISBN-Barcode-Scanner

This blog talks about storing the scanned books on Airtable: How to Scan Barcodes to Save Books to Airtable

References