|
@@ -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 },
|
|
|
+ })
|
|
|
}
|
|
|
}
|