mirror of
https://github.com/typst/typst
synced 2025-06-08 05:06:24 +08:00
Integrate new timestamp logic
This commit is contained in:
parent
c5b1a61c7f
commit
944cd8caae
@ -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()?,
|
||||
|
@ -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,
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user