296 lines
9.5 KiB
Rust
296 lines
9.5 KiB
Rust
use arrayvec::{ArrayString, ArrayVec};
|
|
use png::{BitDepth, ColorType, EncodingError};
|
|
use std::fs::File;
|
|
use std::io::{BufRead, BufReader, BufWriter, Lines};
|
|
use std::{env, io, process};
|
|
use thiserror::Error;
|
|
|
|
mod nummap;
|
|
|
|
#[cfg(test)]
|
|
mod test;
|
|
|
|
#[derive(Error, Debug)]
|
|
#[allow(clippy::enum_variant_names)]
|
|
enum CifError {
|
|
#[error("The CIF number string was too long")]
|
|
CapacityError,
|
|
#[error("CIF Number is invalid: {0:?}")]
|
|
GrammarError(String),
|
|
#[error("An IO error has occurred: {0}")]
|
|
IoError(io::Error),
|
|
#[error("CIF header is invalid")]
|
|
InvalidHeaderError,
|
|
#[error("CIF size is invalid: {0:?}")]
|
|
InvalidSizeError(String),
|
|
#[error("CIF metadata corrupted: {0:?}")]
|
|
InvalidMetadataError(String),
|
|
#[error("Invalid CIF pixel: {0}")]
|
|
InvalidPixelError(String),
|
|
#[error("CIF image doesn't contain any pixels")]
|
|
NoDataError,
|
|
#[error("PNG error: {0}")]
|
|
PngError(EncodingError),
|
|
#[error("Illegal whitespace: {0:?}")]
|
|
IllegalWhitespaceError(String),
|
|
}
|
|
|
|
#[allow(clippy::enum_glob_use)]
|
|
use crate::CifError::*;
|
|
|
|
impl<T> From<arrayvec::CapacityError<T>> for CifError {
|
|
fn from(_: arrayvec::CapacityError<T>) -> Self {
|
|
CapacityError
|
|
}
|
|
}
|
|
|
|
impl From<io::Error> for CifError {
|
|
fn from(e: io::Error) -> Self {
|
|
IoError(e)
|
|
}
|
|
}
|
|
|
|
impl From<EncodingError> for CifError {
|
|
fn from(e: EncodingError) -> Self {
|
|
PngError(e)
|
|
}
|
|
}
|
|
|
|
type Result<T> = std::result::Result<T, CifError>;
|
|
|
|
fn join_to_arraystring<const C: usize>(elems: &[&str], str: &mut ArrayString<C>) -> Result<()> {
|
|
for (i, item) in elems.iter().enumerate() {
|
|
if i > 0 {
|
|
str.try_push(' ')?;
|
|
}
|
|
str.try_push_str(item)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn lines_read_noempty<T: BufRead>(lines: &mut Lines<T>) -> Result<Option<String>> {
|
|
while let Some(s) = lines.next().transpose()? {
|
|
if s.is_empty() {
|
|
continue
|
|
}
|
|
assert_trailing_leading(&s)?;
|
|
return Ok(Some(s))
|
|
}
|
|
Ok(None)
|
|
}
|
|
|
|
#[inline]
|
|
fn assert_trailing_leading(s: &str) -> Result<()> {
|
|
if s.is_empty() || s.as_bytes()[0] == 32 || s.as_bytes()[s.len() - 1] == 32 {
|
|
return Err(IllegalWhitespaceError(s.to_string()))
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn is_cif_number(num: &str, bytes_per_pixel: u32) -> Result<ArrayVec<&str, 4_usize>> {
|
|
let split = num.split(';').collect::<ArrayVec<_, 4>>();
|
|
if split.len() != bytes_per_pixel as usize {
|
|
return Err(InvalidPixelError(num.to_string()))
|
|
} else {
|
|
Ok(split)
|
|
}
|
|
}
|
|
|
|
fn unfuck_cif_number(number: &str) -> Result<u32> {
|
|
assert_trailing_leading(number)?;
|
|
let mut buf = ArrayString::<2048>::new();
|
|
let split = number.split_ascii_whitespace().collect::<ArrayVec<_, 64>>();
|
|
let splitlen = split.len();
|
|
for (i, word) in split.iter().enumerate() {
|
|
if !word.starts_with("ty") {
|
|
continue
|
|
}
|
|
let thousands = match *word {
|
|
"tysiąc" => {
|
|
if i == 0 {
|
|
&1_u16
|
|
} else {
|
|
return Err(GrammarError(number.to_string()))
|
|
}
|
|
}
|
|
"tysięcy" | "tysiące" => {
|
|
if i > 0 {
|
|
join_to_arraystring(&split[..i], &mut buf)?;
|
|
nummap::U16_NUMMAP.get(&buf).ok_or_else(|| GrammarError(number.to_string()))?
|
|
} else {
|
|
return Err(GrammarError(number.to_string()))
|
|
}
|
|
}
|
|
_ => return Err(GrammarError(number.to_string())),
|
|
};
|
|
let remainder = if i + 1 < splitlen {
|
|
buf.clear();
|
|
join_to_arraystring(&split[i + 1..], &mut buf)?;
|
|
nummap::U16_NUMMAP.get(&buf).ok_or_else(|| GrammarError(number.to_string()))?
|
|
} else {
|
|
&0_u16
|
|
};
|
|
return Ok(u32::from(*thousands) * 1000 + u32::from(*remainder))
|
|
}
|
|
Ok(u32::from(*nummap::U16_NUMMAP.get(number).ok_or_else(|| GrammarError(number.to_string()))?))
|
|
}
|
|
|
|
fn unfuck_cif_number_fast(number: &str) -> Result<u8> {
|
|
assert_trailing_leading(number)?;
|
|
let mut buf = ArrayString::<2048>::new();
|
|
let split = number.split_ascii_whitespace().take(8).collect::<ArrayVec<_, 64>>();
|
|
|
|
join_to_arraystring(&split, &mut buf)?;
|
|
let number = *(nummap::U8_NUMMAP.get(&buf).ok_or_else(|| InvalidPixelError(number.to_string()))?);
|
|
|
|
Ok(number)
|
|
}
|
|
|
|
fn decoder(inputfile: &str, outputfile: &str) -> Result<()> {
|
|
let infile = File::open(inputfile)?;
|
|
let outfile = File::create(outputfile)?;
|
|
let mut lines = BufReader::new(infile).lines();
|
|
let rawriter = BufWriter::new(outfile);
|
|
|
|
// "CIF: <flags>"
|
|
if !lines.next().transpose()?.ok_or(InvalidHeaderError)?.starts_with("CIF:") {
|
|
return Err(InvalidHeaderError)
|
|
}
|
|
|
|
// "WERSJA jeden"
|
|
let version_str = lines_read_noempty(&mut lines)?.ok_or(InvalidHeaderError)?;
|
|
let version_split = version_str.split_ascii_whitespace().take(2).collect::<ArrayVec<_, 2>>();
|
|
if version_split.len() != 2 || version_split[0] != "WERSJA" || version_split[1] != "jeden" {
|
|
return Err(InvalidHeaderError)
|
|
}
|
|
|
|
// "ROZMIAR szerokość: osiem, wysokość: osiem, bitów_na_piksel: trzydzieści dwa"
|
|
let size_string = lines_read_noempty(&mut lines)?.ok_or(InvalidHeaderError)?;
|
|
let size_split = size_string
|
|
.strip_prefix("ROZMIAR")
|
|
.ok_or_else(|| InvalidSizeError(size_string.to_string()))?
|
|
.split(',')
|
|
.take(3)
|
|
.collect::<ArrayVec<_, 3>>();
|
|
if size_split.len() != 3 {
|
|
return Err(InvalidSizeError(size_string.to_string()))
|
|
}
|
|
|
|
let width = unfuck_cif_number(
|
|
size_split[0]
|
|
.trim_start()
|
|
.strip_prefix("szerokość:")
|
|
.ok_or_else(|| InvalidSizeError(size_string.to_string()))?
|
|
.trim_start(),
|
|
)?;
|
|
let height = unfuck_cif_number(
|
|
size_split[1]
|
|
.trim_start()
|
|
.strip_prefix("wysokość:")
|
|
.ok_or_else(|| InvalidSizeError(size_string.to_string()))?
|
|
.trim_start(),
|
|
)?;
|
|
|
|
let format = match unfuck_cif_number_fast(
|
|
size_split[2]
|
|
.trim_start()
|
|
.strip_prefix("bitów_na_piksel:")
|
|
.ok_or_else(|| InvalidSizeError(size_string.to_string()))?
|
|
.trim_start(),
|
|
)? {
|
|
24 => ColorType::Rgb,
|
|
32 => ColorType::Rgba,
|
|
_ => return Err(InvalidSizeError(size_string.to_string())),
|
|
};
|
|
|
|
if width == 0 || height == 0 {
|
|
return Err(NoDataError)
|
|
}
|
|
|
|
let mut outpng = png::Encoder::new(rawriter, width, height);
|
|
outpng.set_color(format);
|
|
outpng.set_depth(BitDepth::Eight);
|
|
|
|
let bytes_per_pixel = if format == ColorType::Rgb { 3 } else { 4 };
|
|
|
|
let mut md_head = false;
|
|
let mut md_txt = ArrayString::<2048>::new();
|
|
let mut md_key = ArrayString::<2048>::new();
|
|
|
|
let first = loop {
|
|
if let Some(line) = lines_read_noempty(&mut lines)? {
|
|
// So this is still some cringe code
|
|
// But currently it works
|
|
if line.starts_with("METADANE ") {
|
|
if md_head {
|
|
let split = line.split_ascii_whitespace().collect::<ArrayVec<_, 256>>();
|
|
if split.len() > 2 {
|
|
return Err(InvalidMetadataError("Metadata values must not contain line feeds.".to_string()))
|
|
}
|
|
md_txt.push_str(&line[8..]);
|
|
continue
|
|
} else {
|
|
md_head = true;
|
|
let mut buf = ArrayString::<2048>::new();
|
|
let split = line.split_ascii_whitespace().collect::<ArrayVec<_, 256>>();
|
|
join_to_arraystring(&split, &mut buf)?;
|
|
let md_split = buf.splitn(3, ' ').collect::<ArrayVec<_, 3>>();
|
|
if md_split.len() != 3 {
|
|
return Err(InvalidMetadataError(line.to_string()))
|
|
}
|
|
md_key.push_str(md_split[1]);
|
|
md_txt.push_str(md_split[2]);
|
|
continue
|
|
}
|
|
} else if is_cif_number(&line, bytes_per_pixel).is_err() {
|
|
md_txt.push_str(&line);
|
|
continue
|
|
} else {
|
|
outpng.add_itxt_chunk(md_key.to_string(), md_txt.to_string())?;
|
|
break line
|
|
}
|
|
}
|
|
return Err(NoDataError)
|
|
};
|
|
|
|
let mut outpng = outpng.write_header()?;
|
|
|
|
let mut pixmap = Vec::with_capacity((bytes_per_pixel * width * height) as usize);
|
|
|
|
if !first.is_empty() {
|
|
let px_split = first.split(';').collect::<ArrayVec<_, 4>>();
|
|
if px_split.len() != bytes_per_pixel as usize {
|
|
return Err(InvalidPixelError(first.to_string()))
|
|
}
|
|
for px in px_split {
|
|
pixmap.push(unfuck_cif_number_fast(px.trim_start())?);
|
|
}
|
|
}
|
|
|
|
while let Some(line) = lines_read_noempty(&mut lines)? {
|
|
let px_split = line.split(';').collect::<ArrayVec<_, 4>>();
|
|
if px_split.len() != bytes_per_pixel as usize {
|
|
return Err(InvalidPixelError(line.to_string()))
|
|
}
|
|
for px in px_split {
|
|
pixmap.push(unfuck_cif_number_fast(px.trim())?);
|
|
}
|
|
}
|
|
|
|
outpng.write_image_data(&pixmap)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn main() {
|
|
let argv = env::args().skip(1).take(2).collect::<ArrayVec<_, 2>>();
|
|
if argv.len() != 2 {
|
|
eprintln!("Invalid usage;\nUsage: zardzewialy-dekoder-cif <input cif file> <output bmp file>");
|
|
process::exit(1);
|
|
}
|
|
if let Err(e) = decoder(&argv[0], &argv[1]) {
|
|
eprintln!("Error: {}", e);
|
|
process::exit(1);
|
|
}
|
|
}
|