diff options
| author | Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> | 2025-04-01 16:42:52 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-01 14:42:52 +0000 |
| commit | 96dd67e011bb317cf78683bcf1edfdfca5e7b6b3 (patch) | |
| tree | 900a0c4e7723af4289685af35d788041055ad4a2 /crates/typst-pdf/src/metadata.rs | |
| parent | 012e14d40cb44997630cf6469a446f217f2e9057 (diff) | |
Switch PDF backend to `krilla` (#5420)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
Diffstat (limited to 'crates/typst-pdf/src/metadata.rs')
| -rw-r--r-- | crates/typst-pdf/src/metadata.rs | 184 |
1 files changed, 184 insertions, 0 deletions
diff --git a/crates/typst-pdf/src/metadata.rs b/crates/typst-pdf/src/metadata.rs new file mode 100644 index 00000000..589c5e2f --- /dev/null +++ b/crates/typst-pdf/src/metadata.rs @@ -0,0 +1,184 @@ +use ecow::EcoString; +use krilla::metadata::{Metadata, TextDirection}; +use typst_library::foundations::{Datetime, Smart}; +use typst_library::layout::Dir; +use typst_library::text::Lang; + +use crate::convert::GlobalContext; + +pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata { + let creator = format!("Typst {}", env!("CARGO_PKG_VERSION")); + + let lang = gc.languages.iter().max_by_key(|(_, &count)| count).map(|(&l, _)| l); + + let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { + TextDirection::RightToLeft + } else { + TextDirection::LeftToRight + }; + + let mut metadata = Metadata::new() + .creator(creator) + .keywords(gc.document.info.keywords.iter().map(EcoString::to_string).collect()) + .authors(gc.document.info.author.iter().map(EcoString::to_string).collect()); + + let lang = gc.languages.iter().max_by_key(|(_, &count)| count).map(|(&l, _)| l); + + if let Some(lang) = lang { + metadata = metadata.language(lang.as_str().to_string()); + } + + if let Some(title) = &gc.document.info.title { + metadata = metadata.title(title.to_string()); + } + + if let Some(subject) = &gc.document.info.description { + metadata = metadata.subject(subject.to_string()); + } + + if let Some(ident) = gc.options.ident.custom() { + metadata = metadata.document_id(ident.to_string()); + } + + // (1) If the `document.date` is set to specific `datetime` or `none`, use it. + // (2) If the `document.date` is set to `auto` or not set, try to use the + // date from the options. + // (3) Otherwise, we don't write date metadata. + let (date, tz) = match (gc.document.info.date, gc.options.timestamp) { + (Smart::Custom(date), _) => (date, None), + (Smart::Auto, Some(timestamp)) => { + (Some(timestamp.datetime), Some(timestamp.timezone)) + } + _ => (None, None), + }; + + if let Some(date) = date.and_then(|d| convert_date(d, tz)) { + metadata = metadata.creation_date(date); + } + + metadata = metadata.text_direction(dir); + + metadata +} + +fn convert_date( + datetime: Datetime, + tz: Option<Timezone>, +) -> Option<krilla::metadata::DateTime> { + let year = datetime.year().filter(|&y| y >= 0)? as u16; + + let mut kd = krilla::metadata::DateTime::new(year); + + if let Some(month) = datetime.month() { + kd = kd.month(month); + } + + if let Some(day) = datetime.day() { + kd = kd.day(day); + } + + if let Some(h) = datetime.hour() { + kd = kd.hour(h); + } + + if let Some(m) = datetime.minute() { + kd = kd.minute(m); + } + + if let Some(s) = datetime.second() { + kd = kd.second(s); + } + + match tz { + Some(Timezone::UTC) => kd = kd.utc_offset_hour(0).utc_offset_minute(0), + Some(Timezone::Local { hour_offset, minute_offset }) => { + kd = kd.utc_offset_hour(hour_offset).utc_offset_minute(minute_offset) + } + None => {} + } + + Some(kd) +} + +/// A timestamp with timezone information. +#[derive(Debug, Clone, Copy)] +pub struct Timestamp { + /// The datetime of the timestamp. + pub(crate) datetime: Datetime, + /// The timezone of the timestamp. + pub(crate) timezone: Timezone, +} + +impl Timestamp { + /// Create a new timestamp with a given datetime and UTC suffix. + pub fn new_utc(datetime: Datetime) -> Self { + Self { datetime, timezone: Timezone::UTC } + } + + /// Create a new timestamp with a given datetime, and a local timezone offset. + pub fn new_local(datetime: Datetime, whole_minute_offset: i32) -> Option<Self> { + let hour_offset = (whole_minute_offset / 60).try_into().ok()?; + // Note: the `%` operator in Rust is the remainder operator, not the + // modulo operator. The remainder operator can return negative results. + // We can simply apply `abs` here because we assume the `minute_offset` + // will have the same sign as `hour_offset`. + let minute_offset = (whole_minute_offset % 60).abs().try_into().ok()?; + match (hour_offset, minute_offset) { + // Only accept valid timezone offsets with `-23 <= hours <= 23`, + // and `0 <= minutes <= 59`. + (-23..=23, 0..=59) => Some(Self { + datetime, + timezone: Timezone::Local { hour_offset, minute_offset }, + }), + _ => None, + } + } +} + +/// A timezone. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Timezone { + /// The UTC timezone. + UTC, + /// The local timezone offset from UTC. And the `minute_offset` will have + /// same sign as `hour_offset`. + Local { hour_offset: i8, minute_offset: u8 }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_timestamp_new_local() { + let dummy_datetime = Datetime::from_ymd_hms(2024, 12, 17, 10, 10, 10).unwrap(); + let test = |whole_minute_offset, expect_timezone| { + assert_eq!( + Timestamp::new_local(dummy_datetime, whole_minute_offset) + .unwrap() + .timezone, + expect_timezone + ); + }; + + // Valid timezone offsets + test(0, Timezone::Local { hour_offset: 0, minute_offset: 0 }); + test(480, Timezone::Local { hour_offset: 8, minute_offset: 0 }); + test(-480, Timezone::Local { hour_offset: -8, minute_offset: 0 }); + test(330, Timezone::Local { hour_offset: 5, minute_offset: 30 }); + test(-210, Timezone::Local { hour_offset: -3, minute_offset: 30 }); + test(-720, Timezone::Local { hour_offset: -12, minute_offset: 0 }); // AoE + + // Corner cases + test(315, Timezone::Local { hour_offset: 5, minute_offset: 15 }); + test(-225, Timezone::Local { hour_offset: -3, minute_offset: 45 }); + test(1439, Timezone::Local { hour_offset: 23, minute_offset: 59 }); + test(-1439, Timezone::Local { hour_offset: -23, minute_offset: 59 }); + + // Invalid timezone offsets + assert!(Timestamp::new_local(dummy_datetime, 1440).is_none()); + assert!(Timestamp::new_local(dummy_datetime, -1440).is_none()); + assert!(Timestamp::new_local(dummy_datetime, i32::MAX).is_none()); + assert!(Timestamp::new_local(dummy_datetime, i32::MIN).is_none()); + } +} |
