use std::{any::TypeId, fmt::Display, str::FromStr};
use crate::{
common::{CharacterSet, ECIInput, Eci, MinimalECIInput, Result},
Exceptions,
};
use super::Compaction;
const TEXT_COMPACTION: u32 = 0;
const BYTE_COMPACTION: u32 = 1;
const NUMERIC_COMPACTION: u32 = 2;
const SUBMODE_ALPHA: u32 = 0;
const SUBMODE_LOWER: u32 = 1;
const SUBMODE_MIXED: u32 = 2;
const SUBMODE_PUNCTUATION: u32 = 3;
const LATCH_TO_TEXT: u32 = 900;
const LATCH_TO_BYTE_PADDED: u32 = 901;
const LATCH_TO_NUMERIC: u32 = 902;
const SHIFT_TO_BYTE: u32 = 913;
const LATCH_TO_BYTE: u32 = 924;
const ECI_USER_DEFINED: u32 = 925;
const ECI_GENERAL_PURPOSE: u32 = 926;
const ECI_CHARSET: u32 = 927;
const TEXT_MIXED_RAW: [u8; 30] = [
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 38, 13, 9, 44, 58, 35, 45, 46, 36, 47, 43, 37, 42, 61,
94, 0, 32, 0, 0, 0,
];
const TEXT_PUNCTUATION_RAW: [u8; 30] = [
59, 60, 62, 64, 91, 92, 93, 95, 96, 126, 33, 13, 9, 44, 58, 10, 45, 46, 36, 47, 34, 124, 42,
40, 41, 63, 123, 125, 39, 0,
];
const DEFAULT_ENCODING: CharacterSet = CharacterSet::ISO8859_1;
const MIXED: [i8; 128] = {
let mut mixed = [-1_i8; 128];
let mut i = 0;
while i < TEXT_MIXED_RAW.len() {
let b = TEXT_MIXED_RAW[i] as usize;
if b > 0 {
mixed[b] = i as i8;
}
i += 1;
}
mixed
};
const PUNCTUATION: [i8; 128] = {
let mut punct = [-1_i8; 128];
let mut i = 0;
while i < TEXT_PUNCTUATION_RAW.len() {
let b = TEXT_PUNCTUATION_RAW[i] as usize;
if b > 0 {
punct[b] = i as i8;
}
i += 1;
}
punct
};
pub fn encodeHighLevel(
msg: &str,
compaction: Compaction,
encoding: Option<CharacterSet>,
autoECI: bool,
) -> Result<String> {
let mut encoding = encoding;
if msg.is_empty() {
return Err(Exceptions::writer_with("Empty message not allowed"));
}
if encoding.is_none() && !autoECI {
for ch in msg.chars() {
if ch as u32 > 255 {
return Err(Exceptions::writer_with(format!("Non-encodable character detected: {} (Unicode: {}). Consider specifying EncodeHintType.PDF417_AUTO_ECI and/or EncodeTypeHint.CHARACTER_SET.", ch as u32, ch)));
}
}
}
let mut sb = String::with_capacity(msg.len());
let input: Box<dyn ECIInput>;
if autoECI {
input = Box::new(MinimalECIInput::new(msg, encoding, None));
} else {
input = Box::new(NoECIInput::new(msg.to_owned()));
if encoding.is_none() {
encoding = Some(DEFAULT_ENCODING);
} else if &DEFAULT_ENCODING != encoding.as_ref().ok_or(Exceptions::ILLEGAL_STATE)? {
encodingECI(
Eci::from(encoding.ok_or(Exceptions::ILLEGAL_STATE)?),
&mut sb,
)?;
}
}
let len = input.length();
let mut p = 0;
let mut textSubMode = SUBMODE_ALPHA;
match compaction {
Compaction::TEXT => {
encodeText(input.as_ref(), p, len as u32, &mut sb, textSubMode)?;
}
Compaction::BYTE if autoECI => encodeMultiECIBinary(
input.as_ref(),
0,
input.length() as u32,
TEXT_COMPACTION,
&mut sb,
)?,
Compaction::BYTE => {
let msgBytes = encoding
.as_ref()
.ok_or(Exceptions::ILLEGAL_STATE)?
.encode(&input.to_string())
.unwrap_or_default(); encodeBinary(
&msgBytes,
p,
msgBytes.len() as u32,
BYTE_COMPACTION,
&mut sb,
)?;
}
Compaction::NUMERIC => {
sb.push(char::from_u32(LATCH_TO_NUMERIC).ok_or(Exceptions::PARSE)?);
encodeNumeric(input.as_ref(), p, len as u32, &mut sb)?;
}
_ => {
let mut encodingMode = TEXT_COMPACTION; while p < len as u32 {
while p < len as u32 && input.isECI(p)? {
encodingECI(input.getECIValue(p as usize)?, &mut sb)?;
p += 1;
}
if p >= len as u32 {
break;
}
let n = determineConsecutiveDigitCount(input.as_ref(), p)?;
if n >= 13 {
sb.push(char::from_u32(LATCH_TO_NUMERIC).ok_or(Exceptions::PARSE)?);
encodingMode = NUMERIC_COMPACTION;
textSubMode = SUBMODE_ALPHA; encodeNumeric(input.as_ref(), p, n, &mut sb)?;
p += n;
} else {
let t = determineConsecutiveTextCount(input.as_ref(), p)?;
if t >= 5 || n == len as u32 {
if encodingMode != TEXT_COMPACTION {
sb.push(char::from_u32(LATCH_TO_TEXT).ok_or(Exceptions::PARSE)?);
encodingMode = TEXT_COMPACTION;
textSubMode = SUBMODE_ALPHA; }
textSubMode = encodeText(input.as_ref(), p, t, &mut sb, textSubMode)?;
p += t;
} else {
let mut b = determineConsecutiveBinaryCount(
input.as_ref(),
p,
if autoECI { None } else { encoding },
)?;
if b == 0 {
b = 1;
}
let bytes = if autoECI {
None
} else {
let str = input
.subSequence(p as usize, (p + b) as usize)?
.iter()
.collect::<String>();
if let Ok(enc_str) = encoding
.as_ref()
.ok_or(Exceptions::ILLEGAL_STATE)?
.encode(&str)
{
Some(enc_str)
} else {
None
}
};
let bytes_ok = bytes.is_some();
if (bytes_ok && b == 1) && (encodingMode == TEXT_COMPACTION) {
if autoECI {
encodeMultiECIBinary(
input.as_ref(),
p,
1,
TEXT_COMPACTION,
&mut sb,
)?;
} else {
encodeBinary(
bytes.as_ref().ok_or(Exceptions::ILLEGAL_STATE)?,
0,
1,
TEXT_COMPACTION,
&mut sb,
)?;
}
} else {
if autoECI {
encodeMultiECIBinary(
input.as_ref(),
p,
p + b,
encodingMode,
&mut sb,
)?;
} else {
encodeBinary(
bytes.as_ref().ok_or(Exceptions::ILLEGAL_STATE)?,
0,
bytes.as_ref().ok_or(Exceptions::ILLEGAL_STATE)?.len() as u32,
encodingMode,
&mut sb,
)?;
}
encodingMode = BYTE_COMPACTION;
textSubMode = SUBMODE_ALPHA; }
p += b;
}
}
}
}
}
Ok(sb)
}
fn encodeText<T: ECIInput + ?Sized>(
input: &T,
startpos: u32,
count: u32,
sb: &mut String,
initialSubmode: u32,
) -> Result<u32> {
let mut tmp = String::with_capacity(count as usize);
let mut submode = initialSubmode;
let mut idx = 0;
loop {
if input.isECI(startpos + idx)? {
encodingECI(input.getECIValue((startpos + idx) as usize)?, sb)?;
idx += 1;
} else {
let ch = input.charAt((startpos + idx) as usize)?;
match submode {
SUBMODE_ALPHA => {
if isAlphaUpper(ch) {
if ch == ' ' {
tmp.push(26 as char); } else {
tmp.push(char::from_u32(ch as u32 - 65).ok_or(Exceptions::PARSE)?);
}
} else if isAlphaLower(ch) {
submode = SUBMODE_LOWER;
tmp.push(27 as char); continue;
} else if isMixed(ch) {
submode = SUBMODE_MIXED;
tmp.push(28 as char); continue;
} else {
tmp.push(29 as char); tmp.push(
char::from_u32(PUNCTUATION[ch as usize] as u32)
.ok_or(Exceptions::PARSE)?,
);
}
}
SUBMODE_LOWER => {
if isAlphaLower(ch) {
if ch == ' ' {
tmp.push(26 as char); } else {
tmp.push(char::from_u32(ch as u32 - 97).ok_or(Exceptions::PARSE)?);
}
} else if isAlphaUpper(ch) {
tmp.push(27 as char); tmp.push(char::from_u32(ch as u32 - 65).ok_or(Exceptions::PARSE)?);
} else if isMixed(ch) {
submode = SUBMODE_MIXED;
tmp.push(28 as char); continue;
} else {
tmp.push(29 as char); tmp.push(
char::from_u32(PUNCTUATION[ch as usize] as u32)
.ok_or(Exceptions::PARSE)?,
);
}
}
SUBMODE_MIXED => {
if isMixed(ch) {
tmp.push(
char::from_u32(MIXED[ch as usize] as u32).ok_or(Exceptions::PARSE)?,
);
} else if isAlphaUpper(ch) {
submode = SUBMODE_ALPHA;
tmp.push(28 as char); continue;
} else if isAlphaLower(ch) {
submode = SUBMODE_LOWER;
tmp.push(27 as char); continue;
} else {
if startpos + idx + 1 < count
&& !input.isECI(startpos + idx + 1)?
&& isPunctuation(input.charAt((startpos + idx + 1) as usize)?)
{
submode = SUBMODE_PUNCTUATION;
tmp.push(25 as char); continue;
}
tmp.push(29 as char); tmp.push(
char::from_u32(PUNCTUATION[ch as usize] as u32)
.ok_or(Exceptions::PARSE)?,
);
}
}
_ =>
{
if isPunctuation(ch) {
tmp.push(
char::from_u32(PUNCTUATION[ch as usize] as u32)
.ok_or(Exceptions::PARSE)?,
);
} else {
submode = SUBMODE_ALPHA;
tmp.push(29 as char); continue;
}
}
}
idx += 1;
if idx >= count {
break;
}
}
}
let mut h = 0 as char;
let len = tmp.chars().count();
for i in 0..len {
let odd = (i % 2) != 0;
if odd {
h = char::from_u32(
(h as u32 * 30) + tmp.chars().nth(i).ok_or(Exceptions::INDEX_OUT_OF_BOUNDS)? as u32,
)
.ok_or(Exceptions::PARSE)?;
sb.push(h);
} else {
h = tmp.chars().nth(i).ok_or(Exceptions::INDEX_OUT_OF_BOUNDS)?;
}
}
if (len % 2) != 0 {
sb.push(char::from_u32((h as u32 * 30) + 29).ok_or(Exceptions::PARSE)?);
}
Ok(submode)
}
fn encodeMultiECIBinary<T: ECIInput + ?Sized>(
input: &T,
startpos: u32,
count: u32,
startmode: u32,
sb: &mut String,
) -> Result<()> {
let end = (startpos + count).min(input.length() as u32);
let mut localStart = startpos;
loop {
while localStart < end && input.isECI(localStart)? {
encodingECI(input.getECIValue(localStart as usize)?, sb)?;
localStart += 1;
}
let mut localEnd = localStart;
while localEnd < end && !input.isECI(localEnd)? {
localEnd += 1;
}
let localCount = localEnd as i32 - localStart as i32;
if localCount <= 0 {
break;
} else {
encodeBinary(
&subBytes(input, localStart, localEnd)?,
0,
localCount as u32,
if localStart == startpos {
startmode
} else {
BYTE_COMPACTION
},
sb,
)?;
localStart = localEnd;
}
}
Ok(())
}
pub fn subBytes<T: ECIInput + ?Sized>(input: &T, start: u32, end: u32) -> Result<Vec<u8>> {
let count = (end - start) as usize;
let mut result = vec![0_u8; count];
for i in start as usize..end as usize {
result[i - start as usize] = input.charAt(i)? as u8;
}
Ok(result)
}
fn encodeBinary(
bytes: &[u8],
startpos: u32,
count: u32,
startmode: u32,
sb: &mut String,
) -> Result<()> {
if count == 1 && startmode == TEXT_COMPACTION {
sb.push(char::from_u32(SHIFT_TO_BYTE).ok_or(Exceptions::PARSE)?);
} else if (count % 6) == 0 {
sb.push(char::from_u32(LATCH_TO_BYTE).ok_or(Exceptions::PARSE)?);
} else {
sb.push(char::from_u32(LATCH_TO_BYTE_PADDED).ok_or(Exceptions::PARSE)?);
}
let mut idx = startpos;
if count >= 6 {
let mut chars = [0 as char; 5]; while (startpos + count - idx) >= 6 {
let mut t: i64 = 0;
for i in 0..6 {
t <<= 8;
t += bytes[idx as usize + i as usize] as i64;
}
for ch in &mut chars {
*ch = char::from_u32((t % 900) as u32).ok_or(Exceptions::PARSE)?;
t /= 900;
}
sb.push_str(&chars.into_iter().rev().collect::<String>());
idx += 6;
}
}
for i in idx..(startpos + count) {
let ch = bytes[i as usize];
sb.push(ch as char);
}
Ok(())
}
fn encodeNumeric<T: ECIInput + ?Sized>(
input: &T,
startpos: u32,
count: u32,
sb: &mut String,
) -> Result<()> {
let mut idx = 0;
let mut tmp = String::with_capacity(count as usize / 3 + 1);
let NUM900: num::BigUint = num::BigUint::from(900_u16); let NUM0: num::BigUint = num::BigUint::from(0_u8);
while idx < count {
tmp.clear();
let len = 44.min(count as isize - idx as isize);
let part = format!(
"1{}",
input
.subSequence(
(startpos + idx) as usize,
(startpos + idx + len as u32) as usize
)?
.iter()
.collect::<String>()
);
let mut bigint = num::BigUint::from_str(&part)
.map_err(|e| Exceptions::parse_with(format!("issue parsing {part}: {e}")))?; loop {
tmp.push(
char::from_u32((&bigint % &NUM900).try_into().map_err(|e| {
Exceptions::parse_with(format!("erorr converting {bigint} to u32: {e}"))
})?)
.ok_or(Exceptions::PARSE)?,
);
bigint /= &NUM900;
if bigint == NUM0 {
break;
}
}
sb.push_str(&tmp.chars().rev().collect::<String>());
idx += len as u32;
}
Ok(())
}
fn isDigit(ch: char) -> bool {
ch.is_ascii_digit()
}
fn isAlphaUpper(ch: char) -> bool {
ch == ' ' || ch.is_ascii_uppercase()
}
fn isAlphaLower(ch: char) -> bool {
ch == ' ' || ch.is_ascii_lowercase()
}
fn isMixed(ch: char) -> bool {
MIXED[ch as usize] != -1
}
fn isPunctuation(ch: char) -> bool {
PUNCTUATION[ch as usize] != -1
}
fn isText(ch: char) -> bool {
ch == '\t' || ch == '\n' || ch == '\r' || (ch as u32 >= 32 && ch as u32 <= 126)
}
fn determineConsecutiveDigitCount<T: ECIInput + ?Sized>(input: &T, startpos: u32) -> Result<u32> {
let mut count = 0;
let len = input.length();
let mut idx = startpos as usize;
if idx < len {
while idx < len && !input.isECI(idx as u32)? && isDigit(input.charAt(idx)?) {
count += 1;
idx += 1;
}
}
Ok(count)
}
fn determineConsecutiveTextCount<T: ECIInput + ?Sized>(input: &T, startpos: u32) -> Result<u32> {
let len = input.length();
let mut idx = startpos as usize;
while idx < len {
let mut numericCount = 0;
while numericCount < 13
&& idx < len
&& !input.isECI(idx as u32)?
&& isDigit(input.charAt(idx)?)
{
numericCount += 1;
idx += 1;
}
if numericCount >= 13 {
return Ok((idx - startpos as usize - numericCount) as u32);
}
if numericCount > 0 {
continue;
}
if input.isECI(idx as u32)? || !isText(input.charAt(idx)?) {
break;
}
idx += 1;
}
Ok((idx - startpos as usize) as u32)
}
fn determineConsecutiveBinaryCount<T: ECIInput + ?Sized + 'static>(
input: &T,
startpos: u32,
encoding: Option<CharacterSet>,
) -> Result<u32> {
let len = input.length();
let mut idx = startpos as usize;
while idx < len {
let mut numericCount = 0;
let mut i = idx;
while numericCount < 13 && !input.isECI(i as u32)? && isDigit(input.charAt(i)?) {
numericCount += 1;
i = idx + numericCount;
if i >= len {
break;
}
}
if numericCount >= 13 {
return Ok(idx as u32 - startpos);
}
if let Some(encoder) = encoding {
let can_encode = encoder.encode(&input.charAt(idx)?.to_string()).is_ok();
if !can_encode {
if TypeId::of::<T>() != TypeId::of::<NoECIInput>() {
return Err(Exceptions::illegal_state_with("expected NoECIInput type"));
}
let ch = input.charAt(idx)?;
return Err(Exceptions::writer_with(format!(
"Non-encodable character detected: {} (Unicode: {})",
ch, ch as u32
)));
}
}
idx += 1;
}
Ok(idx as u32 - startpos)
}
fn encodingECI(eci: Eci, sb: &mut String) -> Result<()> {
if (0..900).contains(&(eci as i32)) {
sb.push(char::from_u32(ECI_CHARSET).ok_or(Exceptions::PARSE)?);
sb.push(char::from_u32(eci as u32).ok_or(Exceptions::PARSE)?);
} else if (eci as i32) < 810900 {
sb.push(char::from_u32(ECI_GENERAL_PURPOSE).ok_or(Exceptions::PARSE)?);
sb.push(char::from_u32(((eci as i32) / 900 - 1) as u32).ok_or(Exceptions::PARSE)?);
sb.push(char::from_u32(((eci as i32) % 900) as u32).ok_or(Exceptions::PARSE)?);
} else if (eci as i32) < 811800 {
sb.push(char::from_u32(ECI_USER_DEFINED).ok_or(Exceptions::PARSE)?);
sb.push(char::from_u32((810900 - (eci as i32)) as u32).ok_or(Exceptions::PARSE)?);
} else {
return Err(Exceptions::writer_with(format!(
"ECI number not in valid range from 0..811799, but was {eci}"
)));
}
Ok(())
}
struct NoECIInput(String);
impl ECIInput for NoECIInput {
fn length(&self) -> usize {
self.0.chars().count()
}
fn charAt(&self, index: usize) -> Result<char> {
self.0
.chars()
.nth(index)
.ok_or(Exceptions::INDEX_OUT_OF_BOUNDS)
}
fn subSequence(&self, start: usize, end: usize) -> Result<Vec<char>> {
let res: Vec<char> = self.0.chars().skip(start).take(end - start).collect();
Ok(res)
}
fn isECI(&self, _index: u32) -> Result<bool> {
Ok(false)
}
fn getECIValue(&self, _index: usize) -> Result<Eci> {
Ok(Eci::Unknown)
}
fn haveNCharacters(&self, index: usize, n: usize) -> Result<bool> {
Ok(index + n <= self.0.len())
}
}
impl NoECIInput {
pub fn new(input: String) -> Self {
Self(input)
}
}
impl Display for NoECIInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod PDF417EncoderTestCase {
use crate::{
common::CharacterSet,
pdf417::encoder::{pdf_417_high_level_encoder::encodeHighLevel, Compaction},
};
#[test]
fn testEncodeAuto() {
let encoded = encodeHighLevel("ABCD", Compaction::AUTO, Some(CharacterSet::UTF8), false)
.expect("encode");
assert_eq!("\u{039f}\u{001A}\u{0385}ABCD", encoded);
}
#[test]
fn testEncodeAutoWithSpecialChars() {
encodeHighLevel(
"1%§s ?aG$",
Compaction::AUTO,
Some(CharacterSet::UTF8),
false,
)
.expect("encode");
}
#[test]
fn testEncodeIso88591WithSpecialChars() {
encodeHighLevel(
"asdfg§asd",
Compaction::AUTO,
Some(CharacterSet::ISO8859_1),
false,
)
.expect("encode");
}
#[test]
fn testEncodeText() {
let encoded = encodeHighLevel("ABCD", Compaction::TEXT, Some(CharacterSet::UTF8), false)
.expect("encode");
assert_eq!("Ο\u{001A}\u{0001}?", encoded);
}
#[test]
fn testEncodeNumeric() {
let encoded = encodeHighLevel("1234", Compaction::NUMERIC, Some(CharacterSet::UTF8), false)
.expect("encode");
assert_eq!("\u{039f}\u{001A}\u{0386}\u{C}\u{01b2}", encoded);
}
#[test]
fn testEncodeByte() {
let encoded = encodeHighLevel("abcd", Compaction::BYTE, Some(CharacterSet::UTF8), false)
.expect("encode");
assert_eq!("\u{039f}\u{001A}\u{0385}abcd", encoded);
}
#[test]
#[should_panic]
fn testEncodeEmptyString() {
encodeHighLevel("", Compaction::AUTO, None, false).expect("encode");
}
}