Guía 32 – Lector de código de barras 2D

Al igual que cuando facilité un ejemplo para generar códigos QR, al facilitar un ejemplo para leer códigos QR han surgido peticiones de lectores, de hacer algo similar para código de barras de 2D.

He seguido el mismo método que emplee para para los QR. He estado viendo ejemplos y casi todo lo que he visto me enviaba a esta página de Quagga.

Las posibilidades son casi infinitas. No me ha sido fácil decirme cuál ejemplo utilizar, ni ajustarlo a la funcionalidad que deseaba tuviera el ejemplo.

Objetivo

Poder leer códigos de barras de 2D en las aplicaciones realizadas en PHPRunner. Principalmente, para que utilizando un móvil podamos producir entrada de información desde un código de barras.

DEMO: https://fhumanes.com/scannerBarcode/

Solución Técnica

Como he indicado la solución se basa en el uso de la solución de Quagga. Es muy importante que quienes vayáis a usar esta solución accedáis a la información de la solución para conocer todas las posibilidades que facilita, así como todas las opciones de configuración que dispone. El ejemplo queda bastante abierto, ya que seguramente lo deberéis ajustar al tipo de código de barras que requiera vuestro producto.

Como en la solución de la lectura de QR, la solución pasa por dividir el código en:

  • Codificación de HTML, se integra mediante un código “snippet“.
  • Codificación de JavaScript, se integra en el evento “JavaScript OnLoad Event

El aspecto de la aplicación es:


Veis que la solución utiliza un rectángulo para fijar la detección del código de barras y también utiliza una raya roja para identificar cuándo ha detectado la información del código.

Las opciones que presenta el ejemplo se pueden ocultar mediante un atributo del HTML, y así no ser personalizables. También, se pueden indicar cuáles son las opciones por defecto que deseáis dejar.

El código del snippet es:

$html =<<<EOT
<link rel="stylesheet" href="MyCode/quagga.css">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">

<div class="wrapper">
    <div id="container" class="container">
      <div class="controls">
         <fieldset class="reader-config-group">
            <button class="stop">Stop</button><br>
            <label>
              <span>Barcode-Type</span>
              <select name="decoder_readers">
                 <option value="code_128" selected="selected">Code 128</option>
                 <option value="code_39">Code 39</option>
                 <option value="code_39_vin">Code 39 VIN</option>
                 <option value="ean">EAN</option>
                 <option value="ean_extended">EAN-extended</option>
                 <option value="ean_8">EAN-8</option>
                 <option value="upc">UPC</option>
                 <option value="upc_e">UPC-E</option>
                 <option value="codabar">Codabar</option>
                 <option value="i2of5">I2of5</option>
                 <option value="2of5">Standard 2 of 5</option>
                 <option value="code_93">Code 93</option>
              </select>
            </label>
            <label>
              <span>Resolution (long side)</span>
              <select name="input-stream_constraints">
                 <option value="320x240">320px</option>
                 <option selected="selected" value="640x480">640px</option>
                 <option value="800x600">800px</option>
                 <option value="1280x720">1280px</option>
                 <option value="1600x960">1600px</option>
                 <option value="1920x1080">1920px</option>
              </select>
            </label>
            <label>
              <span>Patch-Size</span>
              <select name="locator_patch-size">
                 <option value="x-small">x-small</option>
                 <option value="small">small</option>
                 <option selected="selected" value="medium">medium</option>
                 <option value="large">large</option>
                 <option value="x-large">x-large</option>
              </select>
            </label>
            <label>
              <span>Half-Sample</span>
              <input type="checkbox" checked="checked" name="locator_half-sample" />
            </label>
            <label>
              <span>Workers</span>
              <select name="numOfWorkers">
                 <option value="0">0</option>
                 <option value="1">1</option>
                 <option value="2">2</option>
                 <option selected="selected" value="4">4</option>
                 <option value="8">8</option>
              </select>
            </label>
            <label>
              <span>Camera</span>
              <select name="input-stream_constraints" id="deviceSelection">
              </select>
            </label>
            <label style="display: none">
              <span>Zoom</span>
              <select name="settings_zoom"></select>
            </label>
            <label style="display: none">
              <span>Torch</span>
              <input type="checkbox" name="settings_torch" />
            </label>
         </fieldset>
      </div>
      <div id="interactive" class="viewport"></div>
    </div>
 </div>
 <script src="MyCode/adapter.js" type="text/javascript"></script>
 <script src="MyCode/quagga.min.js" type="text/javascript"></script>
 <script src="MyCode/scale.fix.js"></script>

EOT;
echo $html;

El código de JavaScript es:

var ctrlBarcode = Runner.getControl(pageid, 'text1');
ctrlBarcode.makeReadonly();

$(function () {
    var App = {
        init: function () {
            Quagga.init(this.state, function (err) {
                if (err) {
                    console.log(err);
                    return;
                }
                App.attachListeners();
                App.checkCapabilities();
                Quagga.start();
            });
        },
        checkCapabilities: function () {
            var track = Quagga.CameraAccess.getActiveTrack();
            var capabilities = {};
            if (typeof track.getCapabilities === 'function') {
                capabilities = track.getCapabilities();
            }
            this.applySettingsVisibility('zoom', capabilities.zoom);
            this.applySettingsVisibility('torch', capabilities.torch);
        },
        updateOptionsForMediaRange: function (node, range) {
            console.log('updateOptionsForMediaRange', node, range);
            var NUM_STEPS = 6;
            var stepSize = (range.max - range.min) / NUM_STEPS;
            var option;
            var value;
            while (node.firstChild) {
                node.removeChild(node.firstChild);
            }
            for (var i = 0; i <= NUM_STEPS; i++) {
                value = range.min + (stepSize * i);
                option = document.createElement('option');
                option.value = value;
                option.innerHTML = value;
                node.appendChild(option);
            }
        },
        applySettingsVisibility: function (setting, capability) {
            // depending on type of capability
            if (typeof capability === 'boolean') {
                var node = document.querySelector('input[name="settings_' + setting + '"]');
                if (node) {
                    node.parentNode.style.display = capability ? 'block' : 'none';
                }
                return;
            }
            if (window.MediaSettingsRange && capability instanceof window.MediaSettingsRange) {
                var node = document.querySelector('select[name="settings_' + setting + '"]');
                if (node) {
                    this.updateOptionsForMediaRange(node, capability);
                    node.parentNode.style.display = 'block';
                }
                return;
            }
        },
        initCameraSelection: function () {
            var streamLabel = Quagga.CameraAccess.getActiveStreamLabel();

            return Quagga.CameraAccess.enumerateVideoDevices()
                .then(function (devices) {
                    function pruneText(text) {
                        return text.length > 30 ? text.substr(0, 30) : text;
                    }
                    var $deviceSelection = document.getElementById("deviceSelection");
                    while ($deviceSelection.firstChild) {
                        $deviceSelection.removeChild($deviceSelection.firstChild);
                    }
                    devices.forEach(function (device) {
                        var $option = document.createElement("option");
                        $option.value = device.deviceId || device.id;
                        $option.appendChild(document.createTextNode(pruneText(device
                            .label || device.deviceId || device.id)));
                        $option.selected = streamLabel === device.label;
                        $deviceSelection.appendChild($option);
                    });
                });
        },
        attachListeners: function () {
            var self = this;

            self.initCameraSelection();
            $(".controls").on("click", "button.stop", function (e) {
                e.preventDefault();
                Quagga.stop();
            });

            $(".controls .reader-config-group").on("change", "input, select", function (e) {
                e.preventDefault();
                var $target = $(e.target),
                    value = $target.attr("type") === "checkbox" ? $target.prop(
                        "checked") : $target.val(),
                    name = $target.attr("name"),
                    state = self._convertNameToState(name);

                console.log("Value of " + state + " changed to " + value);
                self.setState(state, value);
            });
        },
        _accessByPath: function (obj, path, val) {
            var parts = path.split('.'),
                depth = parts.length,
                setter = (typeof val !== "undefined") ? true : false;

            return parts.reduce(function (o, key, i) {
                if (setter && (i + 1) === depth) {
                    if (typeof o[key] === "object" && typeof val === "object") {
                        Object.assign(o[key], val);
                    } else {
                        o[key] = val;
                    }
                }
                return key in o ? o[key] : {};
            }, obj);
        },
        _convertNameToState: function (name) {
            return name.replace("_", ".").split("-").reduce(function (result, value) {
                return result + value.charAt(0).toUpperCase() + value.substring(1);
            });
        },
        detachListeners: function () {
            $(".controls").off("click", "button.stop");
            $(".controls .reader-config-group").off("change", "input, select");
        },
        applySetting: function (setting, value) {
            var track = Quagga.CameraAccess.getActiveTrack();
            if (track && typeof track.getCapabilities === 'function') {
                switch (setting) {
                    case 'zoom':
                        return track.applyConstraints({
                            advanced: [{
                                zoom: parseFloat(value)
                            }]
                        });
                    case 'torch':
                        return track.applyConstraints({
                            advanced: [{
                                torch: !!value
                            }]
                        });
                }
            }
        },
        setState: function (path, value) {
            var self = this;

            if (typeof self._accessByPath(self.inputMapper, path) === "function") {
                value = self._accessByPath(self.inputMapper, path)(value);
            }

            if (path.startsWith('settings.')) {
                var setting = path.substring(9);
                return self.applySetting(setting, value);
            }
            self._accessByPath(self.state, path, value);

            console.log(JSON.stringify(self.state));
            App.detachListeners();
            Quagga.stop();
            App.init();
        },
        inputMapper: {
            inputStream: {
                constraints: function (value) {
                    if (/^(\d+)x(\d+)$/.test(value)) {
                        var values = value.split('x');
                        return {
                            width: {
                                min: parseInt(values[0])
                            },
                            height: {
                                min: parseInt(values[1])
                            }
                        };
                    }
                    return {
                        deviceId: value
                    };
                }
            },
            numOfWorkers: function (value) {
                return parseInt(value);
            },
            decoder: {
                readers: function (value) {
                    if (value === 'ean_extended') {
                        return [{
                            format: "ean_reader",
                            config: {
                                supplements: [
                                    'ean_5_reader', 'ean_2_reader'
                                ]
                            }
                        }];
                    }
                    return [{
                        format: value + "_reader",
                        config: {}
                    }];
                }
            }
        },
        state: {
            inputStream: {
                type: "LiveStream",
                constraints: {
                    width: {
                        min: 640
                    },
                    height: {
                        min: 480
                    },
                    aspectRatio: {
                        min: 1,
                        max: 100
                    },
                    facingMode: "environment" // or user
                }
            },
            locator: {
                patchSize: "medium",
                halfSample: true
            },
            numOfWorkers: 2,
            frequency: 10,
            decoder: {
                readers: [{
                    format: "code_128_reader",
                    config: {}
                }]
            },
            locate: true
        },
        lastResult: null
    };

    App.init();

    Quagga.onProcessed(function (result) {
        var drawingCtx = Quagga.canvas.ctx.overlay,
            drawingCanvas = Quagga.canvas.dom.overlay;

        if (result) {
            if (result.boxes) {
                drawingCtx.clearRect(0, 0, parseInt(drawingCanvas.getAttribute("width")),
                    parseInt(drawingCanvas.getAttribute("height")));
                result.boxes.filter(function (box) {
                    return box !== result.box;
                }).forEach(function (box) {
                    Quagga.ImageDebug.drawPath(box, {
                        x: 0,
                        y: 1
                    }, drawingCtx, {
                        color: "green",
                        lineWidth: 2
                    });
                });
            }

            if (result.box) {
                Quagga.ImageDebug.drawPath(result.box, {
                    x: 0,
                    y: 1
                }, drawingCtx, {
                    color: "#00F",
                    lineWidth: 2
                });
            }

            if (result.codeResult && result.codeResult.code) {
                Quagga.ImageDebug.drawPath(result.line, {
                    x: 'x',
                    y: 'y'
                }, drawingCtx, {
                    color: 'red',
                    lineWidth: 3
                });
            }
        }
    });

    Quagga.onDetected(function (result) {
        var code = result.codeResult.code;
      ctrlBarcode.setValue(code);        //  Put the value in the field   <<<-------------------------------------------------------------
      var audio = new Audio('MyCode/beep.mp3');
      audio.play();
        // alert(code);
      Quagga.stop();
    });
});

Indicaros que he entendido y corregido muy poco de este código. Casi todo él es el resultado de obtener el ejemplo de la web del fabricante.

Para lo que necesitéis, podéis contactarme a través de mi email [email protected]

Os dejo el ejemplo con una copia de la tabla necesaria, para que lo instaléis en vuestros Windows y podáis hacer todas las pruebas que necesitéis.

Si estáis pensando en un sistema en que generéis el código de barras y la captura de información a través de él, creo que la mejor solución es utilizar código QR.

Adjuntos

Archivo Tamaño de archivo Descargas
zip PHPRunner 10.4 y backup de base de datos 363 KB 282

Blog personal para facilitar soporte gratuito a usuarios de PHPRunner