Skip to content

Commit

Permalink
Document and rename stuff
Browse files Browse the repository at this point in the history
  • Loading branch information
elegaanz committed May 17, 2024
1 parent b0d18a7 commit 2689e74
Show file tree
Hide file tree
Showing 9 changed files with 249 additions and 208 deletions.
291 changes: 144 additions & 147 deletions crates/typst-pdf/src/catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,166 +10,163 @@ use typst::foundations::{Datetime, Smart};
use typst::layout::Dir;
use typst::text::Lang;

use crate::WriteResources;
use crate::{hash_base64, outline, page::PdfPageLabel};
use crate::{FinalStep, WriteResources};

pub struct Catalog<'a> {
pub ident: Smart<&'a str>,
pub timestamp: Option<Datetime>,
}

impl<'a> FinalStep<WriteResources<'a>> for Catalog<'a> {
/// Write the document catalog.

fn run(&self, ctx: WriteResources, pdf: &mut Pdf, alloc: &mut Ref) {
let lang = ctx
.resources
.languages
.iter()
.max_by_key(|(_, &count)| count)
.map(|(&l, _)| l);

let dir = if lang.map(Lang::dir) == Some(Dir::RTL) {
Direction::R2L
} else {
Direction::L2R
};

// Write the outline tree.
let outline_root_id = outline::write_outline(pdf, alloc, &ctx);

// Write the page labels.
let page_labels = write_page_labels(pdf, alloc, &ctx);

// Write the document information.
let info_ref = alloc.bump();
let mut info = pdf.document_info(info_ref);
let mut xmp = XmpWriter::new();
if let Some(title) = &ctx.document.title {
info.title(TextStr(title));
xmp.title([(None, title.as_str())]);
}

let authors = &ctx.document.author;
if !authors.is_empty() {
// Turns out that if the authors are given in both the document
// information dictionary and the XMP metadata, Acrobat takes a little
// bit of both: The first author from the document information
// dictionary and the remaining authors from the XMP metadata.
//
// To fix this for Acrobat, we could omit the remaining authors or all
// metadata from the document information catalog (it is optional) and
// only write XMP. However, not all other tools (including Apple
// Preview) read the XMP data. This means we do want to include all
// authors in the document information dictionary.
//
// Thus, the only alternative is to fold all authors into a single
// `<rdf:li>` in the XMP metadata. This is, in fact, exactly what the
// PDF/A spec Part 1 section 6.7.3 has to say about the matter. It's a
// bit weird to not use the array (and it makes Acrobat show the author
// list in quotes), but there's not much we can do about that.
let joined = authors.join(", ");
info.author(TextStr(&joined));
xmp.creator([joined.as_str()]);
}
pub fn write_catalog(
ctx: WriteResources,
ident: Smart<&str>,
timestamp: Option<Datetime>,
pdf: &mut Pdf,
alloc: &mut Ref,
) {
let lang = ctx
.resources
.languages
.iter()
.max_by_key(|(_, &count)| count)
.map(|(&l, _)| l);

let dir = if lang.map(Lang::dir) == Some(Dir::RTL) {
Direction::R2L
} else {
Direction::L2R
};

// Write the outline tree.
let outline_root_id = outline::write_outline(pdf, alloc, &ctx);

// Write the page labels.
let page_labels = write_page_labels(pdf, alloc, &ctx);

// Write the document information.
let info_ref = alloc.bump();
let mut info = pdf.document_info(info_ref);
let mut xmp = XmpWriter::new();
if let Some(title) = &ctx.document.title {
info.title(TextStr(title));
xmp.title([(None, title.as_str())]);
}

let creator = eco_format!("Typst {}", env!("CARGO_PKG_VERSION"));
info.creator(TextStr(&creator));
xmp.creator_tool(&creator);
let authors = &ctx.document.author;
if !authors.is_empty() {
// Turns out that if the authors are given in both the document
// information dictionary and the XMP metadata, Acrobat takes a little
// bit of both: The first author from the document information
// dictionary and the remaining authors from the XMP metadata.
//
// To fix this for Acrobat, we could omit the remaining authors or all
// metadata from the document information catalog (it is optional) and
// only write XMP. However, not all other tools (including Apple
// Preview) read the XMP data. This means we do want to include all
// authors in the document information dictionary.
//
// Thus, the only alternative is to fold all authors into a single
// `<rdf:li>` in the XMP metadata. This is, in fact, exactly what the
// PDF/A spec Part 1 section 6.7.3 has to say about the matter. It's a
// bit weird to not use the array (and it makes Acrobat show the author
// list in quotes), but there's not much we can do about that.
let joined = authors.join(", ");
info.author(TextStr(&joined));
xmp.creator([joined.as_str()]);
}

let keywords = &ctx.document.keywords;
if !keywords.is_empty() {
let joined = keywords.join(", ");
info.keywords(TextStr(&joined));
xmp.pdf_keywords(&joined);
}
let creator = eco_format!("Typst {}", env!("CARGO_PKG_VERSION"));
info.creator(TextStr(&creator));
xmp.creator_tool(&creator);

if let Some(date) = ctx.document.date.unwrap_or(self.timestamp) {
let tz = ctx.document.date.is_auto();
if let Some(pdf_date) = pdf_date(date, tz) {
info.creation_date(pdf_date);
info.modified_date(pdf_date);
}
if let Some(xmp_date) = xmp_date(date, tz) {
xmp.create_date(xmp_date);
xmp.modify_date(xmp_date);
}
}
let keywords = &ctx.document.keywords;
if !keywords.is_empty() {
let joined = keywords.join(", ");
info.keywords(TextStr(&joined));
xmp.pdf_keywords(&joined);
}

info.finish();
xmp.num_pages(ctx.document.pages.len() as u32);
xmp.format("application/pdf");
xmp.language(ctx.resources.languages.keys().map(|lang| LangId(lang.as_str())));

// A unique ID for this instance of the document. Changes if anything
// changes in the frames.
let instance_id = hash_base64(&pdf.as_bytes());

// Determine the document's ID. It should be as stable as possible.
const PDF_VERSION: &str = "PDF-1.7";
let doc_id = if let Smart::Custom(ident) = self.ident {
// We were provided with a stable ID. Yay!
hash_base64(&(PDF_VERSION, ident))
} else if ctx.document.title.is_some() && !ctx.document.author.is_empty() {
// If not provided from the outside, but title and author were given, we
// compute a hash of them, which should be reasonably stable and unique.
hash_base64(&(PDF_VERSION, &ctx.document.title, &ctx.document.author))
} else {
// The user provided no usable metadata which we can use as an `/ID`.
instance_id.clone()
};

// Write IDs.
xmp.document_id(&doc_id);
xmp.instance_id(&instance_id);
pdf.set_file_id((doc_id.clone().into_bytes(), instance_id.into_bytes()));

xmp.rendition_class(RenditionClass::Proof);
xmp.pdf_version("1.7");

let xmp_buf = xmp.finish(None);
let meta_ref = alloc.bump();
pdf.stream(meta_ref, xmp_buf.as_bytes())
.pair(Name(b"Type"), Name(b"Metadata"))
.pair(Name(b"Subtype"), Name(b"XML"));

// Write the document catalog.
let catalog_ref = alloc.bump();
let mut catalog = pdf.catalog(catalog_ref);
catalog.pages(ctx.page_tree_ref);
catalog.viewer_preferences().direction(dir);
catalog.metadata(meta_ref);

// Write the named destination tree.
let mut name_dict = catalog.names();
let mut dests_name_tree = name_dict.destinations();
let mut names = dests_name_tree.names();
for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests {
names.insert(Str(name.as_str().as_bytes()), dest_ref);
if let Some(date) = ctx.document.date.unwrap_or(timestamp) {
let tz = ctx.document.date.is_auto();
if let Some(pdf_date) = pdf_date(date, tz) {
info.creation_date(pdf_date);
info.modified_date(pdf_date);
}
names.finish();
dests_name_tree.finish();
name_dict.finish();

// Insert the page labels.
if !page_labels.is_empty() {
let mut num_tree = catalog.page_labels();
let mut entries = num_tree.nums();
for (n, r) in &page_labels {
entries.insert(n.get() as i32 - 1, *r);
}
if let Some(xmp_date) = xmp_date(date, tz) {
xmp.create_date(xmp_date);
xmp.modify_date(xmp_date);
}
}

if let Some(outline_root_id) = outline_root_id {
catalog.outlines(outline_root_id);
info.finish();
xmp.num_pages(ctx.document.pages.len() as u32);
xmp.format("application/pdf");
xmp.language(ctx.resources.languages.keys().map(|lang| LangId(lang.as_str())));

// A unique ID for this instance of the document. Changes if anything
// changes in the frames.
let instance_id = hash_base64(&pdf.as_bytes());

// Determine the document's ID. It should be as stable as possible.
const PDF_VERSION: &str = "PDF-1.7";
let doc_id = if let Smart::Custom(ident) = ident {
// We were provided with a stable ID. Yay!
hash_base64(&(PDF_VERSION, ident))
} else if ctx.document.title.is_some() && !ctx.document.author.is_empty() {
// If not provided from the outside, but title and author were given, we
// compute a hash of them, which should be reasonably stable and unique.
hash_base64(&(PDF_VERSION, &ctx.document.title, &ctx.document.author))
} else {
// The user provided no usable metadata which we can use as an `/ID`.
instance_id.clone()
};

// Write IDs.
xmp.document_id(&doc_id);
xmp.instance_id(&instance_id);
pdf.set_file_id((doc_id.clone().into_bytes(), instance_id.into_bytes()));

xmp.rendition_class(RenditionClass::Proof);
xmp.pdf_version("1.7");

let xmp_buf = xmp.finish(None);
let meta_ref = alloc.bump();
pdf.stream(meta_ref, xmp_buf.as_bytes())
.pair(Name(b"Type"), Name(b"Metadata"))
.pair(Name(b"Subtype"), Name(b"XML"));

// Write the document catalog.
let catalog_ref = alloc.bump();
let mut catalog = pdf.catalog(catalog_ref);
catalog.pages(ctx.page_tree_ref);
catalog.viewer_preferences().direction(dir);
catalog.metadata(meta_ref);

// Write the named destination tree.
let mut name_dict = catalog.names();
let mut dests_name_tree = name_dict.destinations();
let mut names = dests_name_tree.names();
for &(name, dest_ref, ..) in &ctx.references.named_destinations.dests {
names.insert(Str(name.as_str().as_bytes()), dest_ref);
}
names.finish();
dests_name_tree.finish();
name_dict.finish();

// Insert the page labels.
if !page_labels.is_empty() {
let mut num_tree = catalog.page_labels();
let mut entries = num_tree.nums();
for (n, r) in &page_labels {
entries.insert(n.get() as i32 - 1, *r);
}
}

if let Some(lang) = lang {
catalog.lang(TextStr(lang.as_str()));
}
if let Some(outline_root_id) = outline_root_id {
catalog.outlines(outline_root_id);
}

catalog.finish();
if let Some(lang) = lang {
catalog.lang(TextStr(lang.as_str()));
}

catalog.finish();
}

/// Write the page labels.
Expand Down
2 changes: 1 addition & 1 deletion crates/typst-pdf/src/color_font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub fn write_color_fonts(
chunk: &mut PdfChunk,
out: &mut Output,
) -> impl Fn(&mut References) -> &mut Output {
context.resources.write(&mut |resources: &Resources| {
context.resources.traverse(&mut |resources: &Resources| {
let Some(color_fonts) = &resources.color_fonts else {
return;
};
Expand Down
2 changes: 1 addition & 1 deletion crates/typst-pdf/src/extg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub fn write_graphic_states(
chunk: &mut PdfChunk,
out: &mut Output,
) -> impl Fn(&mut References) -> &mut Output {
context.resources.write(&mut |resources| {
context.resources.traverse(&mut |resources| {
for external_gs in resources.ext_gs.items() {
if out.contains_key(external_gs) {
continue;
Expand Down
2 changes: 1 addition & 1 deletion crates/typst-pdf/src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub fn write_fonts(
chunk: &mut PdfChunk,
out: &mut Output,
) -> impl Fn(&mut References) -> &mut Output {
context.resources.write(&mut |resources| {
context.resources.traverse(&mut |resources| {
for font in resources.fonts.items() {
if out.contains_key(font) {
continue;
Expand Down
2 changes: 1 addition & 1 deletion crates/typst-pdf/src/gradient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ pub fn write_gradients(
chunk: &mut PdfChunk,
out: &mut Output,
) -> impl Fn(&mut References) -> &mut Output {
context.resources.write(&mut |resources| {
context.resources.traverse(&mut |resources| {
for pdf_gradient in resources.gradients.items().cloned().collect::<Vec<_>>() {
if out.contains_key(&pdf_gradient) {
continue;
Expand Down
2 changes: 1 addition & 1 deletion crates/typst-pdf/src/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ pub fn write_images(
chunk: &mut PdfChunk,
out: &mut Output,
) -> impl Fn(&mut References) -> &mut Output {
context.resources.write(&mut |resources| {
context.resources.traverse(&mut |resources| {
for (i, image) in resources.images.items().enumerate() {
if out.contains_key(image) {
continue;
Expand Down

0 comments on commit 2689e74

Please sign in to comment.