///|
/// A PDF date with local time-zone offset components.
///
/// The fields mirror PDF's `D:YYYYMMDDHHmmSSOHH'mm'` representation. Month and
/// day default to `1` when omitted during parsing; time fields default to zero.
pub(all) struct PdfDate {
year : Int
month : Int
day : Int
hour : Int
minute : Int
second : Int
hour_offset : Int
minute_offset : Int
} derive(Debug, Eq, ToJson)
///|
fn pdf_date_check(date : PdfDate) -> Unit raise @core.PdfError {
if date.year < 0 ||
date.year > 9999 ||
date.month < 1 ||
date.month > 12 ||
date.day < 1 ||
date.day > 31 ||
date.hour < 0 ||
date.hour > 23 ||
date.minute < 0 ||
date.minute > 59 ||
date.second < 0 ||
date.second > 59 ||
date.hour_offset < -23 ||
date.hour_offset > 23 ||
date.minute_offset < -59 ||
date.minute_offset > 59 {
raise BadDate
}
}
///|
fn pdf_date_abs(value : Int) -> Int {
if value < 0 {
-value
} else {
value
}
}
///|
fn pdf_date_pad2(value : Int) -> String {
if value < 10 {
"0" + value.to_string()
} else {
value.to_string()
}
}
///|
fn pdf_date_pad4(value : Int) -> String {
if value < 10 {
"000" + value.to_string()
} else if value < 100 {
"00" + value.to_string()
} else if value < 1000 {
"0" + value.to_string()
} else {
value.to_string()
}
}
///|
/// Formats a PDF date as an ASCII string in PDF date syntax.
///
/// Raises `@core.PdfError::BadDate` if any field is outside the representable PDF
/// date ranges.
pub fn pdf_string_of_date(date : PdfDate) -> String raise @core.PdfError {
pdf_date_check(date)
let offset = if date.hour_offset < 0 {
"-" +
pdf_date_pad2(pdf_date_abs(date.hour_offset)) +
"'" +
pdf_date_pad2(pdf_date_abs(date.minute_offset)) +
"'"
} else if date.hour_offset > 0 {
"+" +
pdf_date_pad2(pdf_date_abs(date.hour_offset)) +
"'" +
pdf_date_pad2(pdf_date_abs(date.minute_offset)) +
"'"
} else {
"Z"
}
"D:" +
pdf_date_pad4(date.year) +
pdf_date_pad2(date.month) +
pdf_date_pad2(date.day) +
pdf_date_pad2(date.hour) +
pdf_date_pad2(date.minute) +
pdf_date_pad2(date.second) +
offset
}
///|
fn pdf_date_digit(byte : Byte) -> Int? {
let value = byte.to_int()
if value >= 48 && value <= 57 {
Some(value - 48)
} else {
None
}
}
///|
fn pdf_date_two_digits(
bytes : BytesView,
index : Int,
) -> Int raise @core.PdfError {
if index + 1 >= bytes.length() {
raise BadDate
}
match (pdf_date_digit(bytes[index]), pdf_date_digit(bytes[index + 1])) {
(Some(first), Some(second)) => first * 10 + second
_ => raise BadDate
}
}
///|
fn pdf_date_four_digits(
bytes : BytesView,
index : Int,
) -> Int raise @core.PdfError {
if index + 3 >= bytes.length() {
raise BadDate
}
match
(
pdf_date_digit(bytes[index]),
pdf_date_digit(bytes[index + 1]),
pdf_date_digit(bytes[index + 2]),
pdf_date_digit(bytes[index + 3]),
) {
(Some(a), Some(b), Some(c), Some(d)) => ((a * 10 + b) * 10 + c) * 10 + d
_ => raise BadDate
}
}
///|
fn pdf_date_is_local_time_start(byte : Byte) -> Bool {
let value = byte.to_int()
value == 43 || value == 45 || value == 90
}
///|
fn pdf_date_parse_optional_two(
bytes : BytesView,
index : Int,
default : Int,
hour_offset : Ref[Int],
minute_offset : Ref[Int],
offset_sign : Ref[Int],
) -> (Int, Int) raise @core.PdfError {
if index >= bytes.length() {
(default, bytes.length())
} else if pdf_date_is_local_time_start(bytes[index]) {
pdf_date_parse_local_time(
bytes, index, hour_offset, minute_offset, offset_sign,
)
(default, bytes.length())
} else if index + 1 < bytes.length() {
(pdf_date_two_digits(bytes, index), index + 2)
} else {
(default, bytes.length())
}
}
///|
fn pdf_date_parse_local_time(
bytes : BytesView,
index : Int,
hour_offset : Ref[Int],
minute_offset : Ref[Int],
offset_sign : Ref[Int],
) -> Unit raise @core.PdfError {
if index < bytes.length() {
let (sign, offset_index) = match bytes[index].to_int() {
43 => (1, index + 1)
45 => (-1, index + 1)
90 => (0, index + 1)
_ => (0, bytes.length())
}
let (hour, next_index) = pdf_date_parse_optional_two(
bytes, offset_index, 0, hour_offset, minute_offset, offset_sign,
)
if next_index < bytes.length() {
let (minute, _) = pdf_date_parse_optional_two(
bytes,
next_index + 1,
0,
hour_offset,
minute_offset,
offset_sign,
)
hour_offset.val = hour
minute_offset.val = minute
offset_sign.val = sign
}
}
}
///|
fn pdf_date_has_distiller_y2k_bug(bytes : BytesView, index : Int) -> Bool {
index + 4 < bytes.length() &&
bytes[index].to_int() == 49 &&
bytes[index + 1].to_int() == 57 &&
bytes[index + 2].to_int() == 49 &&
bytes[index + 3].to_int() == 48 &&
bytes[index + 4].to_int() == 48
}
///|
/// Parses a PDF date from a byte view.
///
/// Both strings with and without the `D:` prefix are accepted. Missing optional
/// date fields use the PDF defaults described on `PdfDate`. Raises
/// `@core.PdfError::BadDate` for malformed or out-of-range dates.
pub fn pdf_date_of_view(bytes : BytesView) -> PdfDate raise @core.PdfError {
let mut index = if bytes.length() >= 2 &&
bytes[0].to_int() == 68 &&
bytes[1].to_int() == 58 {
2
} else {
0
}
let year = if pdf_date_has_distiller_y2k_bug(bytes, index) {
index += 5
2000
} else {
let value = pdf_date_four_digits(bytes, index)
index += 4
value
}
let hour_offset = Ref::{ val: 0, }
let minute_offset = Ref::{ val: 0, }
let offset_sign = Ref::{ val: 0, }
let (month, next_index) = pdf_date_parse_optional_two(
bytes, index, 1, hour_offset, minute_offset, offset_sign,
)
index = next_index
let (day, next_index) = pdf_date_parse_optional_two(
bytes, index, 1, hour_offset, minute_offset, offset_sign,
)
index = next_index
let (hour, next_index) = pdf_date_parse_optional_two(
bytes, index, 0, hour_offset, minute_offset, offset_sign,
)
index = next_index
let (minute, next_index) = pdf_date_parse_optional_two(
bytes, index, 0, hour_offset, minute_offset, offset_sign,
)
index = next_index
let (second, next_index) = pdf_date_parse_optional_two(
bytes, index, 0, hour_offset, minute_offset, offset_sign,
)
pdf_date_parse_local_time(
bytes, next_index, hour_offset, minute_offset, offset_sign,
)
let date = PdfDate::{
year,
month,
day,
hour,
minute,
second,
hour_offset: hour_offset.val * offset_sign.val,
minute_offset: minute_offset.val * offset_sign.val,
}
pdf_date_check(date)
date
}
///|
/// Parses a PDF date from owned PDF bytes.
pub fn pdf_date_of_bytes(
bytes : @core.PdfBytes,
) -> PdfDate raise @core.PdfError {
pdf_date_of_view(bytes)
}
///|
/// Parses a PDF date from an ASCII string.
///
/// Raises `@core.PdfError::BadDate` for malformed or out-of-range dates.
pub fn pdf_date_of_string(text : String) -> PdfDate raise @core.PdfError {
let bytes = @ascii.encode(text)
pdf_date_of_view(bytes)
}
///|
/// Formats a PDF date as an owned PDF byte string.
///
/// Raises `@core.PdfError::BadDate` if any field is outside the representable PDF
/// date ranges.
pub fn pdf_bytes_of_date(date : PdfDate) -> @core.PdfBytes raise @core.PdfError {
let text = pdf_string_of_date(date)
@ascii.encode(text)
}