@@ -35,8 +35,7 @@ export class CodeOneCore {
3535 generate ( ) {
3636 const requested = this . options . version == null ? null : String ( this . options . version ) . toUpperCase ( )
3737 if ( requested ?. startsWith ( 'T' ) ) {
38- rejectUnsupportedControlOptions ( this . options , 'T' )
39- return generateVersionT ( this . data , requested )
38+ return generateVersionT ( this . data , requested , this . options )
4039 }
4140 if ( requested === 'A-H' || VERSION_MAIN . some ( version => version . name === requested ) ) {
4241 return generateMainVersion ( this . data , requested , this . options )
@@ -165,20 +164,22 @@ function placeVersionS(codewords, version) {
165164 return modules
166165}
167166
168- function generateVersionT ( value , requested ) {
169- const data = normalizeLatin1 ( value )
170- if ( data . length > 90 ) throw new RangeError ( 'Code One Version T accepts at most 90 Latin-1 characters.' )
171- const encoded = encodeVersionTData ( data )
172- const version = chooseVersionT ( encoded . length , requested )
167+ function generateVersionT ( value , requested , options ) {
168+ const data = normalizeMainData ( value , options )
169+ const controls = createControlPrefix ( options , data )
170+ if ( controls . data . length > 90 ) throw new RangeError ( 'Code One Version T accepts at most 90 encoded characters including ECI escapes.' )
171+ const selection = chooseEncoding ( controls . data , VERSION_T , requested , 'T' , options . mode , controls . prefix , controls . gs1 )
172+ const { version, encoded, mode } = selection
173173 const dataCodewords = [ ...encoded , ...Array ( version . dataWords - encoded . length ) . fill ( 129 ) ]
174174 const checkCodewords = reedSolomonInField ( dataCodewords , version . checkWords , 0x12d , 256 )
175175 const codewords = [ ...dataCodewords , ...checkCodewords ]
176176 const modules = placeVersionT ( codewords , version )
177177 return {
178- format : 'CodeOne' , family : 'T' , version : version . name , data,
178+ format : 'CodeOne' , family : 'T' , version : version . name , data : value ,
179179 width : modules [ 0 ] . length , height : modules . length , modules,
180180 dataCodewords, checkCodewords, codewords,
181- encodingMode : selectVersionTMode ( data ) , readyForScan : true ,
181+ encodingMode : mode , gs1 : controls . gs1 , eci : controls . eci ,
182+ structuredAppend : controls . structuredAppend , readyForScan : true ,
182183 }
183184}
184185
@@ -202,23 +203,6 @@ function rejectUnsupportedControlOptions(options, family) {
202203 }
203204}
204205
205- function selectVersionTMode ( data ) {
206- if ( / ^ \d { 90 } $ / . test ( data ) ) return 'decimal'
207- if ( / ^ [ A - Z 0 - 9 ] + $ / . test ( data ) ) {
208- const remainder = data . length % 3
209- const codewordLength = 1 + Math . floor ( data . length / 3 ) * 2 + ( remainder === 1 ? 1 : remainder === 2 ? 2 : 0 )
210- if ( VERSION_T . some ( version => version . dataWords === codewordLength ) ) return 'c40'
211- }
212- return 'ascii'
213- }
214-
215- function encodeVersionTData ( data ) {
216- const mode = selectVersionTMode ( data )
217- if ( mode === 'decimal' ) return encodeDecimal ( data )
218- if ( mode === 'c40' ) return encodeC40 ( data )
219- return encodeAscii ( data )
220- }
221-
222206function encodeAscii ( data , gs1 = false ) {
223207 const codewords = [ ]
224208 for ( let i = 0 ; i < data . length ; ) {
@@ -235,48 +219,6 @@ function encodeAscii(data, gs1 = false) {
235219 return codewords
236220}
237221
238- function encodeC40 ( data ) {
239- const values = [ ]
240- for ( const character of data ) {
241- if ( character === ' ' ) values . push ( 3 )
242- else if ( / \d / . test ( character ) ) values . push ( character . charCodeAt ( 0 ) - 44 )
243- else values . push ( character . charCodeAt ( 0 ) - 51 )
244- }
245- const codewords = [ 230 ]
246- let position = 0
247- while ( position + 2 < values . length ) {
248- const packed = 1600 * values [ position ] + 40 * values [ position + 1 ] + values [ position + 2 ] + 1
249- codewords . push ( packed >> 8 , packed & 255 )
250- position += 3
251- }
252- const remaining = values . length - position
253- if ( remaining === 1 ) codewords . push ( data . charCodeAt ( position ) + 1 )
254- else if ( remaining === 2 ) {
255- const packed = 1600 * values [ position ] + 40 * values [ position + 1 ] + 1
256- codewords . push ( packed >> 8 , packed & 255 )
257- }
258- return codewords
259- }
260-
261- function encodeDecimal ( data ) {
262- let bits = '1111'
263- for ( let i = 0 ; i < data . length ; i += 3 ) bits += ( Number ( data . slice ( i , i + 3 ) ) + 1 ) . toString ( 2 ) . padStart ( 10 , '0' )
264- if ( bits . length % 8 !== 0 ) throw new Error ( 'Internal Code One decimal alignment error.' )
265- const codewords = [ ]
266- for ( let i = 0 ; i < bits . length ; i += 8 ) codewords . push ( Number . parseInt ( bits . slice ( i , i + 8 ) , 2 ) )
267- return codewords
268- }
269-
270- function chooseVersionT ( length , requested ) {
271- const minimum = VERSION_T . find ( version => length <= version . dataWords )
272- if ( ! minimum ) throw new RangeError ( `Input requires ${ length } codewords; Code One T-48 allows at most 38.` )
273- if ( requested === 'T' ) return minimum
274- const version = VERSION_T . find ( candidate => candidate . name === requested )
275- if ( ! version ) throw new RangeError ( 'Supported Code One T versions are T, T-16, T-32 and T-48.' )
276- if ( length > version . dataWords ) throw new RangeError ( `${ requested } accepts at most ${ version . dataWords } data codewords; input requires ${ length } .` )
277- return version
278- }
279-
280222function placeVersionT ( codewords , version ) {
281223 const sub = VERSION_T . indexOf ( version ) + 1
282224 const width = sub * 16 + 1
@@ -375,13 +317,17 @@ function normalizeStructuredAppend(value) {
375317}
376318
377319function chooseMainEncoding ( data , requested , requestedMode , prefix = [ ] , gs1 = false ) {
320+ return chooseEncoding ( data , VERSION_MAIN , requested , 'A-H' , requestedMode , prefix , gs1 )
321+ }
322+
323+ function chooseEncoding ( data , allVersions , requested , automaticName , requestedMode , prefix = [ ] , gs1 = false ) {
378324 const mode = requestedMode == null ? 'auto' : String ( requestedMode ) . toLowerCase ( )
379325 if ( ! [ 'auto' , 'ascii' , 'c40' , 'text' , 'edi' , 'decimal' , 'byte' ] . includes ( mode ) ) {
380326 throw new RangeError ( 'Code One mode must be auto, ascii, c40, text, edi, decimal or byte.' )
381327 }
382- const versions = requested === 'A-H'
383- ? VERSION_MAIN
384- : [ VERSION_MAIN . find ( version => version . name === requested ) ]
328+ const versions = requested === automaticName
329+ ? allVersions
330+ : [ allVersions . find ( version => version . name === requested ) ]
385331 let shortest = Infinity
386332 for ( const version of versions ) {
387333 const remainingCapacity = version . dataWords - prefix . length
@@ -390,21 +336,22 @@ function chooseMainEncoding(data, requested, requestedMode, prefix = [], gs1 = f
390336 for ( const candidate of candidates ) shortest = Math . min ( shortest , candidate . codewords . length )
391337 const fitting = candidates . filter ( candidate => candidate . codewords . length <= version . dataWords )
392338 if ( fitting . length ) {
393- fitting . sort ( ( left , right ) => left . codewords . length - right . codewords . length )
339+ const priority = { mixed : 0 , decimal : 1 , c40 : 2 , text : 3 , edi : 4 , byte : 5 , ascii : 6 }
340+ fitting . sort ( ( left , right ) => left . codewords . length - right . codewords . length || priority [ left . mode ] - priority [ right . mode ] )
394341 return { version, encoded : fitting [ 0 ] . codewords , mode : fitting [ 0 ] . mode }
395342 }
396343 }
397- const name = requested === 'A-H' ? ' Code One H' : `Code One ${ requested } `
344+ const name = requested === automaticName ? ` Code One ${ versions . at ( - 1 ) . name } ` : `Code One ${ requested } `
398345 const capacity = versions . at ( - 1 ) . dataWords
399346 throw new RangeError ( `${ name } accepts at most ${ capacity } data codewords; input requires ${ shortest } .` )
400347}
401348
402349function createMainCandidates ( data , capacity , mode , gs1 ) {
403350 const encoders = {
404351 ascii : ( ) => encodeAscii ( data , gs1 ) ,
405- c40 : ( ) => encodeTripletMode ( data , capacity , 'c40' ) ,
406- text : ( ) => encodeTripletMode ( data , capacity , 'text' ) ,
407- edi : ( ) => encodeTripletMode ( data , capacity , 'edi' ) ,
352+ c40 : ( ) => encodeTripletMode ( data , capacity , 'c40' , gs1 ) ,
353+ text : ( ) => encodeTripletMode ( data , capacity , 'text' , gs1 ) ,
354+ edi : ( ) => encodeTripletMode ( data , capacity , 'edi' , gs1 ) ,
408355 decimal : ( ) => encodeMainDecimal ( data , capacity ) ,
409356 byte : ( ) => encodeByte ( data , capacity ) ,
410357 }
@@ -415,58 +362,153 @@ function createMainCandidates(data, capacity, mode, gs1) {
415362 if ( mode !== 'auto' ) throw error
416363 }
417364 }
365+ if ( mode === 'auto' ) {
366+ try { candidates . push ( { mode : 'mixed' , codewords : encodeMixed ( data , gs1 ) } ) } catch { }
367+ }
418368 return candidates
419369}
420370
421- function encodeTripletMode ( data , capacity , mode ) {
371+ function encodeMixed ( data , gs1 ) {
372+ const codewords = [ ]
373+ let position = 0
374+ let compacted = false
375+ while ( position < data . length ) {
376+ const rest = data . slice ( position )
377+ const decimal = rest . match ( / ^ \d { 12 , } / ) ?. [ 0 ]
378+ if ( decimal ) {
379+ codewords . push ( ...encodeMainDecimal ( decimal , Infinity ) )
380+ position += decimal . length
381+ compacted = true
382+ continue
383+ }
384+
385+ const byteRun = rest . match ( / ^ [ \x80 - \xff ] { 2 , } / ) ?. [ 0 ]
386+ if ( byteRun ) {
387+ codewords . push ( ...encodeByte ( byteRun , Infinity ) )
388+ position += byteRun . length
389+ compacted = true
390+ continue
391+ }
392+
393+ const runs = [
394+ [ 'c40' , rest . match ( / ^ [ A - Z 0 - 9 ] { 6 , } / ) ?. [ 0 ] ] ,
395+ [ 'text' , rest . match ( / ^ [ a - z 0 - 9 ] { 6 , } / ) ?. [ 0 ] ] ,
396+ [ 'edi' , rest . match ( / ^ [ \r * > A - Z 0 - 9 ] { 6 , } / ) ?. [ 0 ] ] ,
397+ ] . map ( ( [ tripletMode , run ] ) => {
398+ const decimalStart = run ?. search ( / \d { 12 , } / ) ?? - 1
399+ return [ tripletMode , decimalStart > 0 ? run . slice ( 0 , decimalStart ) : run ]
400+ } )
401+ const selected = runs . find ( ( [ , run ] ) => run )
402+ if ( selected ) {
403+ const [ tripletMode , run ] = selected
404+ const length = run . length - run . length % 3
405+ if ( length >= 3 ) {
406+ codewords . push ( ...encodeClosedTripletSegment ( run . slice ( 0 , length ) , tripletMode ) )
407+ position += length
408+ compacted = true
409+ continue
410+ }
411+ }
412+
413+ if ( position + 1 < data . length && / \d / . test ( data [ position ] ) && / \d / . test ( data [ position + 1 ] ) ) {
414+ codewords . push ( Number ( data . slice ( position , position + 2 ) ) + 130 )
415+ position += 2
416+ } else {
417+ codewords . push ( ...encodeAscii ( data [ position ] , gs1 ) )
418+ position ++
419+ }
420+ }
421+ if ( ! compacted ) throw new RangeError ( 'No compact Code One segments found.' )
422+ return codewords
423+ }
424+
425+ function encodeClosedTripletSegment ( data , mode ) {
426+ const latch = mode === 'c40' ? 230 : mode === 'text' ? 239 : 238
427+ const values = [ ...data ] . flatMap ( character => mode === 'edi'
428+ ? [ ediValue ( character , false ) ]
429+ : c40TextValues ( character , mode , false ) )
430+ if ( values . length % 3 !== 0 ) throw new Error ( 'Internal Code One triplet alignment error.' )
431+ const codewords = [ latch ]
432+ for ( let index = 0 ; index < values . length ; index += 3 ) {
433+ appendTriplet ( codewords , values [ index ] , values [ index + 1 ] , values [ index + 2 ] )
434+ }
435+ codewords . push ( 255 )
436+ return codewords
437+ }
438+
439+ function encodeTripletMode ( data , capacity , mode , gs1 = false ) {
422440 const definitions = {
423- c40 : { latch : 230 , pattern : / ^ [ A - Z 0 - 9 ] + $ / , value : c40Value } ,
424- text : { latch : 239 , pattern : / ^ [ a - z 0 - 9 ] + $ / , value : textValue } ,
425- edi : { latch : 238 , pattern : / ^ [ \r * > 0 - 9 A - Z ] + $ / , value : ediValue } ,
441+ c40 : { latch : 230 , values : character => c40TextValues ( character , 'c40' , gs1 ) } ,
442+ text : { latch : 239 , values : character => c40TextValues ( character , 'text' , gs1 ) } ,
443+ edi : { latch : 238 , values : character => [ ediValue ( character , gs1 ) ] } ,
426444 }
427445 const definition = definitions [ mode ]
428- if ( ! definition . pattern . test ( data ) ) throw new RangeError ( `Code One ${ mode . toUpperCase ( ) } mode cannot encode this payload without shifts.` )
429- const values = [ ...data ] . map ( definition . value )
430- const codewords = [ definition . latch ]
431- let position = 0
432- while ( position + 2 < values . length ) {
433- const packed = 1600 * values [ position ] + 40 * values [ position + 1 ] + values [ position + 2 ] + 1
434- codewords . push ( packed >> 8 , packed & 255 )
435- position += 3
436- }
437- const remaining = values . length - position
438- const room = capacity - codewords . length
439- if ( remaining === 1 && room === 1 ) {
440- codewords . push ( data . charCodeAt ( position ) + 1 )
446+ const characters = [ ...data ]
447+ const groups = characters . map ( definition . values )
448+ const values = groups . flat ( )
449+ const packedCodewords = [ definition . latch ]
450+ let valuePosition = 0
451+ while ( valuePosition + 2 < values . length ) {
452+ appendTriplet ( packedCodewords , values [ valuePosition ] , values [ valuePosition + 1 ] , values [ valuePosition + 2 ] )
453+ valuePosition += 3
454+ }
455+ const remaining = values . length - valuePosition
456+ const room = capacity - packedCodewords . length
457+ if ( remaining === 0 ) {
458+ if ( packedCodewords . length < capacity ) packedCodewords . push ( 255 )
459+ return packedCodewords
460+ }
461+ if ( remaining === 1 && room === 1 && groups . at ( - 1 ) . length === 1 ) {
462+ packedCodewords . push ( ...encodeAscii ( characters . at ( - 1 ) , gs1 ) )
463+ return packedCodewords
441464 } else if ( remaining === 2 && room === 2 ) {
442- const packed = 1600 * values [ position ] + 40 * values [ position + 1 ] + 1
443- codewords . push ( packed >> 8 , packed & 255 )
444- } else {
445- if ( codewords . length < capacity ) codewords . push ( 255 )
446- codewords . push ( ...encodeAscii ( data . slice ( position ) ) )
465+ appendTriplet ( packedCodewords , values [ valuePosition ] , values [ valuePosition + 1 ] , 0 )
466+ return packedCodewords
467+ }
468+
469+ let characterPosition = groups . length
470+ let prefixValueCount = values . length
471+ while ( characterPosition > 0 && prefixValueCount % 3 !== 0 ) prefixValueCount -= groups [ -- characterPosition ] . length
472+ const codewords = [ definition . latch ]
473+ for ( let index = 0 ; index < prefixValueCount ; index += 3 ) {
474+ appendTriplet ( codewords , values [ index ] , values [ index + 1 ] , values [ index + 2 ] )
447475 }
476+ codewords . push ( 255 , ...encodeAscii ( characters . slice ( characterPosition ) . join ( '' ) , gs1 ) )
448477 return codewords
449478}
450479
451- function c40Value ( character ) {
452- if ( character === ' ' ) return 3
453- if ( / \d / . test ( character ) ) return character . charCodeAt ( 0 ) - 44
454- return character . charCodeAt ( 0 ) - 51
480+ function appendTriplet ( codewords , first , second , third ) {
481+ const packed = 1600 * first + 40 * second + third + 1
482+ codewords . push ( packed >> 8 , packed & 255 )
455483}
456484
457- function textValue ( character ) {
458- if ( character === ' ' ) return 3
459- if ( / \d / . test ( character ) ) return character . charCodeAt ( 0 ) - 44
460- return character . charCodeAt ( 0 ) - 83
485+ function c40TextValues ( character , mode , gs1 ) {
486+ const value = character . charCodeAt ( 0 )
487+ if ( gs1 && value === 29 ) return [ 1 , 27 ]
488+ if ( value >= 128 ) return [ 1 , 30 , ...c40TextValues ( String . fromCharCode ( value - 128 ) , mode , false ) ]
489+ if ( value === 32 ) return [ 3 ]
490+ if ( value >= 48 && value <= 57 ) return [ value - 44 ]
491+ if ( mode === 'c40' && value >= 65 && value <= 90 ) return [ value - 51 ]
492+ if ( mode === 'text' && value >= 97 && value <= 122 ) return [ value - 83 ]
493+ if ( value <= 31 ) return [ 0 , value ]
494+ if ( value >= 33 && value <= 47 ) return [ 1 , value - 33 ]
495+ if ( value >= 58 && value <= 64 ) return [ 1 , value - 43 ]
496+ if ( value >= 91 && value <= 95 ) return [ 1 , value - 69 ]
497+ if ( mode === 'c40' ) return [ 2 , value - 96 ]
498+ if ( value === 96 ) return [ 2 , 0 ]
499+ if ( value >= 65 && value <= 90 ) return [ 2 , value - 64 ]
500+ return [ 2 , value - 96 ]
461501}
462502
463- function ediValue ( character ) {
503+ function ediValue ( character , gs1 ) {
504+ if ( gs1 && character . charCodeAt ( 0 ) === 29 ) throw new RangeError ( 'Code One EDI mode cannot encode an inline GS1 separator.' )
464505 if ( character === '\r' ) return 0
465506 if ( character === '*' ) return 1
466507 if ( character === '>' ) return 2
467508 if ( character === ' ' ) return 3
468509 if ( / \d / . test ( character ) ) return character . charCodeAt ( 0 ) - 44
469- return character . charCodeAt ( 0 ) - 51
510+ if ( / ^ [ A - Z ] $ / . test ( character ) ) return character . charCodeAt ( 0 ) - 51
511+ throw new RangeError ( 'Code One EDI mode accepts CR, space, *, >, digits and uppercase letters.' )
470512}
471513
472514function encodeByte ( data , capacity ) {
0 commit comments