fix(ui): show creation and modification dates in Document Properties (#5534) (#5536)

This commit is contained in:
Nate Glasby authored and GitHub committed 2026-11-09 10:06:03 +08:00
1 parent 7e90730fae
commit b02765b6a1
7 files changed
+47 -5

No files matched your search

+1 -1
View File
@@ -296,7 +296,7 @@ pub(crate) fn info(d: &Document) -> Value {
// Custom document properties (Document Properties ▸ Custom): /Info entries beyond the
// ones the spec names, set and cleared with doc_set_info.
"custom": i.custom.iter().map(|(name, value)| json!({ "name": name, "value": value })).collect::<Vec<_>>(),
"creator": i.creator, "producer": i.producer,
"creator": i.creator, "producer": i.producer, "created": i.created, "modified": i.modified,
"tagged": i.tagged,
"has_javascript": i.has_javascript,
// Where the document opens (/OpenAction, named destinations resolved): 1-based page and
+5 -2
View File
@@ -763,8 +763,9 @@ pub(crate) fn same_pages(a: &[pdfcraft_render::PageInfo], b: &[pdfcraft_render::
}
/// Each document-information field of `info` with its value in `cos` (`/Info`, else XMP), read
/// the way the full inspection reads them: NULs and spaces trimmed, empty is none.
pub(crate) fn info_fields<'a>(info: &'a mut DocInfo, cos: &pdfcraft_cos::Document) -> [(&'a mut Option<String>, Option<String>); 6] {
/// the way the full inspection reads them: NULs and spaces trimmed, empty is none, dates shown as
/// [`pdfcraft_render::pretty_date`] shows them.
pub(crate) fn info_fields<'a>(info: &'a mut DocInfo, cos: &pdfcraft_cos::Document) -> [(&'a mut Option<String>, Option<String>); 8] {
let read = |key| pdfcraft_organize::info(cos, key).map(|v| v.trim_matches('\0').trim().to_string()).filter(|v| !v.is_empty());
[
(&mut info.title, read("Title")),
@@ -773,6 +774,8 @@ pub(crate) fn info_fields<'a>(info: &'a mut DocInfo, cos: &pdfcraft_cos::Documen
(&mut info.keywords, read("Keywords")),
(&mut info.creator, read("Creator")),
(&mut info.producer, read("Producer")),
(&mut info.created, read("CreationDate").map(|d| pdfcraft_render::pretty_date(&d))),
(&mut info.modified, read("ModDate").map(|d| pdfcraft_render::pretty_date(&d))),
]
}
+4
View File
@@ -53,6 +53,10 @@ pub struct DocInfo {
pub keywords: Option<String>,
pub creator: Option<String>,
pub producer: Option<String>,
/// `/Info /CreationDate` and `/ModDate` for display ("2026-10-01 12:30", see [`pretty_date`]);
/// a value that isn't a PDF date is shown as written.
pub created: Option<String>,
pub modified: Option<String>,
/// Custom document properties: `/Info` entries with a key the spec does not name and a text
/// value, as (name, text), in the dictionary's order (at most 256).
pub custom: Vec<(String, String)>,
+8
View File
@@ -188,6 +188,14 @@ impl<'a> Inspector<'a> {
info.keywords = self.text(d, b"Keywords");
info.creator = self.text(d, b"Creator");
info.producer = self.text(d, b"Producer");
// Dates are text strings only (ISO 32000-2 §7.9.4), as the engine reads them after an
// edit (`pdfcraft_organize::info`): a name value is no date.
let date = |key: &[u8]| match self.resolve(d.get(key).ok()?) {
Object::String(..) => self.text(d, key).map(|s| pretty_date(&s)),
_ => None,
};
info.created = date(b"CreationDate");
info.modified = date(b"ModDate");
const STANDARD: [&[u8]; 9] =
[b"Title", b"Author", b"Subject", b"Keywords", b"Creator", b"Producer", b"CreationDate", b"ModDate", b"Trapped"];
// The same rule as the engine's list after an edit (`pdfcraft_organize::custom_info`,
+25
View File
@@ -729,6 +729,31 @@ const CUSTOM_INFO: &[u8] = b"%PDF-1.7
trailer << /Root 1 0 R /Info 4 0 R >>
%%EOF";
/// One page whose `/Info` is `info` (the dictionary's contents).
fn with_info(info: &str) -> Vec<u8> {
format!(
"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 200 200] >> endobj
3 0 obj << /Type /Page /Parent 2 0 R >> endobj
4 0 obj << {info} >> endobj
trailer << /Root 1 0 R /Info 4 0 R >>
%%EOF"
)
.into_bytes()
}
#[test]
fn creation_and_modification_dates_are_read_for_document_properties() {
let info = inspect(Arc::new(with_info("/CreationDate (D:20240105093000+01'00') /ModDate (D:20261031174500Z)")), None).expect("opens");
assert_eq!((info.created.as_deref(), info.modified.as_deref()), (Some("2024-01-05 09:30"), Some("2026-10-31 17:45")));
// Not a PDF date: shown as written. Empty, a number, a name and absent: no date.
let info = inspect(Arc::new(with_info("/CreationDate (last Tuesday) /ModDate ()")), None).expect("opens");
assert_eq!((info.created.as_deref(), info.modified.as_deref()), (Some("last Tuesday"), None));
let info = inspect(Arc::new(with_info("/CreationDate 20240105 /ModDate /D:20240105093000Z")), None).expect("opens");
assert_eq!((info.created, info.modified), (None, None));
}
#[test]
fn custom_document_properties_are_listed_apart_from_the_standard_ones() {
let info = inspect(Arc::new(CUSTOM_INFO.to_vec()), None).expect("opens");
+2
View File
@@ -550,6 +550,8 @@ fn dialog_properties(ui: &mut egui::Ui, app: &mut PdfCraftApp, req: &mut Pending
}
row(ui, "Application", i.creator.clone().unwrap_or_default());
row(ui, "PDF producer", i.producer.clone().unwrap_or_default());
row(ui, "Created", i.created.clone().unwrap_or_default());
row(ui, "Modified", i.modified.clone().unwrap_or_default());
}
PropsTab::InitialView => {
use pdfcraft_engine::{InitialLayout as L, Magnification as M, Navigation as N};
+2 -2
View File
@@ -3538,8 +3538,8 @@ milestone = "M3"
status = "shipped"
commands = ["file.properties"]
tools = ["doc_info"]
evidence = ["crates/ui-egui/tests/it/editing.rs::document_properties_edit_is_one_undoable_step", "crates/ui-egui/tests/it/editing.rs::cancelling_properties_discards_the_draft", "crates/organize/src/tests.rs::document_info_falls_back_to_the_xmp_packet", "crates/engine/src/tests.rs::document_information_stated_only_in_xmp_is_shown", "crates/ui-egui/tests/it/dialog_chrome.rs::document_properties_values_line_up_and_long_fields_wrap", "crates/ui-egui/tests/it/dialog_chrome.rs::space_audit_tab_measures_the_file_and_stays_in_the_card"]
notes = "Description, Initial View, Security, Fonts, Custom (organize.custom-properties) and Advanced, plus a Space Audit tab Acrobat does not have (its audit is only in the PDF Optimizer); the audit runs when the tab opens and again after an edit. Title, Author, Subject and Keywords (and Creator, Producer in doc_info) come from /Info, else from the XMP packet (dc:title, dc:creator, dc:description, pdf:Keywords, xmp:CreatorTool, pdf:Producer) for files that state them only there; edits write both. Missing: a UTF-16 XMP packet isn't read, and /Info kept only in an older revision's trailer isn't looked up"
evidence = ["crates/ui-egui/tests/it/editing.rs::document_properties_edit_is_one_undoable_step", "crates/ui-egui/tests/it/editing.rs::cancelling_properties_discards_the_draft", "crates/organize/src/tests.rs::document_info_falls_back_to_the_xmp_packet", "crates/engine/src/tests.rs::document_information_stated_only_in_xmp_is_shown", "crates/ui-egui/tests/it/dialog_chrome.rs::document_properties_values_line_up_and_long_fields_wrap", "crates/ui-egui/tests/it/dialog_chrome.rs::space_audit_tab_measures_the_file_and_stays_in_the_card", "crates/render/src/inspect/tests.rs::creation_and_modification_dates_are_read_for_document_properties"]
notes = "Description, Initial View, Security, Fonts, Custom (organize.custom-properties) and Advanced, plus a Space Audit tab Acrobat does not have (its audit is only in the PDF Optimizer); the audit runs when the tab opens and again after an edit. Title, Author, Subject and Keywords (and Creator, Producer in doc_info) come from /Info, else from the XMP packet (dc:title, dc:creator, dc:description, pdf:Keywords, xmp:CreatorTool, pdf:Producer) for files that state them only there; edits write both. Description and doc_info show the /Info CreationDate and ModDate as written (YYYY-MM-DD HH:MM, not converted to local time; #5534). Missing: the dates from XMP (xmp:CreateDate, xmp:ModifyDate) when /Info has none, editing them, a UTF-16 XMP packet isn't read, and /Info kept only in an older revision's trailer isn't looked up"
[[feature]]
id = "organize.edit-metadata"