Integrate new timestamp logic

This commit is contained in:
Laurenz Stampfl 2024-12-17 13:58:04 +01:00
parent c5b1a61c7f
commit 944cd8caae
5 changed files with 140 additions and 27 deletions

View File

@ -17,7 +17,7 @@ use typst::html::HtmlDocument;
use typst::layout::{Frame, Page, PageRanges, PagedDocument};
use typst::syntax::{FileId, Source, Span};
use typst::WorldExt;
use typst_pdf::{PdfOptions, Validator};
use typst_pdf::{PdfOptions, Timestamp, Validator};
use crate::args::{
CompileArgs, CompileCommand, DiagnosticFormat, Input, Output, OutputFormat,
@ -261,11 +261,24 @@ fn export_paged(document: &PagedDocument, config: &CompileConfig) -> SourceResul
/// Export to a PDF.
fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<()> {
// If the timestamp is provided through the CLI, use UTC suffix,
// else, use the current local time and timezone.
let timestamp = match config.creation_timestamp {
Some(timestamp) => convert_datetime(timestamp).map(Timestamp::new_utc),
None => {
let local_datetime = chrono::Local::now();
convert_datetime(local_datetime).and_then(|datetime| {
Timestamp::new_local(
datetime,
local_datetime.offset().local_minus_utc() / 60,
)
})
}
};
let options = PdfOptions {
ident: Smart::Auto,
timestamp: convert_datetime(
config.creation_timestamp.unwrap_or_else(chrono::Utc::now),
),
timestamp,
page_ranges: config.pages.clone(),
pdf_version: config.pdf_version.map(|v| match v {
PdfVersion::V_1_4 => typst_pdf::PdfVersion::Pdf14,
@ -294,7 +307,9 @@ fn export_pdf(document: &PagedDocument, config: &CompileConfig) -> SourceResult<
}
/// Convert [`chrono::DateTime`] to [`Datetime`]
fn convert_datetime(date_time: chrono::DateTime<chrono::Utc>) -> Option<Datetime> {
fn convert_datetime<Tz: chrono::TimeZone>(
date_time: chrono::DateTime<Tz>,
) -> Option<Datetime> {
Datetime::from_ymd_hms(
date_time.year(),
date_time.month().try_into().ok()?,

View File

@ -4,7 +4,6 @@ use crate::page::PageLabelExt;
use crate::util::{display_font, AbsExt, PointExt, SizeExt, TransformExt};
use crate::{paint, PdfOptions};
use bytemuck::TransparentWrapper;
use ecow::EcoString;
use krilla::action::{Action, LinkAction};
use krilla::annotation::{LinkAnnotation, Target};
use krilla::destination::{NamedDestination, XyzDestination};
@ -21,7 +20,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
use std::ops::Range;
use std::sync::Arc;
use typst_library::diag::{bail, SourceResult};
use typst_library::foundations::{Datetime, NativeElement};
use typst_library::foundations::NativeElement;
use typst_library::introspection::Location;
use typst_library::layout::{
Abs, Frame, FrameItem, GroupItem, PagedDocument, Point, Size, Transform,

View File

@ -38,9 +38,9 @@ pub struct PdfOptions<'a> {
/// `Auto`, a hash of the document's title and author is used instead (which
/// is reasonably unique and stable).
pub ident: Smart<&'a str>,
/// If not `None`, shall be the creation date of the document as a UTC
/// datetime. It will only be used if `set document(date: ..)` is `auto`.
pub timestamp: Option<Datetime>,
/// If not `None`, shall be the creation timestamp of the document. It will
/// only be used if `set document(date: ..)` is `auto`.
pub timestamp: Option<Timestamp>,
/// Specifies which ranges of pages should be exported in the PDF. When
/// `None`, all pages should be exported.
pub page_ranges: Option<PageRanges>,
@ -49,3 +49,86 @@ pub struct PdfOptions<'a> {
/// A standard the PDF should conform to.
pub validator: Validator,
}
/// 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());
}
}

View File

@ -1,8 +1,9 @@
use ecow::EcoString;
use krilla::metadata::Metadata;
use typst_library::foundations::Datetime;
use typst_library::foundations::{Datetime, Smart};
use crate::krilla::GlobalContext;
use crate::Timezone;
pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata {
let creator = format!("Typst {}", env!("CARGO_PKG_VERSION"));
@ -30,22 +31,29 @@ pub(crate) fn build_metadata(gc: &GlobalContext) -> Metadata {
metadata = metadata.subject(ident.to_string());
}
let tz = gc.document.info.date.is_auto();
if let Some(date) = gc
.document
.info
.date
.unwrap_or(gc.options.timestamp)
.and_then(|d| convert_date(d, tz))
{
// (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.modification_date(date).creation_date(date);
}
metadata
}
// TODO: Sync with recent PR
fn convert_date(datetime: Datetime, tz: bool) -> Option<krilla::metadata::DateTime> {
fn convert_date(
datetime: Datetime,
tz: Option<Timezone>,
) -> Option<krilla::metadata::DateTime> {
let year = datetime.year().filter(|&y| y >= 0)? as u16;
let mut krilla_date = krilla::metadata::DateTime::new(year);
@ -70,8 +78,16 @@ fn convert_date(datetime: Datetime, tz: bool) -> Option<krilla::metadata::DateTi
krilla_date = krilla_date.second(s);
}
if tz {
krilla_date = krilla_date.utc_offset_hour(0).utc_offset_minute(0);
match tz {
Some(Timezone::UTC) => {
krilla_date = krilla_date.utc_offset_hour(0).utc_offset_minute(0)
}
Some(Timezone::Local { hour_offset, minute_offset }) => {
krilla_date = krilla_date
.utc_offset_hour(hour_offset)
.utc_offset_minute(minute_offset)
}
None => {}
}
Some(krilla_date)

View File

@ -8,8 +8,8 @@ use krilla::surface::Surface;
use typst_library::diag::SourceResult;
use typst_library::layout::{Abs, Angle, Quadrant, Ratio, Transform};
use typst_library::visualize::{
Color, ColorSpace, DashPattern, FillRule, FixedStroke, Gradient, Paint, Pattern,
RatioOrAngle, RelativeTo, WeightedColor,
Color, ColorSpace, DashPattern, FillRule, FixedStroke, Gradient, Paint, RatioOrAngle,
RelativeTo, Tiling, WeightedColor,
};
use typst_utils::Numeric;
@ -94,13 +94,13 @@ fn paint(
Ok((p, alpha))
}
Paint::Gradient(g) => Ok(convert_gradient(g, on_text, transforms)),
Paint::Pattern(p) => convert_pattern(gc, p, on_text, surface, transforms),
Paint::Tiling(p) => convert_pattern(gc, p, on_text, surface, transforms),
}
}
pub(crate) fn convert_pattern(
gc: &mut GlobalContext,
pattern: &Pattern,
pattern: &Tiling,
on_text: bool,
surface: &mut Surface,
mut transforms: Transforms,