Skip to content

Commit 48aff64

Browse files
committed
feat(code-one): complete high-level encoding
1 parent c093b1b commit 48aff64

4 files changed

Lines changed: 226 additions & 113 deletions

File tree

docs/formats/code-one.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
The first reviewed slice implements the numeric Version S family: S-10 (up to 6 digits), S-20 (up to 12), and S-30 (up to 18). It includes base-32 data conversion, GF(32) Reed-Solomon error correction, recognition patterns, exact matrix placement, automatic/forced subversion selection, and a separate SVG renderer.
66

7-
The second slice adds the 16-row Version T family (T-16, T-32 and T-48), GF(256) Reed-Solomon error correction, exact recognition patterns, automatic or forced T sizing, Latin-1 ASCII/digit-pair encoding, compact C40 symbols at exact capacity, and the full 90-digit T-48 Decimal payload. Other payloads always use valid ASCII codewords instead of an external encoder.
7+
The second slice adds the 16-row Version T family (T-16, T-32 and T-48), GF(256) Reed-Solomon error correction, exact recognition patterns, automatic or forced T sizing, and the same high-level and control paths as the general family. Its complete 90-digit Decimal payload is checked against the official T-48 matrix.
88

9-
The general family now covers Versions A through H, including all fixed geometries, interleaved GF(256) error-correction blocks, and automatic or forced sizing. Its high-level encoder supports ASCII, the basic C40 and Text character sets, EDI, Decimal, and Byte as explicit whole-payload modes. Automatic mode compares every compatible whole-payload encoding and uses the shortest one that fits. Shifted C40/Text characters and mixed mode switching inside one payload are the remaining optimizations; ASCII fallback keeps supported Latin-1 payloads valid in the meantime.
9+
The general family covers Versions A through H, including all fixed geometries, interleaved GF(256) error-correction blocks, and automatic or forced sizing. Its high-level encoder supports ASCII, C40, Text, EDI, Decimal, and Byte, including C40/Text Shift 1–3, Upper Shift, and GS1 FNC1. Automatic mode compares compatible whole-payload encodings with mixed compact runs and uses the shortest result that fits. When equally short encodings exist, its choice may differ from another conforming encoder without changing the decoded payload.
1010

1111
```js
1212
import { CodeOneCore } from '../../libs/CodeOneCore.js'
@@ -31,4 +31,4 @@ const unicode = new CodeOneCore('Grüße', {
3131

3232
For GS1, pass the normalized element string and ASCII GS (`\x1D`) separators with `gs1: true`; parsing bracketed or parenthesized application identifiers belongs to a separate GS1 parser. Structured Append accepts `{ index, count }` with a count from 2 through 128 and cannot be combined with GS1. UTF-8 requires an explicit ECI assignment.
3333

34-
The remaining work is optimal mixed-mode segmentation and sharing the new control/high-level layer with Version T. Versions S deliberately reject GS1, ECI, and Structured Append, as required by their restricted numeric design. No production path loads Zint, BWIPP, or another barcode encoder.
34+
Versions S deliberately reject GS1, ECI, and Structured Append, as required by their restricted numeric design. Version T and Versions A–H support GS1, ECI, Structured Append, every high-level mode, shifted Latin-1 characters, and mixed compact runs. No production path loads Zint, BWIPP, or another barcode encoder.

libs/CodeOneCore.js

Lines changed: 150 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -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-Z0-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-
222206
function 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-
280222
function 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

377319
function 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

402349
function 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-Z0-9]{6,}/)?.[0]],
395+
['text', rest.match(/^[ a-z0-9]{6,}/)?.[0]],
396+
['edi', rest.match(/^[\r*> A-Z0-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-Z0-9]+$/, value: c40Value },
424-
text: { latch: 239, pattern: /^[ a-z0-9]+$/, value: textValue },
425-
edi: { latch: 238, pattern: /^[\r*> 0-9A-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

472514
function encodeByte(data, capacity) {

0 commit comments

Comments
 (0)