const Mode = require('./mode') const NumericData = require('./numeric-data') const AlphanumericData = require('./alphanumeric-data') const ByteData = require('./byte-data') const KanjiData = require('./kanji-data') const Regex = require('./regex') const Utils = require('./utils') const dijkstra = require('dijkstrajs') /** * Returns UTF8 byte length * * @param {String} str Input string * @return {Number} Number of byte */ function getStringByteLength (str) { return unescape(encodeURIComponent(str)).length } /** * Get a list of segments of the specified mode * from a string * * @param {Mode} mode Segment mode * @param {String} str String to process * @return {Array} Array of object with segments data */ function getSegments (regex, mode, str) { const segments = [] let result while ((result = regex.exec(str)) !== null) { segments.push({ data: result[0], index: result.index, mode: mode, length: result[0].length }) } return segments } /** * Extracts a series of segments with the appropriate * modes from a string * * @param {String} dataStr Input string * @return {Array} Array of object with segments data */ function getSegmentsFromString (dataStr) { const numSegs = getSegments(Regex.NUMERIC, Mode.NUMERIC, dataStr) const alphaNumSegs = getSegments(Regex.ALPHANUMERIC, Mode.ALPHANUMERIC, dataStr) let byteSegs let kanjiSegs if (Utils.isKanjiModeEnabled()) { byteSegs = getSegments(Regex.BYTE, Mode.BYTE, dataStr) kanjiSegs = getSegments(Regex.KANJI, Mode.KANJI, dataStr) } else { byteSegs = getSegments(Regex.BYTE_KANJI, Mode.BYTE, dataStr) kanjiSegs = [] } const segs = numSegs.concat(alphaNumSegs, byteSegs, kanjiSegs) return segs .sort(function (s1, s2) { return s1.index - s2.index }) .map(function (obj) { return { data: obj.data, mode: obj.mode, length: obj.length } }) } /** * Returns how many bits are needed to encode a string of * specified length with the specified mode * * @param {Number} length String length * @param {Mode} mode Segment mode * @return {Number} Bit length */ function getSegmentBitsLength (length, mode) { switch (mode) { case Mode.NUMERIC: return NumericData.getBitsLength(length) case Mode.ALPHANUMERIC: return AlphanumericData.getBitsLength(length) case Mode.KANJI: return KanjiData.getBitsLength(length) case Mode.BYTE: return ByteData.getBitsLength(length) } } /** * Merges adjacent segments which have the same mode * * @param {Array} segs Array of object with segments data * @return {Array} Array of object with segments data */ function mergeSegments (segs) { return segs.reduce(function (acc, curr) { const prevSeg = acc.length - 1 >= 0 ? acc[acc.length - 1] : null if (prevSeg && prevSeg.mode === curr.mode) { acc[acc.length - 1].data += curr.data return acc } acc.push(curr) return acc }, []) } /** * Generates a list of all possible nodes combination which * will be used to build a segments graph. * * Nodes are divided by groups. Each group will contain a list of all the modes * in which is possible to encode the given text. * * For example the text '12345' can be encoded as Numeric, Alphanumeric or Byte. * The group for '12345' will contain then 3 objects, one for each * possible encoding mode. * * Each node represents a possible segment. * * @param {Array} segs Array of object with segments data * @return {Array} Array of object with segments data */ function buildNodes (segs) { const nodes = [] for (let i = 0; i < segs.length; i++) { const seg = segs[i] switch (seg.mode) { case Mode.NUMERIC: nodes.push([seg, { data: seg.data, mode: Mode.ALPHANUMERIC, length: seg.length }, { data: seg.data, mode: Mode.BYTE, length: seg.length } ]) break case Mode.ALPHANUMERIC: nodes.push([seg, { data: seg.data, mode: Mode.BYTE, length: seg.length } ]) break case Mode.KANJI: nodes.push([seg, { data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) } ]) break case Mode.BYTE: nodes.push([ { data: seg.data, mode: Mode.BYTE, length: getStringByteLength(seg.data) } ]) } } return nodes } /** * Builds a graph from a list of nodes. * All segments in each node group will be connected with all the segments of * the next group and so on. * * At each connection will be assigned a weight depending on the * segment's byte length. * * @param {Array} nodes Array of object with segments data * @param {Number} version QR Code version * @return {Object} Graph of all possible segments */ function buildGraph (nodes, version) { const table = {} const graph = { start: {} } let prevNodeIds = ['start'] for (let i = 0; i < nodes.length; i++) { const nodeGroup = nodes[i] const currentNodeIds = [] for (let j = 0; j < nodeGroup.length; j++) { const node = nodeGroup[j] const key = '' + i + j currentNodeIds.push(key) table[key] = { node: node, lastCount: 0 } graph[key] = {} for (let n = 0; n < prevNodeIds.length; n++) { const prevNodeId = prevNodeIds[n] if (table[prevNodeId] && table[prevNodeId].node.mode === node.mode) { graph[prevNodeId][key] = getSegmentBitsLength(table[prevNodeId].lastCount + node.length, node.mode) - getSegmentBitsLength(table[prevNodeId].lastCount, node.mode) table[prevNodeId].lastCount += node.length } else { if (table[prevNodeId]) table[prevNodeId].lastCount = node.length graph[prevNodeId][key] = getSegmentBitsLength(node.length, node.mode) + 4 + Mode.getCharCountIndicator(node.mode, version) // switch cost } } } prevNodeIds = currentNodeIds } for (let n = 0; n < prevNodeIds.length; n++) { graph[prevNodeIds[n]].end = 0 } return { map: graph, table: table } } /** * Builds a segment from a specified data and mode. * If a mode is not specified, the more suitable will be used. * * @param {String} data Input data * @param {Mode | String} modesHint Data mode * @return {Segment} Segment */ function buildSingleSegment (data, modesHint) { let mode const bestMode = Mode.getBestModeForData(data) mode = Mode.from(modesHint, bestMode) // Make sure data can be encoded if (mode !== Mode.BYTE && mode.bit < bestMode.bit) { throw new Error('"' + data + '"' + ' cannot be encoded with mode ' + Mode.toString(mode) + '.\n Suggested mode is: ' + Mode.toString(bestMode)) } // Use Mode.BYTE if Kanji support is disabled if (mode === Mode.KANJI && !Utils.isKanjiModeEnabled()) { mode = Mode.BYTE } switch (mode) { case Mode.NUMERIC: return new NumericData(data) case Mode.ALPHANUMERIC: return new AlphanumericData(data) case Mode.KANJI: return new KanjiData(data) case Mode.BYTE: return new ByteData(data) } } /** * Builds a list of segments from an array. * Array can contain Strings or Objects with segment's info. * * For each item which is a string, will be generated a segment with the given * string and the more appropriate encoding mode. * * For each item which is an object, will be generated a segment with the given * data and mode. * Objects must contain at least the property "data". * If property "mode" is not present, the more suitable mode will be used. * * @param {Array} array Array of objects with segments data * @return {Array} Array of Segments */ exports.fromArray = function fromArray (array) { return array.reduce(function (acc, seg) { if (typeof seg === 'string') { acc.push(buildSingleSegment(seg, null)) } else if (seg.data) { acc.push(buildSingleSegment(seg.data, seg.mode)) } return acc }, []) } /** * Builds an optimized sequence of segments from a string, * which will produce the shortest possible bitstream. * * @param {String} data Input string * @param {Number} version QR Code version * @return {Array} Array of segments */ exports.fromString = function fromString (data, version) { const segs = getSegmentsFromString(data, Utils.isKanjiModeEnabled()) const nodes = buildNodes(segs) const graph = buildGraph(nodes, version) const path = dijkstra.find_path(graph.map, 'start', 'end') const optimizedSegs = [] for (let i = 1; i < path.length - 1; i++) { optimizedSegs.push(graph.table[path[i]].node) } return exports.fromArray(mergeSegments(optimizedSegs)) } /** * Splits a string in various segments with the modes which * best represent their content. * The produced segments are far from being optimized. * The output of this function is only used to estimate a QR Code version * which may contain the data. * * @param {string} data Input string * @return {Array} Array of segments */ exports.rawSplit = function rawSplit (data) { return exports.fromArray( getSegmentsFromString(data, Utils.isKanjiModeEnabled()) ) }