use std::env;
use chrono::{DateTime, TimeZone, Utc};
use fontdrasil::orchestration::{Access, AccessBuilder, Work};
use fontir::orchestration::WorkId as FeWorkId;
use log::warn;
use write_fonts::{
tables::{
head::{Flags, Head, MacStyle},
loca::LocaFormat,
os2::SelectionFlags,
},
types::{Fixed, LongDateTime},
};
use crate::{
error::Error,
orchestration::{AnyWorkId, BeWork, Context, WorkId},
};
#[derive(Debug)]
struct HeadWork {}
pub fn create_head_work() -> Box<BeWork> {
Box::new(HeadWork {})
}
const MACINTOSH_EPOCH: i64 = -2082844800;
fn seconds_since_mac_epoch(datetime: DateTime<Utc>) -> i64 {
let mac_epoch = Utc.timestamp_opt(MACINTOSH_EPOCH, 0).unwrap();
datetime.signed_duration_since(mac_epoch).num_seconds()
}
fn current_timestamp() -> i64 {
let mut src_date = None;
if let Ok(src_date_var) = env::var("SOURCE_DATE_EPOCH") {
if let Ok(timestamp) = src_date_var.parse::<i64>() {
src_date = Utc.timestamp_opt(timestamp, 0).single();
};
if src_date.is_none() {
warn!("Invalid SOURCE_DATE_EPOCH value: {src_date_var:?}. Falling back to Utc::now().",);
}
}
seconds_since_mac_epoch(src_date.unwrap_or_else(Utc::now))
}
fn init_head(
units_per_em: u16,
loca_format: LocaFormat,
flags: Flags,
lowest_rec_ppem: u16,
) -> Head {
Head {
units_per_em,
lowest_rec_ppem,
flags,
index_to_loc_format: match loca_format {
LocaFormat::Short => 0,
LocaFormat::Long => 1,
},
..Default::default()
}
}
fn apply_font_revision(head: &mut Head, major: i32, minor: u32) {
let major = major as f64;
let minor = if minor != 0 {
(minor as f64) * 0.001
} else {
0.0
};
head.font_revision = Fixed::from_f64(major + minor);
}
fn apply_macstyle(head: &mut Head, selection_flags: SelectionFlags) {
head.mac_style = MacStyle::empty();
if selection_flags.contains(SelectionFlags::BOLD) {
head.mac_style |= MacStyle::BOLD;
}
if selection_flags.contains(SelectionFlags::ITALIC) {
head.mac_style |= MacStyle::ITALIC;
}
}
fn apply_created_modified(head: &mut Head, created: Option<DateTime<Utc>>) {
let now = current_timestamp();
head.created = LongDateTime::new(created.map(seconds_since_mac_epoch).unwrap_or(now));
head.modified = LongDateTime::new(now);
}
impl Work<Context, AnyWorkId, Error> for HeadWork {
fn id(&self) -> AnyWorkId {
WorkId::Head.into()
}
fn read_access(&self) -> Access<AnyWorkId> {
AccessBuilder::new()
.variant(FeWorkId::StaticMetadata)
.variant(WorkId::Glyf)
.variant(WorkId::LocaFormat)
.build()
}
fn exec(&self, context: &Context) -> Result<(), Error> {
let static_metadata = context.ir.static_metadata.get();
let loca_format = (*context.loca_format.get().as_ref()).into();
let mut head = init_head(
static_metadata.units_per_em,
loca_format,
static_metadata.misc.head_flags,
static_metadata.misc.lowest_rec_ppm,
);
apply_font_revision(
&mut head,
static_metadata.misc.version_major,
static_metadata.misc.version_minor,
);
apply_created_modified(&mut head, static_metadata.misc.created);
apply_macstyle(&mut head, static_metadata.misc.selection_flags);
context.head.set(head);
Ok(())
}
}
#[cfg(test)]
mod tests {
use chrono::{TimeZone, Utc};
use more_asserts::assert_ge;
use temp_env;
use write_fonts::{
tables::{
head::{Flags, MacStyle},
loca::LocaFormat,
os2::SelectionFlags,
},
types::Fixed,
};
use crate::head::{apply_created_modified, apply_macstyle};
use super::{apply_font_revision, init_head, seconds_since_mac_epoch};
const DEFAULT_HEAD_FLAGS: Flags = Flags::BASELINE_AT_Y_0.union(Flags::LSB_AT_X_0);
#[test]
fn init_head_simple() {
temp_env::with_var_unset("SOURCE_DATE_EPOCH", || {
let now = seconds_since_mac_epoch(Utc::now());
let mut head = init_head(1000, LocaFormat::Long, DEFAULT_HEAD_FLAGS, 42);
apply_created_modified(&mut head, None);
assert_eq!(head.units_per_em, 1000);
assert_eq!(head.index_to_loc_format, 1);
assert_ge!(head.created.as_secs(), now);
assert_ge!(head.modified.as_secs(), now);
assert_eq!(head.lowest_rec_ppem, 42);
});
}
#[test]
fn init_head_with_valid_source_date_epoch() {
let source_date = Utc
.with_ymd_and_hms(1904, 1, 1, 0, 0, 0)
.unwrap()
.timestamp();
temp_env::with_var("SOURCE_DATE_EPOCH", Some(source_date.to_string()), || {
let mut head = init_head(1000, LocaFormat::Short, DEFAULT_HEAD_FLAGS, 42);
apply_created_modified(&mut head, None);
assert_eq!(head.created.as_secs(), 0);
assert_eq!(head.modified.as_secs(), 0);
});
}
#[test]
fn init_head_with_invalid_source_date_epoch() {
let now = seconds_since_mac_epoch(Utc::now());
temp_env::with_var(
"SOURCE_DATE_EPOCH",
Some("I am not a Unix timestamp!"),
|| {
let mut head = init_head(1000, LocaFormat::Short, DEFAULT_HEAD_FLAGS, 42);
apply_created_modified(&mut head, None);
assert_ge!(head.created.as_secs(), now);
assert_ge!(head.modified.as_secs(), now);
},
);
}
#[test]
fn apply_head_macstyle() {
let mut head = init_head(1000, LocaFormat::Long, DEFAULT_HEAD_FLAGS, 42);
apply_macstyle(&mut head, SelectionFlags::ITALIC);
assert_eq!(head.mac_style, MacStyle::ITALIC);
apply_macstyle(&mut head, SelectionFlags::BOLD);
assert_eq!(head.mac_style, MacStyle::BOLD);
apply_macstyle(&mut head, SelectionFlags::BOLD | SelectionFlags::ITALIC);
assert_eq!(head.mac_style, MacStyle::BOLD | MacStyle::ITALIC);
}
#[test]
fn minor_version_thousandths() {
let mut head = init_head(1000, LocaFormat::Long, DEFAULT_HEAD_FLAGS, 42);
apply_font_revision(&mut head, 42, 42);
assert_eq!(Fixed::from_f64(42.042), head.font_revision);
}
}