Add Telepen read / write functionality

This commit is contained in:
Chris Wood
2023-12-31 09:40:35 +00:00
parent c93594057b
commit 75d56b7282
22 changed files with 764 additions and 4 deletions

View File

@@ -32,6 +32,7 @@ resvg = {version = "0.35", optional = true, default-features=false}
serde = { version = "1.0", features = ["derive", "rc"], optional = true }
thiserror = "1.0.44"
multimap = "0.9"
bit_reverse = "0.1.8"
[dev-dependencies]
java-properties = "2.0"

View File

@@ -7,7 +7,7 @@ use rxing::multi::{GenericMultipleBarcodeReader, MultipleBarcodeReader};
use rxing::oned::rss::expanded::RSSExpandedReader;
use rxing::oned::rss::RSS14Reader;
use rxing::oned::{
CodaBarReader, Code39Reader, Code93Reader, EAN13Reader, EAN8Reader, ITFReader, UPCAReader,
CodaBarReader, Code39Reader, Code93Reader, EAN13Reader, EAN8Reader, TelepenReader, ITFReader, UPCAReader,
UPCEReader,
};
use rxing::pdf417::PDF417Reader;
@@ -165,6 +165,17 @@ fn rss_expanded_benchmark(c: &mut Criterion) {
});
}
fn telepen_benchmark(c: &mut Criterion) {
let mut image = get_image("test_resources/blackbox/telepen-1/02.png");
let mut reader = TelepenReader::default();
c.bench_function("telepen", |b| {
b.iter(|| {
let _res = reader.decode(&mut image);
});
});
}
fn upca_benchmark(c: &mut Criterion) {
let mut image = get_image("test_resources/blackbox/upca-4/1.png");
c.bench_function("upca", |b| {
@@ -210,6 +221,7 @@ criterion_group!(
qrcode_benchmark,
rss14_benchmark,
rss_expanded_benchmark,
telepen_benchmark,
upca_benchmark,
upce_benchmark,
multi_barcode_benchmark

View File

@@ -73,6 +73,9 @@ pub enum BarcodeFormat {
/** RSS EXPANDED */
RSS_EXPANDED,
/** TELEPEN */
TELEPEN,
/** UPC-A 1D format. */
UPC_A,
@@ -107,6 +110,7 @@ impl Display for BarcodeFormat {
BarcodeFormat::MICRO_QR_CODE => "mqr",
BarcodeFormat::RSS_14 => "rss 14",
BarcodeFormat::RSS_EXPANDED => "rss expanded",
BarcodeFormat::TELEPEN => "telepen",
BarcodeFormat::UPC_A => "upc a",
BarcodeFormat::UPC_E => "upc e",
BarcodeFormat::UPC_EAN_EXTENSION => "upc/ean extension",
@@ -149,6 +153,7 @@ impl From<&str> for BarcodeFormat {
"rss 14" | "rss_14" | "rss14" | "gs1 databar" | "gs1 databar coupon"
| "gs1_databar_coupon" => BarcodeFormat::RSS_14,
"rss expanded" | "expanded rss" | "rss_expanded" => BarcodeFormat::RSS_EXPANDED,
"telepen" => BarcodeFormat::TELEPEN,
"upc a" | "upc_a" | "upca" => BarcodeFormat::UPC_A,
"upc e" | "upc_e" | "upce" => BarcodeFormat::UPC_E,
"upc ean extension" | "upc extension" | "ean extension" | "upc/ean extension"

View File

@@ -165,7 +165,8 @@ impl MultiFormatReader {
|| self.possible_formats.contains(&BarcodeFormat::CODE_128)
|| self.possible_formats.contains(&BarcodeFormat::ITF)
|| self.possible_formats.contains(&BarcodeFormat::RSS_14)
|| self.possible_formats.contains(&BarcodeFormat::RSS_EXPANDED);
|| self.possible_formats.contains(&BarcodeFormat::RSS_EXPANDED)
|| self.possible_formats.contains(&BarcodeFormat::TELEPEN);
if one_d && !self.try_harder {
if let Ok(res) = self.one_d_reader.decode_with_hints(image, &self.hints) {
return Ok(res);

View File

@@ -22,7 +22,7 @@ use crate::{
datamatrix::DataMatrixWriter,
oned::{
CodaBarWriter, Code128Writer, Code39Writer, Code93Writer, EAN13Writer, EAN8Writer,
ITFWriter, UPCAWriter, UPCEWriter,
ITFWriter, TelepenWriter, UPCAWriter, UPCEWriter,
},
pdf417::PDF417Writer,
qrcode::QRCodeWriter,
@@ -70,6 +70,7 @@ impl Writer for MultiFormatWriter {
BarcodeFormat::PDF_417 => Box::<PDF417Writer>::default(),
BarcodeFormat::CODABAR => Box::<CodaBarWriter>::default(),
BarcodeFormat::DATA_MATRIX => Box::<DataMatrixWriter>::default(),
BarcodeFormat::TELEPEN => Box::<TelepenWriter>::default(),
BarcodeFormat::AZTEC => Box::<AztecWriter>::default(),
_ => {
return Err(Exceptions::illegal_argument_with(format!(

View File

@@ -176,7 +176,8 @@ impl MultiUseMultiFormatReader {
|| self.possible_formats.contains(&BarcodeFormat::CODE_128)
|| self.possible_formats.contains(&BarcodeFormat::ITF)
|| self.possible_formats.contains(&BarcodeFormat::RSS_14)
|| self.possible_formats.contains(&BarcodeFormat::RSS_EXPANDED);
|| self.possible_formats.contains(&BarcodeFormat::RSS_EXPANDED)
|| self.possible_formats.contains(&BarcodeFormat::TELEPEN);
if one_d && !self.try_harder {
if let Ok(res) = self.one_d_reader.decode_with_hints(image, &self.hints) {
return Ok(res);

View File

@@ -24,6 +24,9 @@ pub use code_128_reader::*;
mod itf_reader;
pub use itf_reader::*;
mod telepen_reader;
pub use telepen_reader::*;
mod upc_ean_reader;
pub use upc_ean_reader::*;
@@ -77,6 +80,9 @@ pub use upc_a_writer::*;
mod ean_13_writer;
pub use ean_13_writer::*;
mod telepen_writer;
pub use telepen_writer::*;
mod upc_ean_writer;
pub use upc_ean_writer::*;

View File

@@ -23,6 +23,7 @@ use super::Code93Reader;
use super::ITFReader;
use super::MultiFormatUPCEANReader;
use super::OneDReader;
use super::TelepenReader;
use crate::common::Result;
use crate::DecodeHintValue;
use crate::Exceptions;
@@ -104,6 +105,11 @@ impl OneDReader for MultiFormatOneDReader {
return Ok(res);
}
}
if possible_formats.contains(&BarcodeFormat::TELEPEN) {
if let Ok(res) = TelepenReader::default().decode_row(row_number, row, hints) {
return Ok(res);
}
}
} else {
if let Ok(res) =
MultiFormatUPCEANReader::new(internal_hints).decode_row(row_number, row, hints)
@@ -133,6 +139,9 @@ impl OneDReader for MultiFormatOneDReader {
if let Ok(res) = rss_expanded_reader.decode_row(row_number, row, hints) {
return Ok(res);
}
if let Ok(res) = TelepenReader::default().decode_row(row_number, row, hints) {
return Ok(res);
}
}
Err(Exceptions::NOT_FOUND)

427
src/oned/telepen_reader.rs Normal file
View File

@@ -0,0 +1,427 @@
/*
* Copyright 2008 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use rxing_one_d_proc_derive::OneDReader;
use crate::common::{BitArray, Result};
use crate::Exceptions;
use crate::RXingResult;
use crate::{point_f, BarcodeFormat};
use bit_reverse::ParallelReverse;
use super::OneDReader;
/**
* <p>Decodes Telepen barcodes.</p>
*
* @author Chris Wood
*/
#[derive(OneDReader)]
pub struct TelepenReader {
// Keep some instance variables to avoid reallocations
counters: Vec<u32>,
counterLength: usize,
}
impl Default for TelepenReader {
fn default() -> Self {
Self {
counters: vec![0; 80],
counterLength: 0,
}
}
}
impl OneDReader for TelepenReader {
fn decode_row(
&mut self,
_rowNumber: u32,
row: &crate::common::BitArray,
_hints: &crate::DecodingHintDictionary,
) -> Result<crate::RXingResult> {
self.counters.fill(0);
self.setCounters(row, (row.get_size() as f32 * 0.001) as u32)?;
let startOffset = self.findStartPattern()? as usize;
let end = self.findEndPattern(startOffset)? as usize;
let theCounters = &self.counters;
let mut maxBar = 0;
let mut minBar = u32::MAX;
// 0 position value will be whitespace prior to the barcode beginning
let mut j = startOffset;
// Calculate a median bar / gap width by establishing the smallest and
// largest gaps.
while j <= end {
let currentCounter = theCounters[j];
if currentCounter < minBar {
minBar = currentCounter;
}
if currentCounter > maxBar {
maxBar = currentCounter;
}
j += 1;
}
// Calculate median value as float.
let mut thresholdBar = (minBar + maxBar) as f64 / 2.0;
// Lean very slightly toward thicker bars.
thresholdBar = thresholdBar - (thresholdBar / 10.0);
// Start of the barcode is always a black bar.
let mut isBlack = true;
let mut pattern: Vec<u32> = vec![0; self.counterLength];
let mut patternLength: usize = 0;
pattern.fill(0);
j = startOffset;
// Categorise each value into narrow or wide black or white. Each
// permutation is signified by an integer value.
//
// 0 = Narrow White
// 1 = Narrow Black
// 2 = Wide White
// 3 = Wide Black
//
// Wide elements are 3x the width of narrow, thus:
//
// Black narrow: B
// Black wide: BBB
// White narrow: .
// White wide: ...
while j <= end {
let currentCounter = theCounters[j];
if (currentCounter as f64) < thresholdBar {
if isBlack {
// Narrow black: B
pattern[patternLength] = 1;
}
else {
// Narrow white: .
pattern[patternLength] = 0;
}
}
else {
if isBlack {
// Wide black: BBB
pattern[patternLength] = 3;
}
else {
// Wide white: ...
pattern[patternLength] = 2;
}
}
patternLength += 1;
j += 1;
isBlack = !isBlack;
}
let mut bits: BitArray = BitArray::new();
let mut state = 0;
j = 0;
// Convert narrow-wide sequence into bit array.
while j < patternLength - 1 {
if pattern[j] == 3 && pattern[j + 1] == 2 {
// BBB... = 010
bits.appendBit(false);
bits.appendBit(true);
bits.appendBit(false);
}
else if pattern[j] == 3 && pattern[j + 1] == 0 {
// BBB. = 00
bits.appendBit(false);
bits.appendBit(false);
}
else if pattern[j] == 1 && pattern[j + 1] == 2 && state == 0 {
// B... = 01
bits.appendBit(false);
bits.appendBit(true);
state = 1;
}
else if pattern[j] == 1 && pattern[j + 1] == 2 && state == 1 {
// B... = 10
bits.appendBit(true);
bits.appendBit(false);
state = 0;
}
else if pattern[j] == 1 && pattern[j + 1] == 0 {
// B. = 1
bits.appendBit(true);
}
j += 2;
}
let byteLength = bits.getSizeInBytes();
let mut bytes: Vec<u8> = vec![0; byteLength];
bits.toBytes(0, bytes.as_mut_slice(), 0, byteLength);
j = 0;
// Tweak our byte array to clean things up a little.
while j < byteLength {
// Telepen is little-endian, so need to swap the
// bits around for each byte to be correct.
bytes[j] = bytes[j].swap_bits();
// The first bit in the byte can always be disregarded
// as the highest ASCII decimal value is 127. It might be
// set because it is used as a parity bit during encoding
// (ensuring that there is an equal number of 1 bits in the
// byte).
if bytes[j] >= 128 {
bytes[j] -= 128;
}
j += 1;
}
// First character should be _ which is decimal 95.
if bytes[0] != 95 {
return Err(Exceptions::NOT_FOUND);
}
// Last character should be z which is decimal 122.
if bytes[byteLength - 1] != 122 {
return Err(Exceptions::NOT_FOUND);
}
// Content bytes
let contentBytes = bytes[1 .. byteLength - 2].to_vec();
let contentString = String::from_utf8_lossy(&contentBytes);
// Penultimate byte is a block check character.
let check = bytes[byteLength - 2];
// Calculate the checksum character
let mut sum = 0;
for c in contentString.chars() {
sum += c as u32;
}
let remainder = sum % 127;
let diff = 127 - remainder;
let checksum = if diff != 127 {
diff as u8 as char
}
else {
0 as char
};
// Validate checksum
if check != checksum as u8 {
return Err(Exceptions::NOT_FOUND);
}
let mut result = RXingResult::new(
&contentString,
bytes,
vec![
// TODO: populate with real numbers
point_f(0 as f32, 0 as f32),
point_f(0 as f32, 0 as f32),
],
BarcodeFormat::TELEPEN,
);
result.putMetadata(
RXingResultMetadataType::SYMBOLOGY_IDENTIFIER,
RXingResultMetadataValue::SymbologyIdentifier("]B0".to_owned()),
);
Ok(result)
}
}
impl TelepenReader {
pub fn new() -> Self {
Self {
counters: vec![0; 80], //Vec::with_capacity(80),
counterLength: 0,
}
}
/**
* Records the size of all runs of white and black pixels, starting with white.
* This is just like recordPattern, except it records all the counters, and
* uses our builtin "counters" member for storage.
* @param row row to count from
*/
fn setCounters(&mut self, row: &BitArray, minToleratedWidth: u32) -> Result<()> {
self.counterLength = 0;
let mut i = 1;
let end = row.get_size();
let mut currentColor = false;
let mut count = 1;
// Move to first white pixel
while i < end && row.get(i) {
i += 1;
}
while i < end {
if row.get(i) == currentColor {
count += 1;
}
else {
if count >= minToleratedWidth || self.counterLength == 0 {
self.counterAppend(count);
}
else {
// Noise from previous bar. Treat it as the
// previous colour.
self.counters[self.counterLength - 1] += count;
}
count = 1;
currentColor = !currentColor;
}
i += 1;
}
if count >= minToleratedWidth {
self.counterAppend(count);
}
else {
// Noise from previous bar. Treat it as the
// previous colour.
self.counters[self.counterLength - 1] += count;
}
Ok(())
}
fn counterAppend(&mut self, e: u32) {
self.counters[self.counterLength] = e;
self.counterLength += 1;
if self.counterLength >= self.counters.len() {
let mut temp = vec![0; self.counterLength * 2];
temp[0..self.counterLength].clone_from_slice(&self.counters[..]);
self.counters = temp;
}
}
fn findStartPattern(&mut self) -> Result<u32> {
if self.counterLength <= 20 {
return Err(Exceptions::NOT_FOUND);
}
let mut i = 0;
while i < self.counterLength - 20 {
// Read next 20 in sequence. All 20 must be either between 28% and 38%
// of biggest, or between 90% to 100% of biggest.
let mut j = 0;
let mut maxBar: f32 = 0.0;
let mut minBar: f32 = f32::MAX;
while i + j < self.counterLength && j < 20 {
if (self.counters[i + j] as f32) > maxBar {
maxBar = self.counters[i + j] as f32;
}
if (self.counters[ i + j] as f32) < minBar {
minBar = self.counters[ i + j] as f32;
}
j += 1;
}
j = 0;
let median = minBar as f32 + maxBar as f32 / 2.0;
let mut passed = true;
// First 10 items must be:
// N-N-N-N-N-N-N-N-N-N-W
while i + j < self.counterLength && j < 11 {
if j < 10 {
// Narrow
if (self.counters[i + j] as f32) > median {
passed = false;
break;
}
else if j == 10 {
// Wide
if (self.counters[i + j] as f32) < median {
passed = false;
break;
}
}
}
j += 1;
}
if !passed {
i += 1;
continue;
}
return Ok(i as u32);
}
Err(Exceptions::NOT_FOUND)
}
fn findEndPattern(&mut self, start: usize) -> Result<u32> {
const TOLERANCE: f32 = 0.5;
let mut i = start;
while i < self.counterLength {
// Read next 20 in sequence. All 20 must be either between 28% and 38%
// of biggest, or between 90% to 100% of biggest.
let mut j = 0;
let mut maxBar: f32 = 0.0;
while i + j < self.counterLength && j < 20 {
if (self.counters[i + j] as f32) > maxBar {
maxBar = self.counters[i + j] as f32;
}
j += 1;
}
i = start + 20;
while i < self.counterLength {
if (self.counters[i] as f32) > (maxBar as f32 * (1.0 + TOLERANCE)) {
return Ok((i - 1) as u32);
}
i += 1;
}
}
return Ok((self.counterLength - 1) as u32);
}
pub fn arrayContains(array: &[char], key: char) -> bool {
array.contains(&key)
}
}

253
src/oned/telepen_writer.rs Normal file
View File

@@ -0,0 +1,253 @@
/*
* Copyright 2011 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use rxing_one_d_proc_derive::OneDWriter;
use regex::Regex;
use crate::common::Result;
use crate::BarcodeFormat;
use super::OneDimensionalCodeWriter;
/**
* This class renders Telepen as {@code boolean[]}.
*
* @author Chris Wood
*/
#[derive(OneDWriter, Default)]
pub struct TelepenWriter;
impl OneDimensionalCodeWriter for TelepenWriter {
fn encode_oned(&self, contents: &str) -> Result<Vec<bool>> {
// Calculate the checksum character
let mut sum = 0;
for c in contents.chars() {
sum += c as u32;
}
let remainder = sum % 127;
let diff = 127 - remainder;
let checksum = if diff != 127 {
diff as u8 as char
}
else {
0 as char
};
// Build binary string
let mut binary = String::new();
// Opening character is always _
let mut byte = "_".as_bytes()[0];
let mut bits = self.get_bits(byte);
for i in 0..8 {
binary.push(if bits[i] {
'1'
}
else {
'0'
});
}
// Content
for index in 0..contents.chars().count() {
byte = contents.chars().nth(index).unwrap() as u8;
bits = self.get_bits(byte);
for i in 0..8 {
binary.push(if bits[i] {
'1'
}
else {
'0'
});
}
}
// Checksum
byte = checksum as u8;
bits = self.get_bits(byte);
for i in 0..8 {
binary.push(if bits[i] {
'1'
}
else {
'0'
});
}
// Closing character is always z.
byte = "z".as_bytes()[0];
bits = self.get_bits(byte);
for i in 0..8 {
binary.push(if bits[i] {
'1'
}
else {
'0'
});
}
let re = Regex::new(r"^01|10$|01*0|00|1").unwrap();
let matches: Vec<&str> = re.find_iter(&binary).map(|m| m.as_str()).collect();
let mut result = vec![false; 2000];
let mut resultPosition = 0;
for b in matches {
if b == "010" {
// BBB...
result[resultPosition] = true;
result[resultPosition + 1] = true;
result[resultPosition + 2] = true;
result[resultPosition + 3] = false;
result[resultPosition + 4] = false;
result[resultPosition + 5] = false;
resultPosition += 6;
}
else if b == "00" {
// BBB.
result[resultPosition] = true;
result[resultPosition + 1] = true;
result[resultPosition + 2] = true;
result[resultPosition + 3] = false;
resultPosition += 4;
}
else if b == "1" {
// B.
result[resultPosition] = true;
result[resultPosition + 1] = false;
resultPosition += 2;
}
else if b == "01" {
// B...
result[resultPosition] = true;
result[resultPosition + 1] = false;
result[resultPosition + 2] = false;
result[resultPosition + 3] = false;
resultPosition += 4;
}
else if b == "10" {
// B...
result[resultPosition] = true;
result[resultPosition + 1] = false;
result[resultPosition + 2] = false;
result[resultPosition + 3] = false;
resultPosition += 4;
}
else if b == "0" {
return Err(Exceptions::illegal_argument_with(format!(
"Invalid bit combination!"
)));
}
else {
// B... (B.)* B...
result[resultPosition] = true;
result[resultPosition + 1] = false;
result[resultPosition + 2] = false;
result[resultPosition + 3] = false;
resultPosition += 4;
for _j in 2 .. b.len() - 2 {
result[resultPosition] = true;
result[resultPosition + 1] = false;
resultPosition += 2;
}
result[resultPosition] = true;
result[resultPosition + 1] = false;
result[resultPosition + 2] = false;
result[resultPosition + 3] = false;
resultPosition += 4;
}
}
Ok(result[0 .. resultPosition - 1].to_vec())
}
fn getSupportedWriteFormats(&self) -> Option<Vec<crate::BarcodeFormat>> {
Some(vec![BarcodeFormat::TELEPEN])
}
}
impl TelepenWriter {
fn get_bits(&self, byte: u8) -> Vec<bool> {
let mut bits = vec![false; 8];
let mut oneCount = 0;
for i in 0..8 {
let mask = 1 << i;
bits[i] = (mask & byte) > 0;
if bits[i] {
oneCount += 1;
}
}
// Set parity bit - there must be an even number
// of 1s in the 8 bits.
bits[7] = oneCount % 2 != 0;
return bits;
}
}
/**
* @author Chris Wood
*/
#[cfg(test)]
mod TelepenWriterTestCase {
use crate::{
common::{bit_matrix_test_case, BitMatrix},
BarcodeFormat, Writer,
};
use super::TelepenWriter;
#[test]
fn testEncode() {
doTest(
"Hello world!",
concat!(
"00000",
"10101010101110001110111000111000101110001000100011101010100010001110101010001000101010101000100011101110111000101010101000101000101010101000100011100010001010001110101010001000111010111010101010111011101011101110001010111010111000101010101",
"00000"
),
);
}
fn doTest(input: &str, expected: &str) {
let result = encode(input);
assert_eq!(expected, bit_matrix_test_case::matrix_to_string(&result));
}
fn encode(input: &str) -> BitMatrix {
TelepenWriter
.encode(input, &BarcodeFormat::TELEPEN, 0, 0)
.expect("must encode")
}
}

View File

@@ -0,0 +1 @@
SYMBOLOGY_IDENTIFIER=]B0

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1 @@
Test

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
Wikipedia

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1 @@
Hello world!

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

View File

@@ -0,0 +1 @@
Hello world!

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

View File

@@ -0,0 +1 @@
Hello world!

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2008 ZXing authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
use rxing::{oned::TelepenReader, BarcodeFormat};
mod common;
/**
* @author Chris Wood
*/
#[test]
fn telepen_black_box1_test_case() {
const NUMTESTS: u32 = 5;
let mut tester = common::AbstractBlackBoxTestCase::new(
"test_resources/blackbox/telepen-1",
TelepenReader::new(),
BarcodeFormat::TELEPEN,
);
// super("src/test/resources/blackbox/Telepen-1", new MultiFormatReader(), BarcodeFormat.Telepen);
tester.add_test(NUMTESTS, 2, 0.0);
tester.add_test(NUMTESTS, 2, 180.0);
tester.test_black_box();
}