//! Deserializer for the ImageMacro type. use std::collections::{HashMap, HashSet}; use std::fmt; use std::mem; use itertools::Itertools; use serde::de::{self, Deserialize, IntoDeserializer, Visitor, Unexpected}; use unicode_normalization::UnicodeNormalization; use unreachable::unreachable; use super::super::{Caption, Color, ImageMacro, VAlign, DEFAULT_COLOR, DEFAULT_OUTLINE_COLOR, DEFAULT_FONT, DEFAULT_HALIGN, MAX_CAPTION_COUNT, MAX_WIDTH, MAX_HEIGHT, MAX_CAPTION_LENGTH}; /// Publicly mentioned fields of ImageMacro. const FIELDS: &'static [&'static str] = &[ "template", "width", "height", "captions", ]; /// Semi-official fields that allow to set properties of all captions at once. const WHOLESALE_CAPTION_FIELDS: &'static [&'static str] = &[ "font", "color", "outline", ]; // How many fields (of any kind) are required at the very minimum. const REQUIRED_FIELDS_COUNT: usize = 1; // template const EXPECTING_MSG: &'static str = "representation of an image macro"; lazy_static! { static ref EXPECTING_FIELD_COUNT_MSG: String = format!("at least {}", REQUIRED_FIELDS_COUNT); } impl<'de> Deserialize<'de> for ImageMacro { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de> { deserializer.deserialize_map(ImageMacroVisitor) } } struct ImageMacroVisitor; impl<'de> Visitor<'de> for ImageMacroVisitor { type Value = ImageMacro; fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!(fmt,"{}", EXPECTING_MSG) } fn visit_map(self, mut map: V) -> Result where V: de::MapAccess<'de> { // Preemptively check for length against a minimum. if let Some(size) = map.size_hint() { if size < REQUIRED_FIELDS_COUNT { return Err(de::Error::invalid_length( size, &(&*EXPECTING_FIELD_COUNT_MSG as &str))); } } let mut template = None; let mut width = None; let mut height = None; let mut simple_fields = HashSet::new(); let mut simple_captions: HashMap = HashMap::new(); let mut full_captions: Option> = None; while let Some(key) = map.next_key::()? { let key = key.trim().to_lowercase(); match key.as_str() { // Data that's typically expected (or even mandatory). "template" => { if template.is_some() { return Err(de::Error::duplicate_field("template")); } let value: String = map.next_value()?; trace!("ImageMacro::template = {}", value); if value.is_empty() { return Err(de::Error::invalid_value( Unexpected::Str(&value), &"non-empty string")); } template = Some(value); } "width" => { if width.is_some() { return Err(de::Error::duplicate_field("width")); } let value = map.next_value()?; trace!("ImageMacro::width = {}", value); if value > MAX_WIDTH { return Err(de::Error::custom( format_args!("width is too large: {} > {}", value, MAX_WIDTH))); } width = Some(value); } "height" => { if height.is_some() { return Err(de::Error::duplicate_field("height")); } let value = map.next_value()?; trace!("ImageMacro::height = {}", value); if value > MAX_HEIGHT { return Err(de::Error::custom( format_args!("height is too large: {} > {}", value, MAX_HEIGHT))); } height = Some(value); } // Simplified way of defining top/middle/bottom captions. "top_text" | "middle_text" | "bottom_text" | "top_align" | "middle_align" | "bottom_align" | "top_font" | "middle_font" | "bottom_font" | "top_color" | "middle_color" | "bottom_color" | "top_outline" | "middle_outline" | "bottom_outline" => { let is_duplicate = simple_fields.contains(&key) || WHOLESALE_CAPTION_FIELDS.iter().any(|&f| { key.ends_with(&format!("_{}", f)) && simple_fields.contains(f) }); if is_duplicate { return Err(de::Error::custom(format_args!("duplicate field `{}`", key))); } simple_fields.insert(key.clone()); trace!("ImageMacro::{} = ...", key); let mut parts = key.split("_"); let (valign_part, field_part) = (parts.next().unwrap(), parts.next().unwrap()); // Put the horizontal align / text into the correct Caption. let valign_de = IntoDeserializer::::into_deserializer(valign_part); let valign = VAlign::deserialize(valign_de).unwrap(); let mut caption = simple_captions.entry(valign) .or_insert_with(|| Caption::at(valign)); match field_part { "text" => caption.text = map.next_value()?, "align" => caption.halign = map.next_value()?, "font" => caption.font = map.next_value()?, "color" => caption.color = map.next_value()?, "outline" => caption.outline = map.next_value()?, _ => unsafe { unreachable(); }, } } // Wholesale setting of simple captions' properties. "font" => { assert!(WHOLESALE_CAPTION_FIELDS.contains(&key.as_str())); let is_duplicate = simple_fields.contains(&key) || simple_fields.iter().any(|f| f.ends_with("_font")); if is_duplicate { return Err(de::Error::duplicate_field("font")); } simple_fields.insert("font".into()); let font: String = map.next_value()?; trace!("ImageMacro::font = {}", font); for valign in VAlign::iter_variants() { let mut caption = simple_captions.entry(valign) .or_insert_with(|| Caption::at(valign)); caption.font = font.clone(); } } "color" => { assert!(WHOLESALE_CAPTION_FIELDS.contains(&key.as_str())); let is_duplicate = simple_fields.contains(&key) || simple_fields.iter().any(|f| f.ends_with("_color")); if is_duplicate { return Err(de::Error::duplicate_field("color")); } simple_fields.insert("color".into()); let color: Color = map.next_value()?; trace!("ImageMacro::color = {}", color); for valign in VAlign::iter_variants() { let mut caption = simple_captions.entry(valign) .or_insert_with(|| Caption::at(valign)); caption.color = color; } } "outline" => { assert!(WHOLESALE_CAPTION_FIELDS.contains(&key.as_str())); let is_duplicate = simple_fields.contains(&key) || simple_fields.iter().any(|f| f.ends_with("_outline")); if is_duplicate { return Err(de::Error::duplicate_field("outline")); } simple_fields.insert("outline".into()); let outline: Option = map.next_value()?; trace!("ImageMacro::outline = {:?}", outline); for valign in VAlign::iter_variants() { let mut caption = simple_captions.entry(valign) .or_insert_with(|| Caption::at(valign)); caption.outline = outline; } } // Fully featured caption definition. "captions" => { if full_captions.is_some() { return Err(de::Error::duplicate_field("captions")); } // Deserialize captions, remembering which kind of input source they came from. let sourced_captions: Vec<(CaptionSource, Caption)> = map.next_value::>()?.into_iter() .map(From::from).collect(); if sourced_captions.iter().map(|&(s, _)| s).unique().count() > 1 { return Err(de::Error::custom( "captions must be either all texts, or all complete representations")); } if sourced_captions.is_empty() { full_captions = Some(vec![]); continue; } let source = sourced_captions[0].0; let count = sourced_captions.len(); let captions = sourced_captions.into_iter().map(|(_, c)| c); if source == CaptionSource::Text { // Captions can be just text strings, laid down in the center, // from top to bottom (depending on how many were provided). let valigns = match count { 0 => vec![], 1 => vec![VAlign::Bottom], 2 => vec![VAlign::Top, VAlign::Bottom], 3 => vec![VAlign::Top, VAlign::Middle, VAlign::Bottom], len => return Err(de::Error::invalid_length(len, &"0, 1, 2, or 3 strings")), }; full_captions = Some(valigns.into_iter().zip(captions) .map(|(v, c)| Caption { valign: v, ..c }) .collect()); } else { let captions: Vec<_> = captions.collect(); if captions.len() > MAX_CAPTION_COUNT { return Err(de::Error::custom( format_args!("there can be at most {} captions", MAX_CAPTION_COUNT))); } full_captions = Some(captions); } } key => return Err(de::Error::unknown_field(key, FIELDS)), } } // The input should either use the full "captions" field, // or the simpler version with top/middle/bottom_test/align -- but not both. let mut captions; if full_captions.is_some() { if simple_captions.len() > 0 { return Err(de::Error::custom( "`captions` cannot be provided along with `top/middle/bottom_text/align/etc.`")) } if simple_fields.len() > 0 { return Err(de::Error::custom( format_args!("custom `{}` cannot be provided along with `captions`", simple_fields.iter().next().unwrap()))); } } // Convert everything to "full" captions either way. if simple_captions.len() > 0 { captions = simple_captions.into_iter() .map(|(_, c)| c).filter(|c| !c.text.is_empty()) .collect() } else { captions = full_captions.unwrap_or_else(|| vec![]); } captions.sort_by_key(|c| (c.valign, c.halign)); for caption in &mut captions { caption.text = normalize_text(&caption.text)?; } let template = template.ok_or_else(|| de::Error::missing_field("template"))?; Ok(ImageMacro{template, width, height, captions}) } } // ImageMacro::captions can be provided as either a list of strings, // or a list of complete Caption representations (as maps). // However, we need to remember where the Captions originally came from // in order to possibly pick the correct vertical alignment for them. #[derive(Clone, Copy, PartialEq, Eq, Hash)] enum CaptionSource { Text, Map } struct SourcedCaption(CaptionSource, Caption); impl From for (CaptionSource, Caption) { fn from(sc: SourcedCaption) -> Self { (sc.0, sc.1) } } impl<'de> Deserialize<'de> for SourcedCaption { fn deserialize(deserializer: D) -> Result where D: de::Deserializer<'de> { deserializer.deserialize_any(SourcedCaptionVisitor) } } struct SourcedCaptionVisitor; impl<'de> Visitor<'de> for SourcedCaptionVisitor { type Value = SourcedCaption; fn expecting(&self, fmt: &mut fmt::Formatter) -> fmt::Result { write!(fmt, "text or image macro's caption representation") } /// Deserialize Caption from a string. /// /// The vertical align of such caption is intentionally left undefined, /// as it should be filled in by the ImageMacro deserializer. fn visit_str(self, text: &str) -> Result { let caption = Caption{ text: text.to_owned(), halign: DEFAULT_HALIGN, valign: unsafe { mem::uninitialized() }, font: DEFAULT_FONT.into(), color: DEFAULT_COLOR, outline: Some(DEFAULT_OUTLINE_COLOR), }; let result = SourcedCaption(CaptionSource::Text, caption); Ok(result) } fn visit_map(self, map: V) -> Result where V: de::MapAccess<'de> { // Use the default way of deserializing Caption from a map. let inner_de = de::value::MapAccessDeserializer::new(map); let caption = Deserialize::deserialize(inner_de)?; let result = SourcedCaption(CaptionSource::Map, caption); Ok(result) } } /// Normalize the caption text and validate it. fn normalize_text(s: &str) -> Result { // Use the NFC form as suggested by rusttype crate docs. let normalized = s.nfc(); let mut count = 0; let text: String = normalized.map(|c| { count += 1; c }).collect(); trace!("Caption text normalized to {} characters", count); if count > MAX_CAPTION_LENGTH { return Err(E::custom(format_args!( "caption text is too long: {} > {} characters", count, MAX_CAPTION_LENGTH))); } Ok(text) }