// Text Compaction (ISO/IEC 15438 4.4.2).
//
// Characters are mapped to values 0..29 in one of four sub-modes, and two
// consecutive values are packed into a single codeword as
// `first * 30 + second`. A trailing odd value is padded with 29, the
// punctuation shift code that the standard designates as the text pad.
///|
/// `true` if `code` is a capital letter or a space, i.e. the Alpha sub-mode.
pub fn is_alpha_upper(code : Int) -> Bool {
code == 32 || (code >= 65 && code <= 90)
}
///|
/// `true` if `code` is a lower case letter or a space, i.e. the Lower
/// sub-mode.
pub fn is_alpha_lower(code : Int) -> Bool {
code == 32 || (code >= 97 && code <= 122)
}
///|
/// `true` if the Mixed sub-mode can represent `code`.
pub fn is_mixed(code : Int) -> Bool {
code >= 0 && code < 256 && mixed_value[code] >= 0
}
///|
/// `true` if the Punctuation sub-mode can represent `code`.
pub fn is_punctuation(code : Int) -> Bool {
code >= 0 && code < 256 && punctuation_value[code] >= 0
}
///|
/// `true` if Text Compaction can represent `code` at all.
pub fn is_text_code(code : Int) -> Bool {
is_alpha_upper(code) ||
is_alpha_lower(code) ||
is_mixed(code) ||
is_punctuation(code)
}
///|
/// `true` if every code unit of `input` is text encodable.
pub fn is_text_input(input : Array[Int]) -> Bool {
for code in input {
if !is_text_code(code) {
return false
}
}
true
}
///|
/// `true` if `code` is an ASCII decimal digit.
pub fn is_digit(code : Int) -> Bool {
code >= 48 && code <= 57
}
///|
/// `true` if every code unit of `input` is a decimal digit.
pub fn is_numeric_input(input : Array[Int]) -> Bool {
for code in input {
if !is_digit(code) {
return false
}
}
true
}
///|
/// Pack a run of Text Compaction values into codewords. Returns `None` when
/// the run is empty: PDF417 has no representation for an empty text segment.
fn pack_text_values(values : Array[Int]) -> Array[Int]? {
if values.is_empty() {
return None
}
let result : Array[Int] = []
let mut index = 0
while index < values.length() {
let first = values[index]
let second = if index + 1 < values.length() {
values[index + 1]
} else {
tc_text_pad
}
result.push(first * 30 + second)
index += 2
}
Some(result)
}
///|
/// Run the Text Compaction sub-mode automaton over `input[start..start+count)`
/// starting in `initial`, and return the resulting values together with the
/// sub-mode the encoder ends in.
pub fn text_automaton(
input : Array[Int],
start : Int,
count : Int,
initial : TextSubmode,
) -> (Array[Int], TextSubmode) raise EncodeError {
let values : Array[Int] = []
let mut submode = initial
let mut index = 0
while index < count {
let code = input[start + index]
match submode {
Alpha =>
if is_alpha_upper(code) {
values.push(if code == 32 { 26 } else { code - 65 })
index += 1
} else if is_alpha_lower(code) {
// 27 shifts to Lower for the *same* character, so the value of the
// current character is emitted here, not on the next iteration.
submode = Lower
values.push(tc_alpha_to_lower)
values.push(if code == 32 { 26 } else { code - 97 })
index += 1
} else if is_mixed(code) {
submode = Mixed
values.push(tc_alpha_to_mixed)
} else if is_punctuation(code) {
values.push(tc_alpha_to_punct)
values.push(punctuation_value[code])
index += 1
} else {
raise UnrepresentableChar(code)
}
Lower =>
if is_alpha_lower(code) {
// In Lower sub-mode 27 shifts to Alpha instead of emitting 26, so a
// space is encoded arithmetically here.
values.push(if code == 32 { 26 } else { code - 97 })
index += 1
} else if is_mixed(code) {
submode = Mixed
values.push(tc_lower_to_mixed)
} else if is_punctuation(code) {
values.push(tc_lower_to_punct)
values.push(punctuation_value[code])
index += 1
} else if is_alpha_upper(code) {
// In Lower sub-mode 27 is the shift to Alpha, so lowercase "a".."z"
// and the space are already covered above; only a genuine shift
// lands here.
submode = Alpha
values.push(tc_lower_to_alpha)
} else {
raise UnrepresentableChar(code)
}
Mixed =>
if is_mixed(code) {
values.push(mixed_value[code])
index += 1
} else if is_alpha_upper(code) {
submode = Alpha
values.push(tc_mixed_to_alpha)
} else if is_alpha_lower(code) {
submode = Lower
values.push(tc_mixed_to_lower)
} else if is_punctuation(code) {
if index + 1 < count && is_punctuation(input[start + index + 1]) {
submode = Punctuation
values.push(tc_mixed_to_punct_latch)
} else {
values.push(tc_mixed_to_punct_shift)
values.push(punctuation_value[code])
index += 1
}
} else {
raise UnrepresentableChar(code)
}
Punctuation =>
if is_punctuation(code) {
values.push(punctuation_value[code])
index += 1
} else {
submode = Alpha
values.push(tc_punct_to_alpha)
}
}
}
(values, submode)
}
///|
/// Encode `input[start..start+count)` in Text Compaction and report both the
/// codewords and the sub-mode the encoder finished in.
pub fn encode_text_segment(
input : Array[Int],
start : Int,
count : Int,
initial : TextSubmode,
) -> (Array[Int], TextSubmode) raise EncodeError {
if count <= 0 {
raise EmptyInput
}
let (values, submode) = text_automaton(input, start, count, initial)
match pack_text_values(values) {
Some(words) => (words, submode)
None => raise EmptyInput
}
}
///|
/// Encode a whole input in Text Compaction, starting in the Alpha sub-mode.
///
/// Every code unit must be representable by one of the four sub-modes;
/// otherwise `UnrepresentableChar` is raised.
pub fn encode_text(input : Array[Int]) -> Array[Int] raise EncodeError {
encode_text_from(input, Alpha)
}
///|
/// Like `encode_text`, but lets the caller choose the sub-mode the encoder
/// starts in. Useful when several text segments are concatenated after a
/// single mode latch.
pub fn encode_text_from(
input : Array[Int],
initial : TextSubmode,
) -> Array[Int] raise EncodeError {
if input.is_empty() {
raise EmptyInput
}
let (words, _) = encode_text_segment(input, 0, input.length(), initial)
words
}
///|
/// Encode text with no error handling: returns `None` instead of raising.
pub fn encode_text_opt(input : Array[Int]) -> Array[Int]? {
try encode_text(input) catch {
_ => None
} noraise {
words => Some(words)
}
}