You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							330 lines
						
					
					
						
							9.1 KiB
						
					
					
				
			
		
		
		
			
			
			
				
					
				
				
					
				
			
		
		
	
	
							330 lines
						
					
					
						
							9.1 KiB
						
					
					
				| 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()) | |
|   ) | |
| }
 | |
| 
 |