PDF/.ai import: PDF and .ai files open with the File Info they carry

A PDF, PDF-compatible .ai or .ait opened from its pages had an empty
File Info. Nothing read the file's document information
dictionary or its XMP packet, so file.info reported no author,
description or keywords, even for a PDF that VectorCraft exported.

vectorcraft_pdf::file_info reads both. The author, description,
keywords, copyright status, notice and URL and the created and modified
dates come from the XMP packet (dc:creator, dc:description, dc:subject
or pdf:Keywords, xmpRights:Marked, dc:rights, xmpRights:WebStatement,
xmp:CreateDate, xmp:ModifyDate), else from the dictionary's Author,
Subject, Keywords, CreationDate and ModDate. document.open,
file.newFromTemplate and file.revert put each value through file.info's
checks and leave out a value that file.info refuses. A packet that is
not well-formed XML leaves the dictionary's values. File Info does not
set the title, and a file restored from the document it carries keeps
that document's File Info. vectorcraft-pdf depends on roxmltree 0.21,
which usvg already brings into Cargo.lock.

Docs: docs/mcp.md (File Info), docs/file-format-parity.md,
docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-16 06:16:34 +07:00
committed by bflatastic
parent d1089270ba
commit 261b64251d
12 changed files with 620 additions and 6 deletions
+26 -1
View File
@@ -1,6 +1,7 @@
//! File → File Info (`file.info`): the document title and its descriptive metadata (author,
//! description, keywords, rating, copyright), with the read-only created and modified dates. The
//! SVG (with `metadata`), PDF and PNG writers carry it.
//! SVG (with `metadata`), PDF and PNG writers carry it, and a PDF or `.ai` opens with the File Info
//! it gives ([`read_pdf`]).
use serde_json::{Map, Value, json};
use vectorcraft_doc::metadata::{MAX_RATING, iso8601};
@@ -62,6 +63,30 @@ pub(crate) fn file_info(s: &mut Session, p: &Value) -> Result<Value> {
Ok(info_json(&s.doc()?.doc))
}
/// The File Info a PDF or PDF-compatible `.ai` gives ([`vectorcraft_pdf::file_info`]) into `m`: each
/// field that `file.info` would take (a value it refuses is left out), and the created and
/// modified dates.
pub(crate) fn read_pdf(m: &mut DocMetadata, f: vectorcraft_pdf::FileInfo) {
let status = f.copyrighted.map(|c| if c { CopyrightStatus::Copyrighted } else { CopyrightStatus::PublicDomain });
let fields = [
("author", f.author.map(Value::from)),
("description", f.description.map(Value::from)),
("keywords", f.keywords.map(Value::from)),
("copyrightStatus", status.map(|s| Value::from(s.id()))),
("copyrightNotice", f.copyright_notice.map(Value::from)),
("copyrightUrl", f.copyright_url.map(Value::from)),
];
// `apply` changes the field only when it takes the value.
let mut title = String::new();
for (k, v) in fields {
if let Some(v) = v {
let _ = apply(&mut title, m, k, &v);
}
}
m.created = f.created.or(m.created);
m.modified = f.modified.or(m.modified);
}
fn apply(title: &mut String, m: &mut DocMetadata, k: &str, v: &Value) -> Result<()> {
match k {
"title" => {
+8
View File
@@ -254,6 +254,14 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
_ if format.raster => (raster_doc(&title, bytes)?, vec![], false),
_ => return Err(err(format!("{} files can't be opened yet", format.label))),
};
// A PDF, `.ai` or `.ait` read from its pages or its editing copy, not restored from the document
// it carries, opens with the File Info it gives (a PostScript `.ai` gives none).
if matches!(format.id, "pdf" | "ai" | "ait")
&& !restored
&& let Some(info) = vectorcraft_pdf::file_info(bytes, opts.password.as_deref())
{
super::super::fileinfo::read_pdf(&mut doc.metadata, info);
}
// Threaded type read from a file flows through its frames.
if !doc.text_threads.is_empty() && !restored {
crate::cmd::threads::reflow(&Document::new(1.0, 1.0), &mut doc);