123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330 |
- var Mode = require('./mode')
- var NumericData = require('./numeric-data')
- var AlphanumericData = require('./alphanumeric-data')
- var ByteData = require('./byte-data')
- var KanjiData = require('./kanji-data')
- var Regex = require('./regex')
- var Utils = require('./utils')
- var 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) {
- var segments = []
- var 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) {
- var numSegs = getSegments(Regex.NUMERIC, Mode.NUMERIC, dataStr)
- var alphaNumSegs = getSegments(Regex.ALPHANUMERIC, Mode.ALPHANUMERIC, dataStr)
- var byteSegs
- var 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 = []
- }
- var 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) {
- var 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) {
- var nodes = []
- for (var i = 0; i < segs.length; i++) {
- var 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) {
- var table = {}
- var graph = {'start': {}}
- var prevNodeIds = ['start']
- for (var i = 0; i < nodes.length; i++) {
- var nodeGroup = nodes[i]
- var currentNodeIds = []
- for (var j = 0; j < nodeGroup.length; j++) {
- var node = nodeGroup[j]
- var key = '' + i + j
- currentNodeIds.push(key)
- table[key] = { node: node, lastCount: 0 }
- graph[key] = {}
- for (var n = 0; n < prevNodeIds.length; n++) {
- var 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 (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) {
- var mode
- var 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) {
- var segs = getSegmentsFromString(data, Utils.isKanjiModeEnabled())
- var nodes = buildNodes(segs)
- var graph = buildGraph(nodes, version)
- var path = dijkstra.find_path(graph.map, 'start', 'end')
- var optimizedSegs = []
- for (var 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())
- )
- }
|