1use krilla::metadata::{Metadata, TextDirection};
2use typst_library::foundations::{Datetime, Smart};
3use typst_library::layout::Dir;
4use typst_library::model::Document;
5use typst_library::text::Locale;
6
7use crate::convert::GlobalContext;
8
9pub(crate) fn build_metadata(gc: &GlobalContext, doc_lang: Option<Locale>) -> Metadata {
10 let lang = doc_lang.unwrap_or(Locale::DEFAULT);
13
14 let dir = if lang.lang.dir() == Dir::RTL {
15 TextDirection::RightToLeft
16 } else {
17 TextDirection::LeftToRight
18 };
19
20 let mut metadata = Metadata::new()
21 .keywords(gc.document.info().keywords.iter().map(Into::into).collect())
22 .authors(gc.document.info().author.iter().map(Into::into).collect())
23 .language(lang.rfc_3066().to_string());
24
25 if let Some(creator) = gc
26 .options
27 .creator
28 .clone()
29 .unwrap_or_else(|| Some(format!("Typst {}", typst_utils::version().raw())))
30 {
31 metadata = metadata.creator(creator);
32 }
33
34 if let Some(title) = &gc.document.info().title {
35 metadata = metadata.title(title.to_string());
36 }
37
38 if let Some(description) = &gc.document.info().description {
39 metadata = metadata.description(description.to_string());
40 }
41
42 if let Some(ident) = gc.options.ident.as_ref().custom() {
43 metadata = metadata.document_id(ident.to_string());
44 }
45
46 if let Some(date) = creation_date(gc) {
47 metadata = metadata.creation_date(date);
48 }
49
50 metadata = metadata.text_direction(dir);
51
52 metadata
53}
54
55pub fn creation_date(gc: &GlobalContext) -> Option<krilla::metadata::DateTime> {
60 let (datetime, tz) = match (gc.document.info().date, gc.options.timestamp) {
61 (Smart::Custom(Some(date)), _) => (date, None),
62 (Smart::Auto, Some(timestamp)) => (timestamp.datetime, Some(timestamp.timezone)),
63 _ => return None,
64 };
65
66 let year = datetime.year().filter(|&y| y >= 0)? as u16;
67
68 let mut kd = krilla::metadata::DateTime::new(year);
69
70 if let Some(month) = datetime.month() {
71 kd = kd.month(month);
72 }
73
74 if let Some(day) = datetime.day() {
75 kd = kd.day(day);
76 }
77
78 if let Some(h) = datetime.hour() {
79 kd = kd.hour(h);
80 }
81
82 if let Some(m) = datetime.minute() {
83 kd = kd.minute(m);
84 }
85
86 if let Some(s) = datetime.second() {
87 kd = kd.second(s);
88 }
89
90 match tz {
91 Some(Timezone::UTC) => kd = kd.utc_offset_hour(0).utc_offset_minute(0),
92 Some(Timezone::Local { hour_offset, minute_offset }) => {
93 kd = kd.utc_offset_hour(hour_offset).utc_offset_minute(minute_offset)
94 }
95 None => {}
96 }
97
98 Some(kd)
99}
100
101#[derive(Debug, Copy, Clone, Hash)]
103pub struct Timestamp {
104 pub(crate) datetime: Datetime,
106 pub(crate) timezone: Timezone,
108}
109
110impl Timestamp {
111 pub fn new_utc(datetime: Datetime) -> Self {
113 Self { datetime, timezone: Timezone::UTC }
114 }
115
116 pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option<Self> {
118 let hour_offset = (whole_minute_offset / 60).try_into().ok()?;
119 let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?;
124 match (hour_offset, minute_offset) {
125 (-23..=23, 0..=59) => Some(Self {
128 datetime,
129 timezone: Timezone::Local { hour_offset, minute_offset },
130 }),
131 _ => None,
132 }
133 }
134}
135
136#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
138pub enum Timezone {
139 UTC,
141 Local { hour_offset: i8, minute_offset: u8 },
144}
145
146#[cfg(test)]
147mod tests {
148 use super::*;
149
150 #[test]
151 fn test_timestamp_new_local() {
152 let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap();
153 let test = |whole_minute_offset, expect_timezone| {
154 assert_eq!(
155 Timestamp::new_local(dummy_datetime, whole_minute_offset)
156 .unwrap()
157 .timezone,
158 expect_timezone
159 );
160 };
161
162 test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 });
164 test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 });
165 test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 });
166 test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 });
167 test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 });
168 test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 });
172 test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 });
173 test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 });
174 test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 });
175
176 assert!(Timestamp::new_local(dummy_datetime, 1440).is_none());
178 assert!(Timestamp::new_local(dummy_datetime, -1440).is_none());
179 assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none());
180 assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none());
181 }
182}