|  | @@ -1,7 +1,12 @@
 | 
	
		
			
				|  |  |  use crate::data::Mapping;
 | 
	
		
			
				|  |  | +use crate::errors::{ThymeFileError, ThymeFileStructureError};
 | 
	
		
			
				|  |  |  use crate::image::{Image, Pixel};
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +use serde::{Deserialize, Serialize};
 | 
	
		
			
				|  |  |  use std::collections::HashMap;
 | 
	
		
			
				|  |  | +use std::io::{Read, Seek, Write};
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +const CURRENT_VERSION: &'static str = "0";
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  #[derive(Debug, Clone)]
 | 
	
		
			
				|  |  |  pub enum StitchType {
 | 
	
	
		
			
				|  | @@ -10,13 +15,23 @@ pub enum StitchType {
 | 
	
		
			
				|  |  |      HalfDown,
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +impl StitchType {
 | 
	
		
			
				|  |  | +    fn to_u8(&self) -> u8 {
 | 
	
		
			
				|  |  | +        match self {
 | 
	
		
			
				|  |  | +            StitchType::Normal => 'x' as u8,
 | 
	
		
			
				|  |  | +            StitchType::HalfUp => '/' as u8,
 | 
	
		
			
				|  |  | +            StitchType::HalfDown => '\\' as u8,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |  #[derive(Debug, Clone)]
 | 
	
		
			
				|  |  |  pub struct Stitch {
 | 
	
		
			
				|  |  |      pub color: ColorIdx,
 | 
	
		
			
				|  |  |      pub typ: StitchType,
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -#[derive(Debug)]
 | 
	
		
			
				|  |  | +#[derive(Debug, Serialize, Deserialize)]
 | 
	
		
			
				|  |  |  pub struct Color {
 | 
	
		
			
				|  |  |      pub name: String,
 | 
	
		
			
				|  |  |      pub color: Pixel,
 | 
	
	
		
			
				|  | @@ -24,7 +39,7 @@ pub struct Color {
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  #[derive(Debug, Clone, Copy)]
 | 
	
		
			
				|  |  |  pub struct ColorIdx {
 | 
	
		
			
				|  |  | -    pub idx: usize,
 | 
	
		
			
				|  |  | +    pub idx: u32,
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  #[derive(Debug)]
 | 
	
	
		
			
				|  | @@ -33,6 +48,7 @@ pub struct ThymeFile {
 | 
	
		
			
				|  |  |      pub height: u32,
 | 
	
		
			
				|  |  |      pub palette: Vec<Color>,
 | 
	
		
			
				|  |  |      pub payload: Vec<Option<Stitch>>,
 | 
	
		
			
				|  |  | +    pub metadata: HashMap<String, String>,
 | 
	
		
			
				|  |  |  }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |  impl ThymeFile {
 | 
	
	
		
			
				|  | @@ -40,15 +56,25 @@ impl ThymeFile {
 | 
	
		
			
				|  |  |          let width = image.width;
 | 
	
		
			
				|  |  |          let height = image.height;
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        let lookup = mapping
 | 
	
		
			
				|  |  | -            .iter()
 | 
	
		
			
				|  |  | -            .enumerate()
 | 
	
		
			
				|  |  | -            .map(|(idx, (pixel, _))| (*pixel, ColorIdx { idx }))
 | 
	
		
			
				|  |  | -            .collect::<HashMap<Pixel, ColorIdx>>();
 | 
	
		
			
				|  |  | +        let mut palette = Vec::new();
 | 
	
		
			
				|  |  | +        let mut lookup = HashMap::new();
 | 
	
		
			
				|  |  | +        let mut next_idx = 0;
 | 
	
		
			
				|  |  | +        for (color, info) in mapping.iter() {
 | 
	
		
			
				|  |  | +            if let Some(i) = info {
 | 
	
		
			
				|  |  | +                let idx = ColorIdx { idx: next_idx };
 | 
	
		
			
				|  |  | +                next_idx += 1;
 | 
	
		
			
				|  |  | +                palette.push(Color {
 | 
	
		
			
				|  |  | +                    color: *color,
 | 
	
		
			
				|  |  | +                    name: i.name.to_owned(),
 | 
	
		
			
				|  |  | +                });
 | 
	
		
			
				|  |  | +                lookup.insert(color, idx);
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          let payload = image
 | 
	
		
			
				|  |  |              .iter()
 | 
	
		
			
				|  |  | -            .map(|(_, pixel)| {
 | 
	
		
			
				|  |  | +            .map(|(idx, pixel)| {
 | 
	
		
			
				|  |  | +                println!("{:?}: => {:?}", idx, lookup.get(&pixel));
 | 
	
		
			
				|  |  |                  if let Some(color) = lookup.get(&pixel) {
 | 
	
		
			
				|  |  |                      Some(Stitch {
 | 
	
		
			
				|  |  |                          color: *color,
 | 
	
	
		
			
				|  | @@ -60,19 +86,14 @@ impl ThymeFile {
 | 
	
		
			
				|  |  |              })
 | 
	
		
			
				|  |  |              .collect();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | -        let palette = mapping
 | 
	
		
			
				|  |  | -            .iter()
 | 
	
		
			
				|  |  | -            .map(|(color, name)| Color {
 | 
	
		
			
				|  |  | -                color: *color,
 | 
	
		
			
				|  |  | -                name: name.clone(),
 | 
	
		
			
				|  |  | -            })
 | 
	
		
			
				|  |  | -            .collect();
 | 
	
		
			
				|  |  | +        let metadata = HashMap::new();
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  |          ThymeFile {
 | 
	
		
			
				|  |  |              width,
 | 
	
		
			
				|  |  |              height,
 | 
	
		
			
				|  |  |              palette,
 | 
	
		
			
				|  |  |              payload,
 | 
	
		
			
				|  |  | +            metadata,
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  
 | 
	
	
		
			
				|  | @@ -83,11 +104,139 @@ impl ThymeFile {
 | 
	
		
			
				|  |  |              payload.push(None);
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  |  
 | 
	
		
			
				|  |  | +        let metadata = HashMap::new();
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  |          ThymeFile {
 | 
	
		
			
				|  |  |              width,
 | 
	
		
			
				|  |  |              height,
 | 
	
		
			
				|  |  |              palette,
 | 
	
		
			
				|  |  |              payload,
 | 
	
		
			
				|  |  | +            metadata,
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    pub fn from_stream<R>(stream: &mut R) -> Result<ThymeFile, ThymeFileError>
 | 
	
		
			
				|  |  | +    where
 | 
	
		
			
				|  |  | +        R: Read + Seek,
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        fn missing_component(component: &str) -> ThymeFileError {
 | 
	
		
			
				|  |  | +            ThymeFileStructureError::new(format!(
 | 
	
		
			
				|  |  | +                "Missing component in thyme file: `{}`",
 | 
	
		
			
				|  |  | +                component
 | 
	
		
			
				|  |  | +            ))
 | 
	
		
			
				|  |  | +            .into()
 | 
	
		
			
				|  |  | +        }
 | 
	
		
			
				|  |  | +        let mut zip = zip::ZipArchive::new(stream)?;
 | 
	
		
			
				|  |  | +        let mut width = None;
 | 
	
		
			
				|  |  | +        let mut height = None;
 | 
	
		
			
				|  |  | +        let mut metadata = None;
 | 
	
		
			
				|  |  | +        let mut palette = None;
 | 
	
		
			
				|  |  | +        let mut payload = None;
 | 
	
		
			
				|  |  | +        for i in 0..zip.len() {
 | 
	
		
			
				|  |  | +            let file = zip.by_index(i)?;
 | 
	
		
			
				|  |  | +            match file.name() {
 | 
	
		
			
				|  |  | +                "THYME_VERSION" => {
 | 
	
		
			
				|  |  | +                    // TODO: actually check this
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                "dimensions.json" => {
 | 
	
		
			
				|  |  | +                    let (w, h): (u32, u32) = serde_json::from_reader(file)?;
 | 
	
		
			
				|  |  | +                    width = Some(w);
 | 
	
		
			
				|  |  | +                    height = Some(h);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                "metadata.json" => metadata = Some(serde_json::from_reader(file)?),
 | 
	
		
			
				|  |  | +                "palette.json" => palette = Some(serde_json::from_reader(file)?),
 | 
	
		
			
				|  |  | +                "payload.json" => {
 | 
	
		
			
				|  |  | +                    let ps: Vec<Option<IntermediateStitch>> = serde_json::from_reader(file)?;
 | 
	
		
			
				|  |  | +                    let ps: Result<Vec<Option<Stitch>>, ThymeFileError> = ps
 | 
	
		
			
				|  |  | +                        .iter()
 | 
	
		
			
				|  |  | +                        .map(|n| n.map(IntermediateStitch::to_stitch).transpose())
 | 
	
		
			
				|  |  | +                        .collect();
 | 
	
		
			
				|  |  | +                    payload = Some(ps?);
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +                name => {
 | 
	
		
			
				|  |  | +                    return Err(ThymeFileStructureError::new(format!(
 | 
	
		
			
				|  |  | +                        "Unrecognized element: {}",
 | 
	
		
			
				|  |  | +                        name
 | 
	
		
			
				|  |  | +                    ))
 | 
	
		
			
				|  |  | +                    .into())
 | 
	
		
			
				|  |  | +                }
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +            // whatever
 | 
	
		
			
				|  |  |          }
 | 
	
		
			
				|  |  | +        Ok(ThymeFile {
 | 
	
		
			
				|  |  | +            width: width.ok_or_else(|| missing_component("dimensions"))?,
 | 
	
		
			
				|  |  | +            height: height.ok_or_else(|| missing_component("dimensions"))?,
 | 
	
		
			
				|  |  | +            palette: palette.ok_or_else(|| missing_component("palette"))?,
 | 
	
		
			
				|  |  | +            payload: payload.ok_or_else(|| missing_component("payload"))?,
 | 
	
		
			
				|  |  | +            metadata: metadata.ok_or_else(|| missing_component("metadata"))?,
 | 
	
		
			
				|  |  | +        })
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    pub fn to_stream<W>(&self, stream: &mut W) -> std::io::Result<()>
 | 
	
		
			
				|  |  | +    where
 | 
	
		
			
				|  |  | +        W: Write + Seek,
 | 
	
		
			
				|  |  | +    {
 | 
	
		
			
				|  |  | +        let mut zip = zip::ZipWriter::new(stream);
 | 
	
		
			
				|  |  | +        let options =
 | 
	
		
			
				|  |  | +            zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Bzip2);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // add version metadata
 | 
	
		
			
				|  |  | +        zip.start_file("THYME_VERSION", options)?;
 | 
	
		
			
				|  |  | +        writeln!(zip, "{}", CURRENT_VERSION)?;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // add dimensions
 | 
	
		
			
				|  |  | +        zip.start_file("dimensions.json", options)?;
 | 
	
		
			
				|  |  | +        serde_json::to_writer(&mut zip, &(self.width, self.height))?;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // add metadata
 | 
	
		
			
				|  |  | +        zip.start_file("metadata.json", options)?;
 | 
	
		
			
				|  |  | +        serde_json::to_writer(&mut zip, &self.metadata)?;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // add palette
 | 
	
		
			
				|  |  | +        zip.start_file("palette.json", options)?;
 | 
	
		
			
				|  |  | +        serde_json::to_writer(&mut zip, &self.palette)?;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // add image payload
 | 
	
		
			
				|  |  | +        zip.start_file("payload.json", options)?;
 | 
	
		
			
				|  |  | +        serde_json::to_writer(&mut zip, &self.json_stitches())?;
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +        // add version
 | 
	
		
			
				|  |  | +        zip.finish()?;
 | 
	
		
			
				|  |  | +        Ok(())
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +    fn json_stitches(&self) -> Vec<Option<IntermediateStitch>> {
 | 
	
		
			
				|  |  | +        self.payload
 | 
	
		
			
				|  |  | +            .iter()
 | 
	
		
			
				|  |  | +            .map(|stitch| match stitch {
 | 
	
		
			
				|  |  | +                Some(s) => Some(IntermediateStitch(s.typ.to_u8(), s.color.idx)),
 | 
	
		
			
				|  |  | +                None => None,
 | 
	
		
			
				|  |  | +            })
 | 
	
		
			
				|  |  | +            .collect()
 | 
	
		
			
				|  |  | +    }
 | 
	
		
			
				|  |  | +}
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +// internal structs for serializing/deserializing the image part
 | 
	
		
			
				|  |  | +#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
 | 
	
		
			
				|  |  | +struct IntermediateStitch(u8, u32);
 | 
	
		
			
				|  |  | +
 | 
	
		
			
				|  |  | +impl IntermediateStitch {
 | 
	
		
			
				|  |  | +    fn to_stitch(self) -> Result<Stitch, ThymeFileError> {
 | 
	
		
			
				|  |  | +        let typ = match self.0 {
 | 
	
		
			
				|  |  | +            b'x' => StitchType::Normal,
 | 
	
		
			
				|  |  | +            b'/' => StitchType::HalfUp,
 | 
	
		
			
				|  |  | +            b'\\' => StitchType::HalfDown,
 | 
	
		
			
				|  |  | +            _ => {
 | 
	
		
			
				|  |  | +                return Err(ThymeFileStructureError::new(format!(
 | 
	
		
			
				|  |  | +                    "Unknown stitch type: {}",
 | 
	
		
			
				|  |  | +                    self.0
 | 
	
		
			
				|  |  | +                ))
 | 
	
		
			
				|  |  | +                .into())
 | 
	
		
			
				|  |  | +            }
 | 
	
		
			
				|  |  | +        };
 | 
	
		
			
				|  |  | +        Ok(Stitch {
 | 
	
		
			
				|  |  | +            typ,
 | 
	
		
			
				|  |  | +            color: ColorIdx { idx: self.1 },
 | 
	
		
			
				|  |  | +        })
 | 
	
		
			
				|  |  |      }
 | 
	
		
			
				|  |  |  }
 |