1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
use super::Object;
#[cfg(feature = "chrono_time")]
use chrono::prelude::*;
#[cfg(not(feature = "chrono_time"))]
use time::strptime;
use time::{strftime, Tm};

const TIME_FMT_DECODE_STR: &str = "%Y%m%d%H%M%S%z";

#[cfg(feature = "chrono_time")]
impl From<DateTime<Local>> for Object {
	fn from(date: DateTime<Local>) -> Self {
		let mut timezone_str = date.format("D:%Y%m%d%H%M%S%:z'").to_string().into_bytes();
		convert_utc_offset(&mut timezone_str);
		Object::string_literal(timezone_str)
	}
}

// Find the last `:` and turn it into an `'` to account for PDF weirdness
fn convert_utc_offset(bytes: &mut [u8]) {
	let mut index = bytes.len();
	while let Some(last) = bytes[..index].last_mut() {
		if *last == b':' {
			*last = b'\'';
			break;
		}
		index -= 1;
	}
}

#[cfg(feature = "chrono_time")]
impl From<DateTime<UTC>> for Object {
	fn from(date: DateTime<UTC>) -> Self {
		Object::string_literal(date.format("D:%Y%m%d%H%M%SZ").to_string())
	}
}

impl From<Tm> for Object {
	fn from(date: Tm) -> Self {
		// can only fail if the TIME_FMT_ENCODE_STR would be invalid
		Object::string_literal(if date.tm_utcoff != 0 {
			// D:%Y%m%d%H%M%S:%z'
			//
			// UTC offset in the form +HHMM or -HHMM (empty string if the the object is naive).
			let timezone = strftime("%z", &date).unwrap();
			let timezone_str_start = strftime("%Y%m%d%H%M%S", &date).unwrap();
			let mut timezone_str = format!("D:{}{}:{}'", timezone_str_start, &timezone[..3], &timezone[3..]).into_bytes();
			convert_utc_offset(&mut timezone_str);
			timezone_str
		} else {
			format!("D:{}", strftime("%Y%m%d%H%M%SZ", &date).unwrap()).into_bytes()
		})
	}
}

impl Object {
	// Parses the `D`, `:` and `\` out of a `Object::String` to parse the date time
	fn datetime_string(&self) -> Option<String> {
		if let Object::String(ref bytes, _) = self {
			String::from_utf8(bytes.iter().filter(|b| ![b'D', b':', b'\''].contains(b)).cloned().collect()).ok()
		} else {
			None
		}
	}

	#[cfg(feature = "chrono_time")]
	pub fn as_datetime(&self) -> Option<DateTime<Local>> {
		let text = self.datetime_string()?;
		Local.datetime_from_str(&text, TIME_FMT_DECODE_STR).ok()
	}

	/// WARNING: `tm_wday` (weekday), `tm_yday` (day index in year), `tm_isdst`
	/// (daylight saving time) and `tm_nsec` (nanoseconds of the date from 1970)
	/// are set to 0 since they aren't available in the PDF time format. They could,
	/// however, be calculated manually
	#[cfg(not(feature = "chrono_time"))]
	pub fn as_datetime(&self) -> Option<Tm> {
		let text = self.datetime_string()?;
		strptime(&text, TIME_FMT_DECODE_STR).ok()
	}
}

#[cfg(feature = "chrono_time")]
#[test]
fn parse_datetime() {
	let time = Local::now().with_nanosecond(0).unwrap();
	let text: Object = time.into();
	let time2 = text.as_datetime();
	assert_eq!(time2, Some(time));
}

#[cfg(not(feature = "chrono_time"))]
#[test]
fn parse_datetime() {
	// Tm-based: Ignore tm_wday, tm_yday, tm_isdst and tm_nsec
	// - not important in the date parsing
	let time = Tm {
		tm_wday: 0,
		tm_yday: 0,
		tm_isdst: 0,
		tm_nsec: 0,
		..::time::now()
	};

	let text: Object = time.into();
	let time2 = text.as_datetime();
	assert_eq!(time2, Some(time));
}