Fix PDF title display provenance

This commit is contained in:
Mirko Kerezovic
2026-10-23 01:01:25 +07:00
committed by bflatastic
parent 5e9df83dd4
commit d548b4ff74
9 changed files with 62 additions and 21 deletions
+4 -6
View File
@@ -73,6 +73,7 @@ fn export_with_report_inner(doc: &Document, opts: &PdfOptions, left_out: &mut Ha
let now = vectorcraft_doc::metadata::output_time();
let created_at = opts.created.or(now);
crate::pdfx::require_date(set.standard, created_at)?;
let display_doc_title = opts.display_doc_title || opts.title.as_deref().is_some_and(|title| !title.trim().is_empty());
let mut w = Writer::new(doc, set, &title, created_at, doc.color_mode == vectorcraft_doc::ColorMode::Cmyk)?;
// Preserve Editing: the native document as an embedded file (`check` refused PDF/A with it).
let mut warnings = set.warnings();
@@ -132,7 +133,7 @@ fn export_with_report_inner(doc: &Document, opts: &PdfOptions, left_out: &mut Ha
vectorcraft_doc::cancel::check().map_err(|_| PdfError::Cancelled)?;
let (layers, overprinted, invisible) = (ex.layers, ex.overprinted, ex.invisible);
w.absorb(ex);
let (bytes, more) = w.finish(left_out)?;
let (bytes, more) = w.finish(left_out, display_doc_title)?;
let bytes = crate::forms::finish(bytes, doc, set, layers, overprinted, invisible)?;
// The writer gives the file its creation date as its modification date too.
let bytes = match opts.modified.filter(|m| Some(*m) != created_at) {
@@ -233,8 +234,6 @@ pub(crate) struct Writer {
rgb_in_cmyk: RgbInCmyk,
/// A PDF 1.3 file ([`crate::PdfSettings::pdf13`]): flat, without page groups.
pdf13: bool,
/// The PDF has a document title, so viewers should display it instead of the file name.
display_doc_title: bool,
/// The document's images the exporters wrote, by key, gathered from the exporters
/// ([`Self::absorb`]): [`Self::finish`] finds the key of an image the writer couldn't decode.
images: Vec<(String, Image)>,
@@ -319,7 +318,6 @@ impl Writer {
warnings,
rgb_in_cmyk: RgbInCmyk::default(),
pdf13: set.pdf13(),
display_doc_title: !title.trim().is_empty(),
images: vec![],
})
}
@@ -428,7 +426,7 @@ impl Writer {
/// standard) and the warnings of its drawing. PDF/X-1a spot colours keep their CMYK
/// alternates. When the writer couldn't decode one of the images, its key is added to
/// `left_out` ([`leaving_out_undecodable`]) and the writer's error is returned.
pub(crate) fn finish(mut self, left_out: &mut HashSet<String>) -> Result<(Vec<u8>, Vec<String>), PdfError> {
pub(crate) fn finish(mut self, left_out: &mut HashSet<String>, display_doc_title: bool) -> Result<(Vec<u8>, Vec<String>), PdfError> {
let images = std::mem::take(&mut self.images);
let mut bytes = self.pdf.finish().map_err(|e| {
if let write::WriteError::Serialization(k) = &e
@@ -443,7 +441,7 @@ impl Writer {
}
let lab = !self.lab_spots.is_empty() && !self.out.standard.cmyk_only();
let bytes = if lab { crate::lab_spot::lab_alternates(bytes, &self.lab_spots) } else { bytes };
let (bytes, more) = self.out.write_catalog(bytes, self.display_doc_title)?;
let (bytes, more) = self.out.write_catalog(bytes, display_doc_title)?;
self.warnings.extend(more);
self.warnings.extend(self.rgb_in_cmyk.warning());
Ok((crate::pdfx::finish(bytes, self.out.standard, self.pdf13)?, self.warnings))
+2
View File
@@ -94,6 +94,8 @@ pub struct PdfOptions {
pub artboards: Option<Vec<usize>>,
/// Document title for the metadata; `None` = the document's title.
pub title: Option<String>,
/// Ask PDF viewers to display the document title instead of the file name.
pub display_doc_title: bool,
/// Creation date as Unix seconds (UTC); `None` = now (native) / omitted (wasm). PDF/A needs a date.
pub created: Option<i64>,
/// Modification date as Unix seconds (UTC); `None` = the creation date.
+1 -1
View File
@@ -426,7 +426,7 @@ fn print_inner(doc: &Document, opts: &PrintOptions, left_out: &mut HashSet<Strin
for ex in exporters {
w.absorb(ex);
}
let (bytes, more) = w.finish(left_out)?;
let (bytes, more) = w.finish(left_out, false)?;
let bytes = crate::forms::finish(bytes, doc, &pdf, false, overprinted, invisible)?;
let mut warnings = plan.warnings;
for m in more {
+2 -2
View File
@@ -902,11 +902,11 @@ fn empty_and_populated_file_info_preserve_key_presence() {
for key in ["/Title", "/Author", "/Keywords", "<dc:title>", "<dc:creator>", "<pdf:Keywords>"] {
assert!(!s.contains(key), "unexpected empty metadata {key}");
}
assert!(!s.contains("/DisplayDocTitle true"), "an untitled document has no title to display");
assert!(!s.contains("/DisplayDocTitle true"), "the default export does not request displaying its metadata title");
d.title = "Synthetic title".into();
d.metadata.author = " Test author ".into();
d.metadata.keywords = vec!["test keyword".into()];
let bytes = export(&d, &PdfOptions { created: Some(1_700_000_000), ..PdfOptions::uncompressed() }).unwrap();
let bytes = export(&d, &PdfOptions { display_doc_title: true, created: Some(1_700_000_000), ..PdfOptions::uncompressed() }).unwrap();
let s = String::from_utf8_lossy(&bytes);
for key in ["/Title", "/Author", "/Keywords", "/CreationDate", "<dc:title>", "<dc:creator>", "<pdf:Keywords>"] {
assert!(s.contains(key), "missing populated metadata {key}");