Refactor PDF export (#4154)

Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
Ana Gelez 2024-05-29 15:01:11 +02:00 committed by GitHub
parent 6d07f702e1
commit 2946cde6fa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 2909 additions and 2052 deletions

View File

@ -0,0 +1,278 @@
use std::num::NonZeroUsize;
use ecow::eco_format;
use pdf_writer::{
types::Direction, writers::PageLabel, Finish, Name, Pdf, Ref, Str, TextStr,
};
use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter};
use typst::foundations::{Datetime, Smart};
use typst::layout::Dir;
use typst::text::Lang;
use crate::WithEverything;
use crate::{hash_base64, outline, page::PdfPageLabel};
/// Write the document catalog.
pub fn write_catalog(
ctx: WithEverything,
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 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 creator = eco_format!("Typst {}", env!("CARGO_PKG_VERSION"));
info.creator(TextStr(&creator));
xmp.creator_tool(&creator);
let keywords = &ctx.document.keywords;
if !keywords.is_empty() {
let joined = keywords.join(", ");
info.keywords(TextStr(&joined));
xmp.pdf_keywords(&joined);
}
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);
}
if let Some(xmp_date) = xmp_date(date, tz) {
xmp.create_date(xmp_date);
xmp.modify_date(xmp_date);
}
}
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(outline_root_id) = outline_root_id {
catalog.outlines(outline_root_id);
}
if let Some(lang) = lang {
catalog.lang(TextStr(lang.as_str()));
}
catalog.finish();
}
/// Write the page labels.
pub(crate) fn write_page_labels(
chunk: &mut Pdf,
alloc: &mut Ref,
ctx: &WithEverything,
) -> Vec<(NonZeroUsize, Ref)> {
// If there is no exported page labeled, we skip the writing
if !ctx.pages.iter().filter_map(Option::as_ref).any(|p| {
p.label
.as_ref()
.is_some_and(|l| l.prefix.is_some() || l.style.is_some())
}) {
return Vec::new();
}
let mut result = vec![];
let empty_label = PdfPageLabel::default();
let mut prev: Option<&PdfPageLabel> = None;
// Skip non-exported pages for numbering.
for (i, page) in ctx.pages.iter().filter_map(Option::as_ref).enumerate() {
let nr = NonZeroUsize::new(1 + i).unwrap();
// If there are pages with empty labels between labeled pages, we must
// write empty PageLabel entries.
let label = page.label.as_ref().unwrap_or(&empty_label);
if let Some(pre) = prev {
if label.prefix == pre.prefix
&& label.style == pre.style
&& label.offset == pre.offset.map(|n| n.saturating_add(1))
{
prev = Some(label);
continue;
}
}
let id = alloc.bump();
let mut entry = chunk.indirect(id).start::<PageLabel>();
// Only add what is actually provided. Don't add empty prefix string if
// it wasn't given for example.
if let Some(prefix) = &label.prefix {
entry.prefix(TextStr(prefix));
}
if let Some(style) = label.style {
entry.style(style.to_pdf_numbering_style());
}
if let Some(offset) = label.offset {
entry.offset(offset.get() as i32);
}
result.push((nr, id));
prev = Some(label);
}
result
}
/// Converts a datetime to a pdf-writer date.
fn pdf_date(datetime: Datetime, tz: bool) -> Option<pdf_writer::Date> {
let year = datetime.year().filter(|&y| y >= 0)? as u16;
let mut pdf_date = pdf_writer::Date::new(year);
if let Some(month) = datetime.month() {
pdf_date = pdf_date.month(month);
}
if let Some(day) = datetime.day() {
pdf_date = pdf_date.day(day);
}
if let Some(h) = datetime.hour() {
pdf_date = pdf_date.hour(h);
}
if let Some(m) = datetime.minute() {
pdf_date = pdf_date.minute(m);
}
if let Some(s) = datetime.second() {
pdf_date = pdf_date.second(s);
}
if tz {
pdf_date = pdf_date.utc_offset_hour(0).utc_offset_minute(0);
}
Some(pdf_date)
}
/// Converts a datetime to an xmp-writer datetime.
fn xmp_date(datetime: Datetime, tz: bool) -> Option<xmp_writer::DateTime> {
let year = datetime.year().filter(|&y| y >= 0)? as u16;
Some(DateTime {
year,
month: datetime.month(),
day: datetime.day(),
hour: datetime.hour(),
minute: datetime.minute(),
second: datetime.second(),
timezone: if tz { Some(Timezone::Utc) } else { None },
})
}

View File

@ -1,10 +1,8 @@
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use pdf_writer::types::DeviceNSubtype; use pdf_writer::{types::DeviceNSubtype, writers, Chunk, Dict, Filter, Name, Ref};
use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref};
use typst::visualize::{Color, ColorSpace, Paint}; use typst::visualize::{Color, ColorSpace, Paint};
use crate::deflate; use crate::{content, deflate, PdfChunk, Renumber, WithResources};
use crate::page::{PageContext, Transforms};
// The names of the color spaces. // The names of the color spaces.
pub const SRGB: Name<'static> = Name(b"srgb"); pub const SRGB: Name<'static> = Name(b"srgb");
@ -30,118 +28,166 @@ static OKLAB_DEFLATED: Lazy<Vec<u8>> =
/// The color spaces present in the PDF document /// The color spaces present in the PDF document
#[derive(Default)] #[derive(Default)]
pub struct ColorSpaces { pub struct ColorSpaces {
oklab: Option<Ref>, use_oklab: bool,
srgb: Option<Ref>, use_srgb: bool,
d65_gray: Option<Ref>, use_d65_gray: bool,
use_linear_rgb: bool, use_linear_rgb: bool,
} }
impl ColorSpaces { impl ColorSpaces {
/// Get a reference to the oklab color space. /// Mark a color space as used.
/// pub fn mark_as_used(&mut self, color_space: ColorSpace) {
/// # Warning
/// The A and B components of the color must be offset by +0.4 before being
/// encoded into the PDF file.
pub fn oklab(&mut self, alloc: &mut Ref) -> Ref {
*self.oklab.get_or_insert_with(|| alloc.bump())
}
/// Get a reference to the srgb color space.
pub fn srgb(&mut self, alloc: &mut Ref) -> Ref {
*self.srgb.get_or_insert_with(|| alloc.bump())
}
/// Get a reference to the gray color space.
pub fn d65_gray(&mut self, alloc: &mut Ref) -> Ref {
*self.d65_gray.get_or_insert_with(|| alloc.bump())
}
/// Mark linear RGB as used.
pub fn linear_rgb(&mut self) {
self.use_linear_rgb = true;
}
/// Write the color space on usage.
pub fn write(
&mut self,
color_space: ColorSpace,
writer: writers::ColorSpace,
alloc: &mut Ref,
) {
match color_space { match color_space {
ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => { ColorSpace::Oklch | ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => {
let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]); self.use_oklab = true;
self.write(ColorSpace::LinearRgb, oklab.alternate_color_space(), alloc); self.use_linear_rgb = true;
oklab.tint_ref(self.oklab(alloc)); }
oklab.attrs().subtype(DeviceNSubtype::DeviceN); ColorSpace::Srgb => {
self.use_srgb = true;
}
ColorSpace::D65Gray => {
self.use_d65_gray = true;
} }
ColorSpace::Oklch => self.write(ColorSpace::Oklab, writer, alloc),
ColorSpace::Srgb => writer.icc_based(self.srgb(alloc)),
ColorSpace::D65Gray => writer.icc_based(self.d65_gray(alloc)),
ColorSpace::LinearRgb => { ColorSpace::LinearRgb => {
writer.cal_rgb( self.use_linear_rgb = true;
[0.9505, 1.0, 1.0888],
None,
Some([1.0, 1.0, 1.0]),
Some([
0.4124, 0.2126, 0.0193, 0.3576, 0.715, 0.1192, 0.1805, 0.0722,
0.9505,
]),
);
} }
ColorSpace::Cmyk => writer.device_cmyk(), ColorSpace::Cmyk => {}
} }
} }
// Write the color spaces to the PDF file. /// Write the color spaces to the PDF file.
pub fn write_color_spaces(&mut self, mut spaces: Dict, alloc: &mut Ref) { pub fn write_color_spaces(&self, mut spaces: Dict, refs: &ColorFunctionRefs) {
if self.oklab.is_some() { if self.use_oklab {
self.write(ColorSpace::Oklab, spaces.insert(OKLAB).start(), alloc); write(ColorSpace::Oklab, spaces.insert(OKLAB).start(), refs);
} }
if self.srgb.is_some() { if self.use_srgb {
self.write(ColorSpace::Srgb, spaces.insert(SRGB).start(), alloc); write(ColorSpace::Srgb, spaces.insert(SRGB).start(), refs);
} }
if self.d65_gray.is_some() { if self.use_d65_gray {
self.write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), alloc); write(ColorSpace::D65Gray, spaces.insert(D65_GRAY).start(), refs);
} }
if self.use_linear_rgb { if self.use_linear_rgb {
self.write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), alloc); write(ColorSpace::LinearRgb, spaces.insert(LINEAR_SRGB).start(), refs);
} }
} }
/// Write the necessary color spaces functions and ICC profiles to the /// Write the necessary color spaces functions and ICC profiles to the
/// PDF file. /// PDF file.
pub fn write_functions(&self, chunk: &mut Chunk) { pub fn write_functions(&self, chunk: &mut Chunk, refs: &ColorFunctionRefs) {
// Write the Oklab function & color space. // Write the Oklab function & color space.
if let Some(oklab) = self.oklab { if self.use_oklab {
chunk chunk
.post_script_function(oklab, &OKLAB_DEFLATED) .post_script_function(refs.oklab.unwrap(), &OKLAB_DEFLATED)
.domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) .domain([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
.range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
.filter(Filter::FlateDecode); .filter(Filter::FlateDecode);
} }
// Write the sRGB color space. // Write the sRGB color space.
if let Some(srgb) = self.srgb { if self.use_srgb {
chunk chunk
.icc_profile(srgb, &SRGB_ICC_DEFLATED) .icc_profile(refs.srgb.unwrap(), &SRGB_ICC_DEFLATED)
.n(3) .n(3)
.range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0]) .range([0.0, 1.0, 0.0, 1.0, 0.0, 1.0])
.filter(Filter::FlateDecode); .filter(Filter::FlateDecode);
} }
// Write the gray color space. // Write the gray color space.
if let Some(gray) = self.d65_gray { if self.use_d65_gray {
chunk chunk
.icc_profile(gray, &GRAY_ICC_DEFLATED) .icc_profile(refs.d65_gray.unwrap(), &GRAY_ICC_DEFLATED)
.n(1) .n(1)
.range([0.0, 1.0]) .range([0.0, 1.0])
.filter(Filter::FlateDecode); .filter(Filter::FlateDecode);
} }
} }
/// Merge two color space usage information together: a given color space is
/// considered to be used if it is used on either side.
pub fn merge(&mut self, other: &Self) {
self.use_d65_gray |= other.use_d65_gray;
self.use_linear_rgb |= other.use_linear_rgb;
self.use_oklab |= other.use_oklab;
self.use_srgb |= other.use_srgb;
}
}
/// Write the color space.
pub fn write(
color_space: ColorSpace,
writer: writers::ColorSpace,
refs: &ColorFunctionRefs,
) {
match color_space {
ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => {
let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]);
write(ColorSpace::LinearRgb, oklab.alternate_color_space(), refs);
oklab.tint_ref(refs.oklab.unwrap());
oklab.attrs().subtype(DeviceNSubtype::DeviceN);
}
ColorSpace::Oklch => write(ColorSpace::Oklab, writer, refs),
ColorSpace::Srgb => writer.icc_based(refs.srgb.unwrap()),
ColorSpace::D65Gray => writer.icc_based(refs.d65_gray.unwrap()),
ColorSpace::LinearRgb => {
writer.cal_rgb(
[0.9505, 1.0, 1.0888],
None,
Some([1.0, 1.0, 1.0]),
Some([
0.4124, 0.2126, 0.0193, 0.3576, 0.715, 0.1192, 0.1805, 0.0722, 0.9505,
]),
);
}
ColorSpace::Cmyk => writer.device_cmyk(),
}
}
/// Global references for color conversion functions.
///
/// These functions are only written once (at most, they are not written if not
/// needed) in the final document, and be shared by all color space
/// dictionaries.
pub struct ColorFunctionRefs {
oklab: Option<Ref>,
srgb: Option<Ref>,
d65_gray: Option<Ref>,
}
impl Renumber for ColorFunctionRefs {
fn renumber(&mut self, offset: i32) {
if let Some(r) = &mut self.oklab {
r.renumber(offset);
}
if let Some(r) = &mut self.srgb {
r.renumber(offset);
}
if let Some(r) = &mut self.d65_gray {
r.renumber(offset);
}
}
}
/// Allocate all necessary [`ColorFunctionRefs`].
pub fn alloc_color_functions_refs(
context: &WithResources,
) -> (PdfChunk, ColorFunctionRefs) {
let mut chunk = PdfChunk::new();
let mut used_color_spaces = ColorSpaces::default();
context.resources.traverse(&mut |r| {
used_color_spaces.merge(&r.colors);
});
let refs = ColorFunctionRefs {
oklab: if used_color_spaces.use_oklab { Some(chunk.alloc()) } else { None },
srgb: if used_color_spaces.use_srgb { Some(chunk.alloc()) } else { None },
d65_gray: if used_color_spaces.use_d65_gray { Some(chunk.alloc()) } else { None },
};
(chunk, refs)
} }
/// This function removes comments, line spaces and carriage returns from a /// This function removes comments, line spaces and carriage returns from a
@ -202,14 +248,29 @@ impl ColorEncode for ColorSpace {
/// Encodes a paint into either a fill or stroke color. /// Encodes a paint into either a fill or stroke color.
pub(super) trait PaintEncode { pub(super) trait PaintEncode {
/// Set the paint as the fill color. /// Set the paint as the fill color.
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms); fn set_as_fill(
&self,
ctx: &mut content::Builder,
on_text: bool,
transforms: content::Transforms,
);
/// Set the paint as the stroke color. /// Set the paint as the stroke color.
fn set_as_stroke(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms); fn set_as_stroke(
&self,
ctx: &mut content::Builder,
on_text: bool,
transforms: content::Transforms,
);
} }
impl PaintEncode for Paint { impl PaintEncode for Paint {
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) { fn set_as_fill(
&self,
ctx: &mut content::Builder,
on_text: bool,
transforms: content::Transforms,
) {
match self { match self {
Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms), Self::Solid(c) => c.set_as_fill(ctx, on_text, transforms),
Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms), Self::Gradient(gradient) => gradient.set_as_fill(ctx, on_text, transforms),
@ -219,9 +280,9 @@ impl PaintEncode for Paint {
fn set_as_stroke( fn set_as_stroke(
&self, &self,
ctx: &mut PageContext, ctx: &mut content::Builder,
on_text: bool, on_text: bool,
transforms: Transforms, transforms: content::Transforms,
) { ) {
match self { match self {
Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms), Self::Solid(c) => c.set_as_stroke(ctx, on_text, transforms),
@ -232,10 +293,10 @@ impl PaintEncode for Paint {
} }
impl PaintEncode for Color { impl PaintEncode for Color {
fn set_as_fill(&self, ctx: &mut PageContext, _: bool, _: Transforms) { fn set_as_fill(&self, ctx: &mut content::Builder, _: bool, _: content::Transforms) {
match self { match self {
Color::Luma(_) => { Color::Luma(_) => {
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc); ctx.resources.colors.mark_as_used(ColorSpace::D65Gray);
ctx.set_fill_color_space(D65_GRAY); ctx.set_fill_color_space(D65_GRAY);
let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); let [l, _, _, _] = ColorSpace::D65Gray.encode(*self);
@ -243,21 +304,21 @@ impl PaintEncode for Color {
} }
// Oklch is converted to Oklab. // Oklch is converted to Oklab.
Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => { Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => {
ctx.parent.colors.oklab(&mut ctx.parent.alloc); ctx.resources.colors.mark_as_used(ColorSpace::Oklab);
ctx.set_fill_color_space(OKLAB); ctx.set_fill_color_space(OKLAB);
let [l, a, b, _] = ColorSpace::Oklab.encode(*self); let [l, a, b, _] = ColorSpace::Oklab.encode(*self);
ctx.content.set_fill_color([l, a, b]); ctx.content.set_fill_color([l, a, b]);
} }
Color::LinearRgb(_) => { Color::LinearRgb(_) => {
ctx.parent.colors.linear_rgb(); ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb);
ctx.set_fill_color_space(LINEAR_SRGB); ctx.set_fill_color_space(LINEAR_SRGB);
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self);
ctx.content.set_fill_color([r, g, b]); ctx.content.set_fill_color([r, g, b]);
} }
Color::Rgb(_) => { Color::Rgb(_) => {
ctx.parent.colors.srgb(&mut ctx.parent.alloc); ctx.resources.colors.mark_as_used(ColorSpace::Srgb);
ctx.set_fill_color_space(SRGB); ctx.set_fill_color_space(SRGB);
let [r, g, b, _] = ColorSpace::Srgb.encode(*self); let [r, g, b, _] = ColorSpace::Srgb.encode(*self);
@ -272,10 +333,10 @@ impl PaintEncode for Color {
} }
} }
fn set_as_stroke(&self, ctx: &mut PageContext, _: bool, _: Transforms) { fn set_as_stroke(&self, ctx: &mut content::Builder, _: bool, _: content::Transforms) {
match self { match self {
Color::Luma(_) => { Color::Luma(_) => {
ctx.parent.colors.d65_gray(&mut ctx.parent.alloc); ctx.resources.colors.mark_as_used(ColorSpace::D65Gray);
ctx.set_stroke_color_space(D65_GRAY); ctx.set_stroke_color_space(D65_GRAY);
let [l, _, _, _] = ColorSpace::D65Gray.encode(*self); let [l, _, _, _] = ColorSpace::D65Gray.encode(*self);
@ -283,21 +344,21 @@ impl PaintEncode for Color {
} }
// Oklch is converted to Oklab. // Oklch is converted to Oklab.
Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => { Color::Oklab(_) | Color::Oklch(_) | Color::Hsl(_) | Color::Hsv(_) => {
ctx.parent.colors.oklab(&mut ctx.parent.alloc); ctx.resources.colors.mark_as_used(ColorSpace::Oklab);
ctx.set_stroke_color_space(OKLAB); ctx.set_stroke_color_space(OKLAB);
let [l, a, b, _] = ColorSpace::Oklab.encode(*self); let [l, a, b, _] = ColorSpace::Oklab.encode(*self);
ctx.content.set_stroke_color([l, a, b]); ctx.content.set_stroke_color([l, a, b]);
} }
Color::LinearRgb(_) => { Color::LinearRgb(_) => {
ctx.parent.colors.linear_rgb(); ctx.resources.colors.mark_as_used(ColorSpace::LinearRgb);
ctx.set_stroke_color_space(LINEAR_SRGB); ctx.set_stroke_color_space(LINEAR_SRGB);
let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self); let [r, g, b, _] = ColorSpace::LinearRgb.encode(*self);
ctx.content.set_stroke_color([r, g, b]); ctx.content.set_stroke_color([r, g, b]);
} }
Color::Rgb(_) => { Color::Rgb(_) => {
ctx.parent.colors.srgb(&mut ctx.parent.alloc); ctx.resources.colors.mark_as_used(ColorSpace::Srgb);
ctx.set_stroke_color_space(SRGB); ctx.set_stroke_color_space(SRGB);
let [r, g, b, _] = ColorSpace::Srgb.encode(*self); let [r, g, b, _] = ColorSpace::Srgb.encode(*self);

View File

@ -0,0 +1,312 @@
//! OpenType fonts generally define monochrome glyphs, but they can also define
//! glyphs with colors. This is how emojis are generally implemented for
//! example.
//!
//! There are various standards to represent color glyphs, but PDF readers don't
//! support any of them natively, so Typst has to handle them manually.
use std::collections::HashMap;
use ecow::eco_format;
use indexmap::IndexMap;
use pdf_writer::Filter;
use pdf_writer::{types::UnicodeCmap, Finish, Name, Rect, Ref};
use ttf_parser::name_id;
use typst::layout::Em;
use typst::text::{color::frame_for_glyph, Font};
use crate::resources::{Resources, ResourcesRefs};
use crate::WithGlobalRefs;
use crate::{
content,
font::{subset_tag, write_font_descriptor, CMAP_NAME, SYSTEM_INFO},
EmExt, PdfChunk,
};
/// Write color fonts in the PDF document.
///
/// They are written as Type3 fonts, which map glyph IDs to arbitrary PDF
/// instructions.
pub fn write_color_fonts(
context: &WithGlobalRefs,
) -> (PdfChunk, HashMap<ColorFontSlice, Ref>) {
let mut out = HashMap::new();
let mut chunk = PdfChunk::new();
context.resources.traverse(&mut |resources: &Resources| {
let Some(color_fonts) = &resources.color_fonts else {
return;
};
for (color_font, font_slice) in color_fonts.iter() {
if out.contains_key(&font_slice) {
continue;
}
// Allocate some IDs.
let subfont_id = chunk.alloc();
let cmap_ref = chunk.alloc();
let descriptor_ref = chunk.alloc();
let widths_ref = chunk.alloc();
// And a map between glyph IDs and the instructions to draw this
// glyph.
let mut glyphs_to_instructions = Vec::new();
let start = font_slice.subfont * 256;
let end = (start + 256).min(color_font.glyphs.len());
let glyph_count = end - start;
let subset = &color_font.glyphs[start..end];
let mut widths = Vec::new();
let mut gids = Vec::new();
let scale_factor = font_slice.font.ttf().units_per_em() as f32;
// Write the instructions for each glyph.
for color_glyph in subset {
let instructions_stream_ref = chunk.alloc();
let width = font_slice
.font
.advance(color_glyph.gid)
.unwrap_or(Em::new(0.0))
.to_font_units();
widths.push(width);
chunk
.stream(
instructions_stream_ref,
color_glyph.instructions.content.wait(),
)
.filter(Filter::FlateDecode);
// Use this stream as instructions to draw the glyph.
glyphs_to_instructions.push(instructions_stream_ref);
gids.push(color_glyph.gid);
}
// Write the Type3 font object.
let mut pdf_font = chunk.type3_font(subfont_id);
pdf_font.pair(Name(b"Resources"), color_fonts.resources.reference);
pdf_font.bbox(color_font.bbox);
pdf_font.matrix([1.0 / scale_factor, 0.0, 0.0, 1.0 / scale_factor, 0.0, 0.0]);
pdf_font.first_char(0);
pdf_font.last_char((glyph_count - 1) as u8);
pdf_font.pair(Name(b"Widths"), widths_ref);
pdf_font.to_unicode(cmap_ref);
pdf_font.font_descriptor(descriptor_ref);
// Write the /CharProcs dictionary, that maps glyph names to
// drawing instructions.
let mut char_procs = pdf_font.char_procs();
for (gid, instructions_ref) in glyphs_to_instructions.iter().enumerate() {
char_procs
.pair(Name(eco_format!("glyph{gid}").as_bytes()), *instructions_ref);
}
char_procs.finish();
// Write the /Encoding dictionary.
let names = (0..glyph_count)
.map(|gid| eco_format!("glyph{gid}"))
.collect::<Vec<_>>();
pdf_font
.encoding_custom()
.differences()
.consecutive(0, names.iter().map(|name| Name(name.as_bytes())));
pdf_font.finish();
// Encode a CMAP to make it possible to search or copy glyphs.
let glyph_set = resources.glyph_sets.get(&font_slice.font).unwrap();
let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO);
for (index, glyph) in subset.iter().enumerate() {
let Some(text) = glyph_set.get(&glyph.gid) else {
continue;
};
if !text.is_empty() {
cmap.pair_with_multiple(index as u8, text.chars());
}
}
chunk.cmap(cmap_ref, &cmap.finish());
// Write the font descriptor.
gids.sort();
let subset_tag = subset_tag(&gids);
let postscript_name = font_slice
.font
.find_name(name_id::POST_SCRIPT_NAME)
.unwrap_or_else(|| "unknown".to_string());
let base_font = eco_format!("{subset_tag}+{postscript_name}");
write_font_descriptor(
&mut chunk,
descriptor_ref,
&font_slice.font,
&base_font,
);
// Write the widths array
chunk.indirect(widths_ref).array().items(widths);
out.insert(font_slice, subfont_id);
}
});
(chunk, out)
}
/// A mapping between `Font`s and all the corresponding `ColorFont`s.
///
/// This mapping is one-to-many because there can only be 256 glyphs in a Type 3
/// font, and fonts generally have more color glyphs than that.
pub struct ColorFontMap<R> {
/// The mapping itself.
map: IndexMap<Font, ColorFont>,
/// The resources required to render the fonts in this map.
///
/// For example, this can be the images for glyphs based on bitmaps or SVG.
pub resources: Resources<R>,
/// The number of font slices (groups of 256 color glyphs), across all color
/// fonts.
total_slice_count: usize,
}
/// A collection of Type3 font, belonging to the same TTF font.
pub struct ColorFont {
/// The IDs of each sub-slice of this font. They are the numbers after "Cf"
/// in the Resources dictionaries.
slice_ids: Vec<usize>,
/// The list of all color glyphs in this family.
///
/// The index in this vector modulo 256 corresponds to the index in one of
/// the Type3 fonts in `refs` (the `n`-th in the vector, where `n` is the
/// quotient of the index divided by 256).
pub glyphs: Vec<ColorGlyph>,
/// The global bounding box of the font.
pub bbox: Rect,
/// A mapping between glyph IDs and character indices in the `glyphs`
/// vector.
glyph_indices: HashMap<u16, usize>,
}
/// A single color glyph.
pub struct ColorGlyph {
/// The ID of the glyph.
pub gid: u16,
/// Instructions to draw the glyph.
pub instructions: content::Encoded,
}
impl ColorFontMap<()> {
/// Creates a new empty mapping
pub fn new() -> Self {
Self {
map: IndexMap::new(),
total_slice_count: 0,
resources: Resources::default(),
}
}
/// For a given glyph in a TTF font, give the ID of the Type3 font and the
/// index of the glyph inside of this Type3 font.
///
/// If this is the first occurrence of this glyph in this font, it will
/// start its encoding and add it to the list of known glyphs.
pub fn get(&mut self, font: &Font, gid: u16) -> (usize, u8) {
let color_font = self.map.entry(font.clone()).or_insert_with(|| {
let global_bbox = font.ttf().global_bounding_box();
let bbox = Rect::new(
font.to_em(global_bbox.x_min).to_font_units(),
font.to_em(global_bbox.y_min).to_font_units(),
font.to_em(global_bbox.x_max).to_font_units(),
font.to_em(global_bbox.y_max).to_font_units(),
);
ColorFont {
bbox,
slice_ids: Vec::new(),
glyphs: Vec::new(),
glyph_indices: HashMap::new(),
}
});
if let Some(index_of_glyph) = color_font.glyph_indices.get(&gid) {
// If we already know this glyph, return it.
(color_font.slice_ids[index_of_glyph / 256], *index_of_glyph as u8)
} else {
// Otherwise, allocate a new ColorGlyph in the font, and a new Type3 font
// if needed
let index = color_font.glyphs.len();
if index % 256 == 0 {
color_font.slice_ids.push(self.total_slice_count);
self.total_slice_count += 1;
}
let frame = frame_for_glyph(font, gid);
let width = font.advance(gid).unwrap_or(Em::new(0.0)).to_font_units();
let instructions = content::build(&mut self.resources, &frame, Some(width));
color_font.glyphs.push(ColorGlyph { gid, instructions });
color_font.glyph_indices.insert(gid, index);
(color_font.slice_ids[index / 256], index as u8)
}
}
/// Assign references to the resource dictionary used by this set of color
/// fonts.
pub fn with_refs(self, refs: &ResourcesRefs) -> ColorFontMap<Ref> {
ColorFontMap {
map: self.map,
resources: self.resources.with_refs(refs),
total_slice_count: self.total_slice_count,
}
}
}
impl<R> ColorFontMap<R> {
/// Iterate over all Type3 fonts.
///
/// Each item of this iterator maps to a Type3 font: it contains
/// at most 256 glyphs. A same TTF font can yield multiple Type3 fonts.
pub fn iter(&self) -> ColorFontMapIter<'_, R> {
ColorFontMapIter { map: self, font_index: 0, slice_index: 0 }
}
}
/// Iterator over a [`ColorFontMap`].
///
/// See [`ColorFontMap::iter`].
pub struct ColorFontMapIter<'a, R> {
/// The map over which to iterate
map: &'a ColorFontMap<R>,
/// The index of TTF font on which we currently iterate
font_index: usize,
/// The sub-font (slice of at most 256 glyphs) at which we currently are.
slice_index: usize,
}
impl<'a, R> Iterator for ColorFontMapIter<'a, R> {
type Item = (&'a ColorFont, ColorFontSlice);
fn next(&mut self) -> Option<Self::Item> {
let (font, color_font) = self.map.map.get_index(self.font_index)?;
let slice_count = (color_font.glyphs.len() / 256) + 1;
if self.slice_index >= slice_count {
self.font_index += 1;
self.slice_index = 0;
return self.next();
}
let slice = ColorFontSlice { font: font.clone(), subfont: self.slice_index };
self.slice_index += 1;
Some((color_font, slice))
}
}
/// A set of at most 256 glyphs (a limit imposed on Type3 fonts by the PDF
/// specification) that represents a part of a TTF font.
#[derive(PartialEq, Eq, Hash, Debug, Clone)]
pub struct ColorFontSlice {
/// The original TTF font.
pub font: Font,
/// The index of the Type3 font, among all those that are necessary to
/// represent the subset of the TTF font we are interested in.
pub subfont: usize,
}

View File

@ -0,0 +1,712 @@
//! Generic writer for PDF content.
//!
//! It is used to write page contents, color glyph instructions, and patterns.
//!
//! See also [`pdf_writer::Content`].
use ecow::eco_format;
use pdf_writer::{
types::{ColorSpaceOperand, LineCapStyle, LineJoinStyle, TextRenderingMode},
Content, Finish, Name, Rect, Str,
};
use typst::layout::{
Abs, Em, Frame, FrameItem, GroupItem, Point, Ratio, Size, Transform,
};
use typst::model::Destination;
use typst::text::{color::is_color_glyph, Font, TextItem, TextItemView};
use typst::utils::{Deferred, Numeric, SliceExt};
use typst::visualize::{
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
};
use crate::color_font::ColorFontMap;
use crate::extg::ExtGState;
use crate::image::deferred_image;
use crate::{color::PaintEncode, resources::Resources};
use crate::{deflate_deferred, AbsExt, EmExt};
/// Encode a [`Frame`] into a content stream.
///
/// The resources that were used in the stream will be added to `resources`.
///
/// `color_glyph_width` should be `None` unless the `Frame` represents a [color
/// glyph].
///
/// [color glyph]: `crate::color_font`
pub fn build(
resources: &mut Resources<()>,
frame: &Frame,
color_glyph_width: Option<f32>,
) -> Encoded {
let size = frame.size();
let mut ctx = Builder::new(resources, size);
if let Some(width) = color_glyph_width {
ctx.content.start_color_glyph(width);
}
// Make the coordinate system start at the top-left.
ctx.transform(
// Make the Y axis go upwards
Transform::scale(Ratio::one(), -Ratio::one())
// Also move the origin to the top left corner
.post_concat(Transform::translate(Abs::zero(), size.y)),
);
// Encode the frame into the content stream.
write_frame(&mut ctx, frame);
Encoded {
size,
content: deflate_deferred(ctx.content.finish()),
uses_opacities: ctx.uses_opacities,
links: ctx.links,
}
}
/// An encoded content stream.
pub struct Encoded {
/// The dimensions of the content.
pub size: Size,
/// The actual content stream.
pub content: Deferred<Vec<u8>>,
/// Whether the content opacities.
pub uses_opacities: bool,
/// Links in the PDF coordinate system.
pub links: Vec<(Destination, Rect)>,
}
/// An exporter for a single PDF content stream.
///
/// Content streams are a series of PDF commands. They can reference external
/// objects only through resources.
///
/// Content streams can be used for page contents, but also to describe color
/// glyphs and patterns.
pub struct Builder<'a, R = ()> {
/// A list of all resources that are used in the content stream.
pub(crate) resources: &'a mut Resources<R>,
/// The PDF content stream that is being built.
pub content: Content,
/// Current graphic state.
state: State,
/// Stack of saved graphic states.
saves: Vec<State>,
/// Wheter any stroke or fill was not totally opaque.
uses_opacities: bool,
/// All clickable links that are present in this content.
links: Vec<(Destination, Rect)>,
}
impl<'a, R> Builder<'a, R> {
/// Create a new content builder.
pub fn new(resources: &'a mut Resources<R>, size: Size) -> Self {
Builder {
resources,
uses_opacities: false,
content: Content::new(),
state: State::new(size),
saves: vec![],
links: vec![],
}
}
}
/// A simulated graphics state used to deduplicate graphics state changes and
/// keep track of the current transformation matrix for link annotations.
#[derive(Debug, Clone)]
struct State {
/// The transform of the current item.
transform: Transform,
/// The transform of first hard frame in the hierarchy.
container_transform: Transform,
/// The size of the first hard frame in the hierarchy.
size: Size,
/// The current font.
font: Option<(Font, Abs)>,
/// The current fill paint.
fill: Option<Paint>,
/// The color space of the current fill paint.
fill_space: Option<Name<'static>>,
/// The current external graphic state.
external_graphics_state: Option<ExtGState>,
/// The current stroke paint.
stroke: Option<FixedStroke>,
/// The color space of the current stroke paint.
stroke_space: Option<Name<'static>>,
/// The current text rendering mode.
text_rendering_mode: TextRenderingMode,
}
impl State {
/// Creates a new, clean state for a given `size`.
pub fn new(size: Size) -> Self {
Self {
transform: Transform::identity(),
container_transform: Transform::identity(),
size,
font: None,
fill: None,
fill_space: None,
external_graphics_state: None,
stroke: None,
stroke_space: None,
text_rendering_mode: TextRenderingMode::Fill,
}
}
/// Creates the [`Transforms`] structure for the current item.
pub fn transforms(&self, size: Size, pos: Point) -> Transforms {
Transforms {
transform: self.transform.pre_concat(Transform::translate(pos.x, pos.y)),
container_transform: self.container_transform,
container_size: self.size,
size,
}
}
}
/// Subset of the state used to calculate the transform of gradients and patterns.
#[derive(Debug, Clone, Copy)]
pub(super) struct Transforms {
/// The transform of the current item.
pub transform: Transform,
/// The transform of first hard frame in the hierarchy.
pub container_transform: Transform,
/// The size of the first hard frame in the hierarchy.
pub container_size: Size,
/// The size of the item.
pub size: Size,
}
impl Builder<'_, ()> {
fn save_state(&mut self) {
self.saves.push(self.state.clone());
self.content.save_state();
}
fn restore_state(&mut self) {
self.content.restore_state();
self.state = self.saves.pop().expect("missing state save");
}
fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) {
let current_state = self.state.external_graphics_state.as_ref();
if current_state != Some(graphics_state) {
let index = self.resources.ext_gs.insert(*graphics_state);
let name = eco_format!("Gs{index}");
self.content.set_parameters(Name(name.as_bytes()));
if graphics_state.uses_opacities() {
self.uses_opacities = true;
}
}
}
fn set_opacities(&mut self, stroke: Option<&FixedStroke>, fill: Option<&Paint>) {
let stroke_opacity = stroke
.map(|stroke| {
let color = match &stroke.paint {
Paint::Solid(color) => *color,
Paint::Gradient(_) | Paint::Pattern(_) => return 255,
};
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
})
.unwrap_or(255);
let fill_opacity = fill
.map(|paint| {
let color = match paint {
Paint::Solid(color) => *color,
Paint::Gradient(_) | Paint::Pattern(_) => return 255,
};
color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
})
.unwrap_or(255);
self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity });
}
pub fn transform(&mut self, transform: Transform) {
let Transform { sx, ky, kx, sy, tx, ty } = transform;
self.state.transform = self.state.transform.pre_concat(transform);
if self.state.container_transform.is_identity() {
self.state.container_transform = self.state.transform;
}
self.content.transform([
sx.get() as _,
ky.get() as _,
kx.get() as _,
sy.get() as _,
tx.to_f32(),
ty.to_f32(),
]);
}
fn group_transform(&mut self, transform: Transform) {
self.state.container_transform =
self.state.container_transform.pre_concat(transform);
}
fn set_font(&mut self, font: &Font, size: Abs) {
if self.state.font.as_ref().map(|(f, s)| (f, *s)) != Some((font, size)) {
let index = self.resources.fonts.insert(font.clone());
let name = eco_format!("F{index}");
self.content.set_font(Name(name.as_bytes()), size.to_f32());
self.state.font = Some((font.clone(), size));
}
}
fn size(&mut self, size: Size) {
self.state.size = size;
}
fn set_fill(&mut self, fill: &Paint, on_text: bool, transforms: Transforms) {
if self.state.fill.as_ref() != Some(fill)
|| matches!(self.state.fill, Some(Paint::Gradient(_)))
{
fill.set_as_fill(self, on_text, transforms);
self.state.fill = Some(fill.clone());
}
}
pub fn set_fill_color_space(&mut self, space: Name<'static>) {
if self.state.fill_space != Some(space) {
self.content.set_fill_color_space(ColorSpaceOperand::Named(space));
self.state.fill_space = Some(space);
}
}
pub fn reset_fill_color_space(&mut self) {
self.state.fill_space = None;
}
fn set_stroke(
&mut self,
stroke: &FixedStroke,
on_text: bool,
transforms: Transforms,
) {
if self.state.stroke.as_ref() != Some(stroke)
|| matches!(
self.state.stroke.as_ref().map(|s| &s.paint),
Some(Paint::Gradient(_))
)
{
let FixedStroke { paint, thickness, cap, join, dash, miter_limit } = stroke;
paint.set_as_stroke(self, on_text, transforms);
self.content.set_line_width(thickness.to_f32());
if self.state.stroke.as_ref().map(|s| &s.cap) != Some(cap) {
self.content.set_line_cap(to_pdf_line_cap(*cap));
}
if self.state.stroke.as_ref().map(|s| &s.join) != Some(join) {
self.content.set_line_join(to_pdf_line_join(*join));
}
if self.state.stroke.as_ref().map(|s| &s.dash) != Some(dash) {
if let Some(pattern) = dash {
self.content.set_dash_pattern(
pattern.array.iter().map(|l| l.to_f32()),
pattern.phase.to_f32(),
);
} else {
self.content.set_dash_pattern([], 0.0);
}
}
if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) {
self.content.set_miter_limit(miter_limit.get() as f32);
}
self.state.stroke = Some(stroke.clone());
}
}
pub fn set_stroke_color_space(&mut self, space: Name<'static>) {
if self.state.stroke_space != Some(space) {
self.content.set_stroke_color_space(ColorSpaceOperand::Named(space));
self.state.stroke_space = Some(space);
}
}
pub fn reset_stroke_color_space(&mut self) {
self.state.stroke_space = None;
}
fn set_text_rendering_mode(&mut self, mode: TextRenderingMode) {
if self.state.text_rendering_mode != mode {
self.content.set_text_rendering_mode(mode);
self.state.text_rendering_mode = mode;
}
}
}
/// Encode a frame into the content stream.
pub(crate) fn write_frame(ctx: &mut Builder, frame: &Frame) {
for &(pos, ref item) in frame.items() {
let x = pos.x.to_f32();
let y = pos.y.to_f32();
match item {
FrameItem::Group(group) => write_group(ctx, pos, group),
FrameItem::Text(text) => write_text(ctx, pos, text),
FrameItem::Shape(shape, _) => write_shape(ctx, pos, shape),
FrameItem::Image(image, size, _) => write_image(ctx, x, y, image, *size),
FrameItem::Link(dest, size) => write_link(ctx, pos, dest, *size),
FrameItem::Tag(_) => {}
}
}
}
/// Encode a group into the content stream.
fn write_group(ctx: &mut Builder, pos: Point, group: &GroupItem) {
let translation = Transform::translate(pos.x, pos.y);
ctx.save_state();
if group.frame.kind().is_hard() {
ctx.group_transform(
ctx.state
.transform
.post_concat(ctx.state.container_transform.invert().unwrap())
.pre_concat(translation)
.pre_concat(group.transform),
);
ctx.size(group.frame.size());
}
ctx.transform(translation.pre_concat(group.transform));
if let Some(clip_path) = &group.clip_path {
write_path(ctx, 0.0, 0.0, clip_path);
ctx.content.clip_nonzero();
ctx.content.end_path();
}
write_frame(ctx, &group.frame);
ctx.restore_state();
}
/// Encode a text run into the content stream.
fn write_text(ctx: &mut Builder, pos: Point, text: &TextItem) {
let ttf = text.font.ttf();
let tables = ttf.tables();
// If the text run contains either only color glyphs (used for emojis for
// example) or normal text we can render it directly
let has_color_glyphs = tables.sbix.is_some()
|| tables.cbdt.is_some()
|| tables.svg.is_some()
|| tables.colr.is_some();
if !has_color_glyphs {
write_normal_text(ctx, pos, TextItemView::all_of(text));
return;
}
let color_glyph_count =
text.glyphs.iter().filter(|g| is_color_glyph(&text.font, g)).count();
if color_glyph_count == text.glyphs.len() {
write_color_glyphs(ctx, pos, TextItemView::all_of(text));
} else if color_glyph_count == 0 {
write_normal_text(ctx, pos, TextItemView::all_of(text));
} else {
// Otherwise we need to split it in smaller text runs
let mut offset = 0;
let mut position_in_run = Abs::zero();
for (color, sub_run) in
text.glyphs.group_by_key(|g| is_color_glyph(&text.font, g))
{
let end = offset + sub_run.len();
// Build a sub text-run
let text_item_view = TextItemView::from_glyph_range(text, offset..end);
// Adjust the position of the run on the line
let pos = pos + Point::new(position_in_run, Abs::zero());
position_in_run += text_item_view.width();
offset = end;
// Actually write the sub text-run
if color {
write_color_glyphs(ctx, pos, text_item_view);
} else {
write_normal_text(ctx, pos, text_item_view);
}
}
}
}
/// Encodes a text run (without any color glyph) into the content stream.
fn write_normal_text(ctx: &mut Builder, pos: Point, text: TextItemView) {
let x = pos.x.to_f32();
let y = pos.y.to_f32();
*ctx.resources.languages.entry(text.item.lang).or_insert(0) += text.glyph_range.len();
let glyph_set = ctx.resources.glyph_sets.entry(text.item.font.clone()).or_default();
for g in text.glyphs() {
let t = text.text();
let segment = &t[g.range()];
glyph_set.entry(g.id).or_insert_with(|| segment.into());
}
let fill_transform = ctx.state.transforms(Size::zero(), pos);
ctx.set_fill(&text.item.fill, true, fill_transform);
let stroke = text.item.stroke.as_ref().and_then(|stroke| {
if stroke.thickness.to_f32() > 0.0 {
Some(stroke)
} else {
None
}
});
if let Some(stroke) = stroke {
ctx.set_stroke(stroke, true, fill_transform);
ctx.set_text_rendering_mode(TextRenderingMode::FillStroke);
} else {
ctx.set_text_rendering_mode(TextRenderingMode::Fill);
}
ctx.set_font(&text.item.font, text.item.size);
ctx.set_opacities(text.item.stroke.as_ref(), Some(&text.item.fill));
ctx.content.begin_text();
// Position the text.
ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]);
let mut positioned = ctx.content.show_positioned();
let mut items = positioned.items();
let mut adjustment = Em::zero();
let mut encoded = vec![];
// Write the glyphs with kerning adjustments.
for glyph in text.glyphs() {
adjustment += glyph.x_offset;
if !adjustment.is_zero() {
if !encoded.is_empty() {
items.show(Str(&encoded));
encoded.clear();
}
items.adjust(-adjustment.to_font_units());
adjustment = Em::zero();
}
let cid = crate::font::glyph_cid(&text.item.font, glyph.id);
encoded.push((cid >> 8) as u8);
encoded.push((cid & 0xff) as u8);
if let Some(advance) = text.item.font.advance(glyph.id) {
adjustment += glyph.x_advance - advance;
}
adjustment -= glyph.x_offset;
}
if !encoded.is_empty() {
items.show(Str(&encoded));
}
items.finish();
positioned.finish();
ctx.content.end_text();
}
/// Encodes a text run made only of color glyphs into the content stream
fn write_color_glyphs(ctx: &mut Builder, pos: Point, text: TextItemView) {
let x = pos.x.to_f32();
let y = pos.y.to_f32();
let mut last_font = None;
ctx.content.begin_text();
ctx.content.set_text_matrix([1.0, 0.0, 0.0, -1.0, x, y]);
// So that the next call to ctx.set_font() will change the font to one that
// displays regular glyphs and not color glyphs.
ctx.state.font = None;
let glyph_set = ctx.resources.glyph_sets.entry(text.item.font.clone()).or_default();
for glyph in text.glyphs() {
// Retrieve the Type3 font reference and the glyph index in the font.
let color_fonts = ctx
.resources
.color_fonts
.get_or_insert_with(|| Box::new(ColorFontMap::new()));
let (font, index) = color_fonts.get(&text.item.font, glyph.id);
if last_font != Some(font) {
ctx.content.set_font(
Name(eco_format!("Cf{}", font).as_bytes()),
text.item.size.to_f32(),
);
last_font = Some(font);
}
ctx.content.show(Str(&[index]));
glyph_set
.entry(glyph.id)
.or_insert_with(|| text.text()[glyph.range()].into());
}
ctx.content.end_text();
}
/// Encode a geometrical shape into the content stream.
fn write_shape(ctx: &mut Builder, pos: Point, shape: &Shape) {
let x = pos.x.to_f32();
let y = pos.y.to_f32();
let stroke = shape.stroke.as_ref().and_then(|stroke| {
if stroke.thickness.to_f32() > 0.0 {
Some(stroke)
} else {
None
}
});
if shape.fill.is_none() && stroke.is_none() {
return;
}
if let Some(fill) = &shape.fill {
ctx.set_fill(fill, false, ctx.state.transforms(shape.geometry.bbox_size(), pos));
}
if let Some(stroke) = stroke {
ctx.set_stroke(
stroke,
false,
ctx.state.transforms(shape.geometry.bbox_size(), pos),
);
}
ctx.set_opacities(stroke, shape.fill.as_ref());
match shape.geometry {
Geometry::Line(target) => {
let dx = target.x.to_f32();
let dy = target.y.to_f32();
ctx.content.move_to(x, y);
ctx.content.line_to(x + dx, y + dy);
}
Geometry::Rect(size) => {
let w = size.x.to_f32();
let h = size.y.to_f32();
if w.abs() > f32::EPSILON && h.abs() > f32::EPSILON {
ctx.content.rect(x, y, w, h);
}
}
Geometry::Path(ref path) => {
write_path(ctx, x, y, path);
}
}
match (&shape.fill, stroke) {
(None, None) => unreachable!(),
(Some(_), None) => ctx.content.fill_nonzero(),
(None, Some(_)) => ctx.content.stroke(),
(Some(_), Some(_)) => ctx.content.fill_nonzero_and_stroke(),
};
}
/// Encode a bezier path into the content stream.
fn write_path(ctx: &mut Builder, x: f32, y: f32, path: &Path) {
for elem in &path.0 {
match elem {
PathItem::MoveTo(p) => {
ctx.content.move_to(x + p.x.to_f32(), y + p.y.to_f32())
}
PathItem::LineTo(p) => {
ctx.content.line_to(x + p.x.to_f32(), y + p.y.to_f32())
}
PathItem::CubicTo(p1, p2, p3) => ctx.content.cubic_to(
x + p1.x.to_f32(),
y + p1.y.to_f32(),
x + p2.x.to_f32(),
y + p2.y.to_f32(),
x + p3.x.to_f32(),
y + p3.y.to_f32(),
),
PathItem::ClosePath => ctx.content.close_path(),
};
}
}
/// Encode a vector or raster image into the content stream.
fn write_image(ctx: &mut Builder, x: f32, y: f32, image: &Image, size: Size) {
let index = ctx.resources.images.insert(image.clone());
ctx.resources.deferred_images.entry(index).or_insert_with(|| {
let (image, color_space) = deferred_image(image.clone());
if let Some(color_space) = color_space {
ctx.resources.colors.mark_as_used(color_space);
}
image
});
let name = eco_format!("Im{index}");
let w = size.x.to_f32();
let h = size.y.to_f32();
ctx.content.save_state();
ctx.content.transform([w, 0.0, 0.0, -h, x, y + h]);
if let Some(alt) = image.alt() {
let mut image_span =
ctx.content.begin_marked_content_with_properties(Name(b"Span"));
let mut image_alt = image_span.properties();
image_alt.pair(Name(b"Alt"), pdf_writer::Str(alt.as_bytes()));
image_alt.finish();
image_span.finish();
ctx.content.x_object(Name(name.as_bytes()));
ctx.content.end_marked_content();
} else {
ctx.content.x_object(Name(name.as_bytes()));
}
ctx.content.restore_state();
}
/// Save a link for later writing in the annotations dictionary.
fn write_link(ctx: &mut Builder, pos: Point, dest: &Destination, size: Size) {
let mut min_x = Abs::inf();
let mut min_y = Abs::inf();
let mut max_x = -Abs::inf();
let mut max_y = -Abs::inf();
// Compute the bounding box of the transformed link.
for point in [
pos,
pos + Point::with_x(size.x),
pos + Point::with_y(size.y),
pos + size.to_point(),
] {
let t = point.transform(ctx.state.transform);
min_x.set_min(t.x);
min_y.set_min(t.y);
max_x.set_max(t.x);
max_y.set_max(t.y);
}
let x1 = min_x.to_f32();
let x2 = max_x.to_f32();
let y1 = max_y.to_f32();
let y2 = min_y.to_f32();
let rect = Rect::new(x1, y1, x2, y2);
ctx.links.push((dest.clone(), rect));
}
fn to_pdf_line_cap(cap: LineCap) -> LineCapStyle {
match cap {
LineCap::Butt => LineCapStyle::ButtCap,
LineCap::Round => LineCapStyle::RoundCap,
LineCap::Square => LineCapStyle::ProjectingSquareCap,
}
}
fn to_pdf_line_join(join: LineJoin) -> LineJoinStyle {
match join {
LineJoin::Miter => LineJoinStyle::MiterJoin,
LineJoin::Round => LineJoinStyle::RoundJoin,
LineJoin::Bevel => LineJoinStyle::BevelJoin,
}
}

View File

@ -1,4 +1,8 @@
use crate::PdfContext; use std::collections::HashMap;
use pdf_writer::Ref;
use crate::{PdfChunk, WithGlobalRefs};
/// A PDF external graphics state. /// A PDF external graphics state.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
@ -22,13 +26,25 @@ impl ExtGState {
} }
/// Embed all used external graphics states into the PDF. /// Embed all used external graphics states into the PDF.
pub(crate) fn write_external_graphics_states(ctx: &mut PdfContext) { pub fn write_graphic_states(
for external_gs in ctx.extg_map.items() { context: &WithGlobalRefs,
let id = ctx.alloc.bump(); ) -> (PdfChunk, HashMap<ExtGState, Ref>) {
ctx.ext_gs_refs.push(id); let mut chunk = PdfChunk::new();
ctx.pdf let mut out = HashMap::new();
.ext_graphics(id) context.resources.traverse(&mut |resources| {
.non_stroking_alpha(external_gs.fill_opacity as f32 / 255.0) for external_gs in resources.ext_gs.items() {
.stroking_alpha(external_gs.stroke_opacity as f32 / 255.0); if out.contains_key(external_gs) {
} continue;
}
let id = chunk.alloc();
out.insert(*external_gs, id);
chunk
.ext_graphics(id)
.non_stroking_alpha(external_gs.fill_opacity as f32 / 255.0)
.stroking_alpha(external_gs.stroke_opacity as f32 / 255.0);
}
});
(chunk, out)
} }

View File

@ -1,24 +1,24 @@
use std::collections::BTreeMap; use std::collections::{BTreeMap, HashMap};
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc; use std::sync::Arc;
use ecow::{eco_format, EcoString}; use ecow::{eco_format, EcoString};
use pdf_writer::types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap}; use pdf_writer::{
use pdf_writer::writers::FontDescriptor; types::{CidFontType, FontFlags, SystemInfo, UnicodeCmap},
use pdf_writer::{Filter, Finish, Name, Rect, Str}; writers::FontDescriptor,
Chunk, Filter, Finish, Name, Rect, Ref, Str,
};
use ttf_parser::{name_id, GlyphId, Tag}; use ttf_parser::{name_id, GlyphId, Tag};
use typst::layout::{Abs, Em, Ratio, Transform};
use typst::text::Font; use typst::text::Font;
use typst::utils::SliceExt; use typst::utils::SliceExt;
use unicode_properties::{GeneralCategory, UnicodeGeneralCategory}; use unicode_properties::{GeneralCategory, UnicodeGeneralCategory};
use crate::page::{write_frame, PageContext}; use crate::{deflate, EmExt, PdfChunk, WithGlobalRefs};
use crate::{deflate, AbsExt, EmExt, PdfContext};
const CFF: Tag = Tag::from_bytes(b"CFF "); const CFF: Tag = Tag::from_bytes(b"CFF ");
const CFF2: Tag = Tag::from_bytes(b"CFF2"); const CFF2: Tag = Tag::from_bytes(b"CFF2");
const CMAP_NAME: Name = Name(b"Custom"); pub(crate) const CMAP_NAME: Name = Name(b"Custom");
const SYSTEM_INFO: SystemInfo = SystemInfo { pub(crate) const SYSTEM_INFO: SystemInfo = SystemInfo {
registry: Str(b"Adobe"), registry: Str(b"Adobe"),
ordering: Str(b"Identity"), ordering: Str(b"Identity"),
supplement: 0, supplement: 0,
@ -26,230 +26,127 @@ const SYSTEM_INFO: SystemInfo = SystemInfo {
/// Embed all used fonts into the PDF. /// Embed all used fonts into the PDF.
#[typst_macros::time(name = "write fonts")] #[typst_macros::time(name = "write fonts")]
pub(crate) fn write_fonts(ctx: &mut PdfContext) { pub fn write_fonts(context: &WithGlobalRefs) -> (PdfChunk, HashMap<Font, Ref>) {
write_color_fonts(ctx); let mut chunk = PdfChunk::new();
let mut out = HashMap::new();
for font in ctx.font_map.items() { context.resources.traverse(&mut |resources| {
let type0_ref = ctx.alloc.bump(); for font in resources.fonts.items() {
let cid_ref = ctx.alloc.bump(); if out.contains_key(font) {
let descriptor_ref = ctx.alloc.bump(); continue;
let cmap_ref = ctx.alloc.bump();
let data_ref = ctx.alloc.bump();
ctx.font_refs.push(type0_ref);
let glyph_set = ctx.glyph_sets.get_mut(font).unwrap();
let ttf = font.ttf();
// Do we have a TrueType or CFF font?
//
// FIXME: CFF2 must be handled differently and requires PDF 2.0
// (or we have to convert it to CFF).
let is_cff = ttf
.raw_face()
.table(CFF)
.or_else(|| ttf.raw_face().table(CFF2))
.is_some();
let postscript_name = font
.find_name(name_id::POST_SCRIPT_NAME)
.unwrap_or_else(|| "unknown".to_string());
let subset_tag = subset_tag(glyph_set);
let base_font = eco_format!("{subset_tag}+{postscript_name}");
let base_font_type0 = if is_cff {
eco_format!("{base_font}-Identity-H")
} else {
base_font.clone()
};
// Write the base font object referencing the CID font.
ctx.pdf
.type0_font(type0_ref)
.base_font(Name(base_font_type0.as_bytes()))
.encoding_predefined(Name(b"Identity-H"))
.descendant_font(cid_ref)
.to_unicode(cmap_ref);
// Write the CID font referencing the font descriptor.
let mut cid = ctx.pdf.cid_font(cid_ref);
cid.subtype(if is_cff { CidFontType::Type0 } else { CidFontType::Type2 });
cid.base_font(Name(base_font.as_bytes()));
cid.system_info(SYSTEM_INFO);
cid.font_descriptor(descriptor_ref);
cid.default_width(0.0);
if !is_cff {
cid.cid_to_gid_map_predefined(Name(b"Identity"));
}
// Extract the widths of all glyphs.
let mut widths = vec![];
for gid in std::iter::once(0).chain(glyph_set.keys().copied()) {
let width = ttf.glyph_hor_advance(GlyphId(gid)).unwrap_or(0);
let units = font.to_em(width).to_font_units();
let cid = glyph_cid(font, gid);
if usize::from(cid) >= widths.len() {
widths.resize(usize::from(cid) + 1, 0.0);
widths[usize::from(cid)] = units;
}
}
// Write all non-zero glyph widths.
let mut first = 0;
let mut width_writer = cid.widths();
for (w, group) in widths.group_by_key(|&w| w) {
let end = first + group.len();
if w != 0.0 {
let last = end - 1;
width_writer.same(first as u16, last as u16, w);
}
first = end;
}
width_writer.finish();
cid.finish();
// Write the /ToUnicode character map, which maps glyph ids back to
// unicode codepoints to enable copying out of the PDF.
let cmap = create_cmap(font, glyph_set);
ctx.pdf.cmap(cmap_ref, &cmap.finish());
// Subset and write the font's bytes.
let glyphs: Vec<_> = glyph_set.keys().copied().collect();
let data = subset_font(font, &glyphs);
let mut stream = ctx.pdf.stream(data_ref, &data);
stream.filter(Filter::FlateDecode);
if is_cff {
stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C"));
}
stream.finish();
let mut font_descriptor =
write_font_descriptor(&mut ctx.pdf, descriptor_ref, font, &base_font);
if is_cff {
font_descriptor.font_file3(data_ref);
} else {
font_descriptor.font_file2(data_ref);
}
}
}
/// Writes color fonts as Type3 fonts
fn write_color_fonts(ctx: &mut PdfContext) {
let color_font_map = ctx.color_font_map.take_map();
for (font, color_font) in color_font_map {
// For each Type3 font that is part of this family…
for (font_index, subfont_id) in color_font.refs.iter().enumerate() {
// Allocate some IDs.
let cmap_ref = ctx.alloc.bump();
let descriptor_ref = ctx.alloc.bump();
let widths_ref = ctx.alloc.bump();
// And a map between glyph IDs and the instructions to draw this
// glyph.
let mut glyphs_to_instructions = Vec::new();
let start = font_index * 256;
let end = (start + 256).min(color_font.glyphs.len());
let glyph_count = end - start;
let subset = &color_font.glyphs[start..end];
let mut widths = Vec::new();
let mut gids = Vec::new();
let scale_factor = font.ttf().units_per_em() as f32;
// Write the instructions for each glyph.
for color_glyph in subset {
let instructions_stream_ref = ctx.alloc.bump();
let width =
font.advance(color_glyph.gid).unwrap_or(Em::new(0.0)).to_font_units();
widths.push(width);
// Create a fake page context for `write_frame`. We are only
// interested in the contents of the page.
let size = color_glyph.frame.size();
let mut page_ctx = PageContext::new(ctx, size);
page_ctx.bottom = size.y.to_f32();
page_ctx.content.start_color_glyph(width);
page_ctx.transform(
// Make the Y axis go upwards, while preserving aspect ratio
Transform::scale(Ratio::one(), -size.aspect_ratio())
// Also move the origin to the top left corner
.post_concat(Transform::translate(Abs::zero(), size.y)),
);
write_frame(&mut page_ctx, &color_glyph.frame);
// Retrieve the stream of the page and write it.
let stream = page_ctx.content.finish();
ctx.pdf.stream(instructions_stream_ref, &stream);
// Use this stream as instructions to draw the glyph.
glyphs_to_instructions.push(instructions_stream_ref);
gids.push(color_glyph.gid);
} }
// Write the Type3 font object. let type0_ref = chunk.alloc();
let mut pdf_font = ctx.pdf.type3_font(*subfont_id); let cid_ref = chunk.alloc();
pdf_font.pair(Name(b"Resources"), ctx.type3_font_resources_ref); let descriptor_ref = chunk.alloc();
pdf_font.bbox(color_font.bbox); let cmap_ref = chunk.alloc();
pdf_font.matrix([1.0 / scale_factor, 0.0, 0.0, 1.0 / scale_factor, 0.0, 0.0]); let data_ref = chunk.alloc();
pdf_font.first_char(0); out.insert(font.clone(), type0_ref);
pdf_font.last_char((glyph_count - 1) as u8);
pdf_font.pair(Name(b"Widths"), widths_ref);
pdf_font.to_unicode(cmap_ref);
pdf_font.font_descriptor(descriptor_ref);
// Write the /CharProcs dictionary, that maps glyph names to let glyph_set = resources.glyph_sets.get(font).unwrap();
// drawing instructions. let ttf = font.ttf();
let mut char_procs = pdf_font.char_procs();
for (gid, instructions_ref) in glyphs_to_instructions.iter().enumerate() {
char_procs
.pair(Name(eco_format!("glyph{gid}").as_bytes()), *instructions_ref);
}
char_procs.finish();
// Write the /Encoding dictionary. // Do we have a TrueType or CFF font?
let names = (0..glyph_count) //
.map(|gid| eco_format!("glyph{gid}")) // FIXME: CFF2 must be handled differently and requires PDF 2.0
.collect::<Vec<_>>(); // (or we have to convert it to CFF).
pdf_font let is_cff = ttf
.encoding_custom() .raw_face()
.differences() .table(CFF)
.consecutive(0, names.iter().map(|name| Name(name.as_bytes()))); .or_else(|| ttf.raw_face().table(CFF2))
pdf_font.finish(); .is_some();
// Encode a CMAP to make it possible to search or copy glyphs.
let glyph_set = ctx.glyph_sets.get_mut(&font).unwrap();
let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO);
for (index, glyph) in subset.iter().enumerate() {
let Some(text) = glyph_set.get(&glyph.gid) else {
continue;
};
if !text.is_empty() {
cmap.pair_with_multiple(index as u8, text.chars());
}
}
ctx.pdf.cmap(cmap_ref, &cmap.finish());
// Write the font descriptor.
gids.sort();
let subset_tag = subset_tag(&gids);
let postscript_name = font let postscript_name = font
.find_name(name_id::POST_SCRIPT_NAME) .find_name(name_id::POST_SCRIPT_NAME)
.unwrap_or_else(|| "unknown".to_string()); .unwrap_or_else(|| "unknown".to_string());
let base_font = eco_format!("{subset_tag}+{postscript_name}");
write_font_descriptor(&mut ctx.pdf, descriptor_ref, &font, &base_font);
// Write the widths array let subset_tag = subset_tag(glyph_set);
ctx.pdf.indirect(widths_ref).array().items(widths); let base_font = eco_format!("{subset_tag}+{postscript_name}");
let base_font_type0 = if is_cff {
eco_format!("{base_font}-Identity-H")
} else {
base_font.clone()
};
// Write the base font object referencing the CID font.
chunk
.type0_font(type0_ref)
.base_font(Name(base_font_type0.as_bytes()))
.encoding_predefined(Name(b"Identity-H"))
.descendant_font(cid_ref)
.to_unicode(cmap_ref);
// Write the CID font referencing the font descriptor.
let mut cid = chunk.cid_font(cid_ref);
cid.subtype(if is_cff { CidFontType::Type0 } else { CidFontType::Type2 });
cid.base_font(Name(base_font.as_bytes()));
cid.system_info(SYSTEM_INFO);
cid.font_descriptor(descriptor_ref);
cid.default_width(0.0);
if !is_cff {
cid.cid_to_gid_map_predefined(Name(b"Identity"));
}
// Extract the widths of all glyphs.
let mut widths = vec![];
for gid in std::iter::once(0).chain(glyph_set.keys().copied()) {
let width = ttf.glyph_hor_advance(GlyphId(gid)).unwrap_or(0);
let units = font.to_em(width).to_font_units();
let cid = glyph_cid(font, gid);
if usize::from(cid) >= widths.len() {
widths.resize(usize::from(cid) + 1, 0.0);
widths[usize::from(cid)] = units;
}
}
// Write all non-zero glyph widths.
let mut first = 0;
let mut width_writer = cid.widths();
for (w, group) in widths.group_by_key(|&w| w) {
let end = first + group.len();
if w != 0.0 {
let last = end - 1;
width_writer.same(first as u16, last as u16, w);
}
first = end;
}
width_writer.finish();
cid.finish();
// Write the /ToUnicode character map, which maps glyph ids back to
// unicode codepoints to enable copying out of the PDF.
let cmap = create_cmap(font, glyph_set);
chunk.cmap(cmap_ref, &cmap.finish());
// Subset and write the font's bytes.
let glyphs: Vec<_> = glyph_set.keys().copied().collect();
let data = subset_font(font, &glyphs);
let mut stream = chunk.stream(data_ref, &data);
stream.filter(Filter::FlateDecode);
if is_cff {
stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C"));
}
stream.finish();
let mut font_descriptor =
write_font_descriptor(&mut chunk, descriptor_ref, font, &base_font);
if is_cff {
font_descriptor.font_file3(data_ref);
} else {
font_descriptor.font_file2(data_ref);
}
} }
} });
(chunk, out)
} }
/// Writes a FontDescriptor dictionary. /// Writes a FontDescriptor dictionary.
fn write_font_descriptor<'a>( pub fn write_font_descriptor<'a>(
pdf: &'a mut pdf_writer::Pdf, pdf: &'a mut Chunk,
descriptor_ref: pdf_writer::Ref, descriptor_ref: Ref,
font: &'a Font, font: &'a Font,
base_font: &EcoString, base_font: &EcoString,
) -> FontDescriptor<'a> { ) -> FontDescriptor<'a> {
@ -317,7 +214,7 @@ fn subset_font(font: &Font, glyphs: &[u16]) -> Arc<Vec<u8>> {
} }
/// Produce a unique 6 letter tag for a glyph set. /// Produce a unique 6 letter tag for a glyph set.
fn subset_tag<T: Hash>(glyphs: &T) -> EcoString { pub(crate) fn subset_tag<T: Hash>(glyphs: &T) -> EcoString {
const LEN: usize = 6; const LEN: usize = 6;
const BASE: u128 = 26; const BASE: u128 = 26;
let mut hash = typst::utils::hash128(&glyphs); let mut hash = typst::utils::hash128(&glyphs);
@ -329,33 +226,40 @@ fn subset_tag<T: Hash>(glyphs: &T) -> EcoString {
std::str::from_utf8(&letter).unwrap().into() std::str::from_utf8(&letter).unwrap().into()
} }
/// Create a /ToUnicode CMap. /// For glyphs that have codepoints mapping to them in the font's cmap table, we
fn create_cmap(font: &Font, glyph_set: &mut BTreeMap<u16, EcoString>) -> UnicodeCmap { /// prefer them over pre-existing text mappings from the document. Only things
let ttf = font.ttf(); /// that don't have a corresponding codepoint (or only a private-use one) like
/// the "Th" in Linux Libertine get the text of their first occurrences in the
/// document instead.
///
/// This function replaces as much copepoints from the document with ones from
/// the cmap table as possible.
pub fn improve_glyph_sets(glyph_sets: &mut HashMap<Font, BTreeMap<u16, EcoString>>) {
for (font, glyph_set) in glyph_sets {
let ttf = font.ttf();
// For glyphs that have codepoints mapping to them in the font's cmap table, for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) {
// we prefer them over pre-existing text mappings from the document. Only if !subtable.is_unicode() {
// things that don't have a corresponding codepoint (or only a private-use continue;
// one) like the "Th" in Linux Libertine get the text of their first }
// occurrences in the document instead.
for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) { subtable.codepoints(|n| {
if !subtable.is_unicode() { let Some(c) = std::char::from_u32(n) else { return };
continue; if c.general_category() == GeneralCategory::PrivateUse {
return;
}
let Some(GlyphId(g)) = ttf.glyph_index(c) else { return };
if glyph_set.contains_key(&g) {
glyph_set.insert(g, c.into());
}
});
} }
subtable.codepoints(|n| {
let Some(c) = std::char::from_u32(n) else { return };
if c.general_category() == GeneralCategory::PrivateUse {
return;
}
let Some(GlyphId(g)) = ttf.glyph_index(c) else { return };
if glyph_set.contains_key(&g) {
glyph_set.insert(g, c.into());
}
});
} }
}
/// Create a /ToUnicode CMap.
fn create_cmap(font: &Font, glyph_set: &BTreeMap<u16, EcoString>) -> UnicodeCmap {
// Produce a reverse mapping from glyphs' CIDs to unicode strings. // Produce a reverse mapping from glyphs' CIDs to unicode strings.
let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO); let mut cmap = UnicodeCmap::new(CMAP_NAME, SYSTEM_INFO);
for (&g, text) in glyph_set.iter() { for (&g, text) in glyph_set.iter() {

View File

@ -1,19 +1,23 @@
use std::collections::HashMap;
use std::f32::consts::{PI, TAU}; use std::f32::consts::{PI, TAU};
use std::sync::Arc; use std::sync::Arc;
use ecow::eco_format; use ecow::eco_format;
use pdf_writer::types::{ColorSpaceOperand, FunctionShadingType}; use pdf_writer::{
use pdf_writer::writers::StreamShadingType; types::{ColorSpaceOperand, FunctionShadingType},
use pdf_writer::{Filter, Finish, Name, Ref}; writers::StreamShadingType,
Filter, Finish, Name, Ref,
};
use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform}; use typst::layout::{Abs, Angle, Point, Quadrant, Ratio, Transform};
use typst::utils::Numeric; use typst::utils::Numeric;
use typst::visualize::{ use typst::visualize::{
Color, ColorSpace, Gradient, RatioOrAngle, RelativeTo, WeightedColor, Color, ColorSpace, Gradient, RatioOrAngle, RelativeTo, WeightedColor,
}; };
use crate::color::{ColorSpaceExt, PaintEncode, QuantizedColor}; use crate::color::{self, ColorSpaceExt, PaintEncode, QuantizedColor};
use crate::page::{PageContext, PageResource, ResourceKind, Transforms}; use crate::{content, WithGlobalRefs};
use crate::{deflate, transform_to_array, AbsExt, PdfContext}; use crate::{deflate, transform_to_array, AbsExt, PdfChunk};
/// A unique-transform-aspect-ratio combination that will be encoded into the /// A unique-transform-aspect-ratio combination that will be encoded into the
/// PDF. /// PDF.
@ -32,122 +36,144 @@ pub struct PdfGradient {
/// Writes the actual gradients (shading patterns) to the PDF. /// Writes the actual gradients (shading patterns) to the PDF.
/// This is performed once after writing all pages. /// This is performed once after writing all pages.
pub(crate) fn write_gradients(ctx: &mut PdfContext) { pub fn write_gradients(
for PdfGradient { transform, aspect_ratio, gradient, angle } in context: &WithGlobalRefs,
ctx.gradient_map.items().cloned().collect::<Vec<_>>() ) -> (PdfChunk, HashMap<PdfGradient, Ref>) {
{ let mut chunk = PdfChunk::new();
let shading = ctx.alloc.bump(); let mut out = HashMap::new();
ctx.gradient_refs.push(shading); context.resources.traverse(&mut |resources| {
for pdf_gradient in resources.gradients.items() {
let color_space = if gradient.space().hue_index().is_some() { if out.contains_key(pdf_gradient) {
ColorSpace::Oklab continue;
} else {
gradient.space()
};
let mut shading_pattern = match &gradient {
Gradient::Linear(_) => {
let shading_function = shading_function(ctx, &gradient, color_space);
let mut shading_pattern = ctx.pdf.shading_pattern(shading);
let mut shading = shading_pattern.function_shading();
shading.shading_type(FunctionShadingType::Axial);
ctx.colors.write(color_space, shading.color_space(), &mut ctx.alloc);
let (mut sin, mut cos) = (angle.sin(), angle.cos());
// Scale to edges of unit square.
let factor = cos.abs() + sin.abs();
sin *= factor;
cos *= factor;
let (x1, y1, x2, y2): (f64, f64, f64, f64) = match angle.quadrant() {
Quadrant::First => (0.0, 0.0, cos, sin),
Quadrant::Second => (1.0, 0.0, cos + 1.0, sin),
Quadrant::Third => (1.0, 1.0, cos + 1.0, sin + 1.0),
Quadrant::Fourth => (0.0, 1.0, cos, sin + 1.0),
};
shading
.anti_alias(gradient.anti_alias())
.function(shading_function)
.coords([x1 as f32, y1 as f32, x2 as f32, y2 as f32])
.extend([true; 2]);
shading.finish();
shading_pattern
} }
Gradient::Radial(radial) => {
let shading_function = shading_function(ctx, &gradient, color_space);
let mut shading_pattern = ctx.pdf.shading_pattern(shading);
let mut shading = shading_pattern.function_shading();
shading.shading_type(FunctionShadingType::Radial);
ctx.colors.write(color_space, shading.color_space(), &mut ctx.alloc); let shading = chunk.alloc();
out.insert(pdf_gradient.clone(), shading);
shading let PdfGradient { transform, aspect_ratio, gradient, angle } = pdf_gradient;
.anti_alias(gradient.anti_alias())
.function(shading_function)
.coords([
radial.focal_center.x.get() as f32,
radial.focal_center.y.get() as f32,
radial.focal_radius.get() as f32,
radial.center.x.get() as f32,
radial.center.y.get() as f32,
radial.radius.get() as f32,
])
.extend([true; 2]);
shading.finish(); let color_space = if gradient.space().hue_index().is_some() {
ColorSpace::Oklab
} else {
gradient.space()
};
shading_pattern let mut shading_pattern = match &gradient {
} Gradient::Linear(_) => {
Gradient::Conic(_) => { let shading_function =
let vertices = compute_vertex_stream(&gradient, aspect_ratio); shading_function(gradient, &mut chunk, color_space);
let mut shading_pattern = chunk.chunk.shading_pattern(shading);
let mut shading = shading_pattern.function_shading();
shading.shading_type(FunctionShadingType::Axial);
let stream_shading_id = ctx.alloc.bump(); color::write(
let mut stream_shading = color_space,
ctx.pdf.stream_shading(stream_shading_id, &vertices); shading.color_space(),
&context.globals.color_functions,
);
ctx.colors.write( let (mut sin, mut cos) = (angle.sin(), angle.cos());
color_space,
stream_shading.color_space(),
&mut ctx.alloc,
);
let range = color_space.range(); // Scale to edges of unit square.
stream_shading let factor = cos.abs() + sin.abs();
.bits_per_coordinate(16) sin *= factor;
.bits_per_component(16) cos *= factor;
.bits_per_flag(8)
.shading_type(StreamShadingType::CoonsPatch)
.decode([
0.0, 1.0, 0.0, 1.0, range[0], range[1], range[2], range[3],
range[4], range[5],
])
.anti_alias(gradient.anti_alias())
.filter(Filter::FlateDecode);
stream_shading.finish(); let (x1, y1, x2, y2): (f64, f64, f64, f64) = match angle.quadrant() {
Quadrant::First => (0.0, 0.0, cos, sin),
Quadrant::Second => (1.0, 0.0, cos + 1.0, sin),
Quadrant::Third => (1.0, 1.0, cos + 1.0, sin + 1.0),
Quadrant::Fourth => (0.0, 1.0, cos, sin + 1.0),
};
let mut shading_pattern = ctx.pdf.shading_pattern(shading); shading
shading_pattern.shading_ref(stream_shading_id); .anti_alias(gradient.anti_alias())
shading_pattern .function(shading_function)
} .coords([x1 as f32, y1 as f32, x2 as f32, y2 as f32])
}; .extend([true; 2]);
shading_pattern.matrix(transform_to_array(transform)); shading.finish();
}
shading_pattern
}
Gradient::Radial(radial) => {
let shading_function =
shading_function(gradient, &mut chunk, color_space_of(gradient));
let mut shading_pattern = chunk.chunk.shading_pattern(shading);
let mut shading = shading_pattern.function_shading();
shading.shading_type(FunctionShadingType::Radial);
color::write(
color_space,
shading.color_space(),
&context.globals.color_functions,
);
shading
.anti_alias(gradient.anti_alias())
.function(shading_function)
.coords([
radial.focal_center.x.get() as f32,
radial.focal_center.y.get() as f32,
radial.focal_radius.get() as f32,
radial.center.x.get() as f32,
radial.center.y.get() as f32,
radial.radius.get() as f32,
])
.extend([true; 2]);
shading.finish();
shading_pattern
}
Gradient::Conic(_) => {
let vertices = compute_vertex_stream(gradient, *aspect_ratio);
let stream_shading_id = chunk.alloc();
let mut stream_shading =
chunk.chunk.stream_shading(stream_shading_id, &vertices);
color::write(
color_space,
stream_shading.color_space(),
&context.globals.color_functions,
);
let range = color_space.range();
stream_shading
.bits_per_coordinate(16)
.bits_per_component(16)
.bits_per_flag(8)
.shading_type(StreamShadingType::CoonsPatch)
.decode([
0.0, 1.0, 0.0, 1.0, range[0], range[1], range[2], range[3],
range[4], range[5],
])
.anti_alias(gradient.anti_alias())
.filter(Filter::FlateDecode);
stream_shading.finish();
let mut shading_pattern = chunk.shading_pattern(shading);
shading_pattern.shading_ref(stream_shading_id);
shading_pattern
}
};
shading_pattern.matrix(transform_to_array(*transform));
}
});
(chunk, out)
} }
/// Writes an expotential or stitched function that expresses the gradient. /// Writes an expotential or stitched function that expresses the gradient.
fn shading_function( fn shading_function(
ctx: &mut PdfContext,
gradient: &Gradient, gradient: &Gradient,
chunk: &mut PdfChunk,
color_space: ColorSpace, color_space: ColorSpace,
) -> Ref { ) -> Ref {
let function = ctx.alloc.bump(); let function = chunk.alloc();
let mut functions = vec![]; let mut functions = vec![];
let mut bounds = vec![]; let mut bounds = vec![];
let mut encode = vec![]; let mut encode = vec![];
@ -166,7 +192,7 @@ fn shading_function(
let real_t = first.1.get() * (1.0 - t) + second.1.get() * t; let real_t = first.1.get() * (1.0 - t) + second.1.get() * t;
let c = gradient.sample(RatioOrAngle::Ratio(Ratio::new(real_t))); let c = gradient.sample(RatioOrAngle::Ratio(Ratio::new(real_t)));
functions.push(single_gradient(ctx, last_c, c, color_space)); functions.push(single_gradient(chunk, last_c, c, color_space));
bounds.push(real_t as f32); bounds.push(real_t as f32);
encode.extend([0.0, 1.0]); encode.extend([0.0, 1.0]);
last_c = c; last_c = c;
@ -174,7 +200,7 @@ fn shading_function(
} }
bounds.push(second.1.get() as f32); bounds.push(second.1.get() as f32);
functions.push(single_gradient(ctx, first.0, second.0, color_space)); functions.push(single_gradient(chunk, first.0, second.0, color_space));
encode.extend([0.0, 1.0]); encode.extend([0.0, 1.0]);
} }
@ -187,7 +213,7 @@ fn shading_function(
bounds.pop(); bounds.pop();
// Create the stitching function. // Create the stitching function.
ctx.pdf chunk
.stitching_function(function) .stitching_function(function)
.domain([0.0, 1.0]) .domain([0.0, 1.0])
.range(color_space.range()) .range(color_space.range())
@ -201,14 +227,13 @@ fn shading_function(
/// Writes an expontential function that expresses a single segment (between two /// Writes an expontential function that expresses a single segment (between two
/// stops) of a gradient. /// stops) of a gradient.
fn single_gradient( fn single_gradient(
ctx: &mut PdfContext, chunk: &mut PdfChunk,
first_color: Color, first_color: Color,
second_color: Color, second_color: Color,
color_space: ColorSpace, color_space: ColorSpace,
) -> Ref { ) -> Ref {
let reference = ctx.alloc.bump(); let reference = chunk.alloc();
chunk
ctx.pdf
.exponential_function(reference) .exponential_function(reference)
.range(color_space.range()) .range(color_space.range())
.c0(color_space.convert(first_color)) .c0(color_space.convert(first_color))
@ -220,7 +245,12 @@ fn single_gradient(
} }
impl PaintEncode for Gradient { impl PaintEncode for Gradient {
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) { fn set_as_fill(
&self,
ctx: &mut content::Builder,
on_text: bool,
transforms: content::Transforms,
) {
ctx.reset_fill_color_space(); ctx.reset_fill_color_space();
let index = register_gradient(ctx, self, on_text, transforms); let index = register_gradient(ctx, self, on_text, transforms);
@ -229,15 +259,13 @@ impl PaintEncode for Gradient {
ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_fill_pattern(None, name); ctx.content.set_fill_pattern(None, name);
ctx.resources
.insert(PageResource::new(ResourceKind::Gradient, id), index);
} }
fn set_as_stroke( fn set_as_stroke(
&self, &self,
ctx: &mut PageContext, ctx: &mut content::Builder,
on_text: bool, on_text: bool,
transforms: Transforms, transforms: content::Transforms,
) { ) {
ctx.reset_stroke_color_space(); ctx.reset_stroke_color_space();
@ -247,17 +275,15 @@ impl PaintEncode for Gradient {
ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_stroke_pattern(None, name); ctx.content.set_stroke_pattern(None, name);
ctx.resources
.insert(PageResource::new(ResourceKind::Gradient, id), index);
} }
} }
/// Deduplicates a gradient to a named PDF resource. /// Deduplicates a gradient to a named PDF resource.
fn register_gradient( fn register_gradient(
ctx: &mut PageContext, ctx: &mut content::Builder,
gradient: &Gradient, gradient: &Gradient,
on_text: bool, on_text: bool,
mut transforms: Transforms, mut transforms: content::Transforms,
) -> usize { ) -> usize {
// Edge cases for strokes. // Edge cases for strokes.
if transforms.size.x.is_zero() { if transforms.size.x.is_zero() {
@ -307,7 +333,9 @@ fn register_gradient(
angle: Gradient::correct_aspect_ratio(rotation, size.aspect_ratio()), angle: Gradient::correct_aspect_ratio(rotation, size.aspect_ratio()),
}; };
ctx.parent.gradient_map.insert(pdf_gradient) ctx.resources.colors.mark_as_used(color_space_of(gradient));
ctx.resources.gradients.insert(pdf_gradient)
} }
/// Writes a single Coons Patch as defined in the PDF specification /// Writes a single Coons Patch as defined in the PDF specification
@ -466,3 +494,11 @@ fn compute_vertex_stream(gradient: &Gradient, aspect_ratio: Ratio) -> Arc<Vec<u8
Arc::new(deflate(&vertices)) Arc::new(deflate(&vertices))
} }
fn color_space_of(gradient: &Gradient) -> ColorSpace {
if gradient.space().hue_index().is_some() {
ColorSpace::Oklab
} else {
gradient.space()
}
}

View File

@ -8,14 +8,119 @@ use typst::visualize::{
ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage, ColorSpace, Image, ImageKind, RasterFormat, RasterImage, SvgImage,
}; };
use crate::{deflate, PdfContext}; use crate::{color, deflate, PdfChunk, WithGlobalRefs};
/// Embed all used images into the PDF.
#[typst_macros::time(name = "write images")]
pub fn write_images(context: &WithGlobalRefs) -> (PdfChunk, HashMap<Image, Ref>) {
let mut chunk = PdfChunk::new();
let mut out = HashMap::new();
context.resources.traverse(&mut |resources| {
for (i, image) in resources.images.items().enumerate() {
if out.contains_key(image) {
continue;
}
let handle = resources.deferred_images.get(&i).unwrap();
match handle.wait() {
EncodedImage::Raster {
data,
filter,
has_color,
width,
height,
icc,
alpha,
} => {
let image_ref = chunk.alloc();
out.insert(image.clone(), image_ref);
let mut image = chunk.chunk.image_xobject(image_ref, data);
image.filter(*filter);
image.width(*width as i32);
image.height(*height as i32);
image.bits_per_component(8);
let mut icc_ref = None;
let space = image.color_space();
if icc.is_some() {
let id = chunk.alloc.bump();
space.icc_based(id);
icc_ref = Some(id);
} else if *has_color {
color::write(
ColorSpace::Srgb,
space,
&context.globals.color_functions,
);
} else {
color::write(
ColorSpace::D65Gray,
space,
&context.globals.color_functions,
);
}
// Add a second gray-scale image containing the alpha values if
// this image has an alpha channel.
if let Some((alpha_data, alpha_filter)) = alpha {
let mask_ref = chunk.alloc.bump();
image.s_mask(mask_ref);
image.finish();
let mut mask = chunk.image_xobject(mask_ref, alpha_data);
mask.filter(*alpha_filter);
mask.width(*width as i32);
mask.height(*height as i32);
mask.color_space().device_gray();
mask.bits_per_component(8);
} else {
image.finish();
}
if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) {
let mut stream = chunk.icc_profile(icc_ref, icc);
stream.filter(Filter::FlateDecode);
if *has_color {
stream.n(3);
stream.alternate().srgb();
} else {
stream.n(1);
stream.alternate().d65_gray();
}
}
}
EncodedImage::Svg(svg_chunk) => {
let mut map = HashMap::new();
svg_chunk.renumber_into(&mut chunk.chunk, |old| {
*map.entry(old).or_insert_with(|| chunk.alloc.bump())
});
out.insert(image.clone(), map[&Ref::new(1)]);
}
}
}
});
(chunk, out)
}
/// Creates a new PDF image from the given image. /// Creates a new PDF image from the given image.
/// ///
/// Also starts the deferred encoding of the image. /// Also starts the deferred encoding of the image.
#[comemo::memoize] #[comemo::memoize]
pub fn deferred_image(image: Image) -> Deferred<EncodedImage> { pub fn deferred_image(image: Image) -> (Deferred<EncodedImage>, Option<ColorSpace>) {
Deferred::new(move || match image.kind() { let color_space = match image.kind() {
ImageKind::Raster(raster) if raster.icc().is_none() => {
if raster.dynamic().color().channel_count() > 2 {
Some(ColorSpace::Srgb)
} else {
Some(ColorSpace::D65Gray)
}
}
_ => None,
};
let deferred = Deferred::new(move || match image.kind() {
ImageKind::Raster(raster) => { ImageKind::Raster(raster) => {
let raster = raster.clone(); let raster = raster.clone();
let (width, height) = (raster.width(), raster.height()); let (width, height) = (raster.width(), raster.height());
@ -28,83 +133,9 @@ pub fn deferred_image(image: Image) -> Deferred<EncodedImage> {
EncodedImage::Raster { data, filter, has_color, width, height, icc, alpha } EncodedImage::Raster { data, filter, has_color, width, height, icc, alpha }
} }
ImageKind::Svg(svg) => EncodedImage::Svg(encode_svg(svg)), ImageKind::Svg(svg) => EncodedImage::Svg(encode_svg(svg)),
}) });
}
/// Embed all used images into the PDF. (deferred, color_space)
#[typst_macros::time(name = "write images")]
pub(crate) fn write_images(ctx: &mut PdfContext) {
for (i, _) in ctx.image_map.items().enumerate() {
let handle = ctx.image_deferred_map.get(&i).unwrap();
match handle.wait() {
EncodedImage::Raster {
data,
filter,
has_color,
width,
height,
icc,
alpha,
} => {
let image_ref = ctx.alloc.bump();
ctx.image_refs.push(image_ref);
let mut image = ctx.pdf.image_xobject(image_ref, data);
image.filter(*filter);
image.width(*width as i32);
image.height(*height as i32);
image.bits_per_component(8);
let mut icc_ref = None;
let space = image.color_space();
if icc.is_some() {
let id = ctx.alloc.bump();
space.icc_based(id);
icc_ref = Some(id);
} else if *has_color {
ctx.colors.write(ColorSpace::Srgb, space, &mut ctx.alloc);
} else {
ctx.colors.write(ColorSpace::D65Gray, space, &mut ctx.alloc);
}
// Add a second gray-scale image containing the alpha values if
// this image has an alpha channel.
if let Some((alpha_data, alpha_filter)) = alpha {
let mask_ref = ctx.alloc.bump();
image.s_mask(mask_ref);
image.finish();
let mut mask = ctx.pdf.image_xobject(mask_ref, alpha_data);
mask.filter(*alpha_filter);
mask.width(*width as i32);
mask.height(*height as i32);
mask.color_space().device_gray();
mask.bits_per_component(8);
} else {
image.finish();
}
if let (Some(icc), Some(icc_ref)) = (icc, icc_ref) {
let mut stream = ctx.pdf.icc_profile(icc_ref, icc);
stream.filter(Filter::FlateDecode);
if *has_color {
stream.n(3);
stream.alternate().srgb();
} else {
stream.n(1);
stream.alternate().d65_gray();
}
}
}
EncodedImage::Svg(chunk) => {
let mut map = HashMap::new();
chunk.renumber_into(&mut ctx.pdf, |old| {
*map.entry(old).or_insert_with(|| ctx.alloc.bump())
});
ctx.image_refs.push(map[&Ref::new(1)]);
}
}
}
} }
/// Encode an image with a suitable filter and return the data, filter and /// Encode an image with a suitable filter and return the data, filter and

View File

@ -1,40 +1,45 @@
//! Exporting of Typst documents into PDFs. //! Exporting of Typst documents into PDFs.
mod catalog;
mod color; mod color;
mod color_font;
mod content;
mod extg; mod extg;
mod font; mod font;
mod gradient; mod gradient;
mod image; mod image;
mod named_destination;
mod outline; mod outline;
mod page; mod page;
mod pattern; mod pattern;
mod resources;
use std::collections::{BTreeMap, HashMap, HashSet}; use std::collections::HashMap;
use std::hash::Hash; use std::hash::Hash;
use std::sync::Arc; use std::ops::{Deref, DerefMut};
use base64::Engine; use base64::Engine;
use ecow::{eco_format, EcoString}; use pdf_writer::{Chunk, Pdf, Ref};
use indexmap::IndexMap; use typst::foundations::{Datetime, Smart};
use pdf_writer::types::Direction; use typst::layout::{Abs, Em, PageRanges, Transform};
use pdf_writer::writers::Destination; use typst::model::Document;
use pdf_writer::{Finish, Name, Pdf, Rect, Ref, Str, TextStr}; use typst::text::Font;
use typst::foundations::{Datetime, Label, NativeElement, Smart};
use typst::introspection::Location;
use typst::layout::{Abs, Dir, Em, Frame, PageRanges, Transform};
use typst::model::{Document, HeadingElem};
use typst::text::color::frame_for_glyph;
use typst::text::{Font, Lang};
use typst::utils::Deferred; use typst::utils::Deferred;
use typst::visualize::Image; use typst::visualize::Image;
use xmp_writer::{DateTime, LangId, RenditionClass, Timezone, XmpWriter};
use crate::color::ColorSpaces; use crate::catalog::write_catalog;
use crate::extg::ExtGState; use crate::color::{alloc_color_functions_refs, ColorFunctionRefs};
use crate::gradient::PdfGradient; use crate::color_font::{write_color_fonts, ColorFontSlice};
use crate::image::EncodedImage; use crate::extg::{write_graphic_states, ExtGState};
use crate::page::EncodedPage; use crate::font::write_fonts;
use crate::pattern::PdfPattern; use crate::gradient::{write_gradients, PdfGradient};
use crate::image::write_images;
use crate::named_destination::{write_named_destinations, NamedDestinations};
use crate::page::{alloc_page_refs, traverse_pages, write_page_tree, EncodedPage};
use crate::pattern::{write_patterns, PdfPattern};
use crate::resources::{
alloc_resources_refs, write_resource_dictionaries, Resources, ResourcesRefs,
};
/// Export a document into a PDF file. /// Export a document into a PDF file.
/// ///
@ -65,311 +70,389 @@ pub fn pdf(
timestamp: Option<Datetime>, timestamp: Option<Datetime>,
page_ranges: Option<PageRanges>, page_ranges: Option<PageRanges>,
) -> Vec<u8> { ) -> Vec<u8> {
let mut ctx = PdfContext::new(document, page_ranges); PdfBuilder::new(document, page_ranges)
page::construct_pages(&mut ctx, &document.pages); .phase(|builder| builder.run(traverse_pages))
font::write_fonts(&mut ctx); .phase(|builder| GlobalRefs {
image::write_images(&mut ctx); color_functions: builder.run(alloc_color_functions_refs),
gradient::write_gradients(&mut ctx); pages: builder.run(alloc_page_refs),
extg::write_external_graphics_states(&mut ctx); resources: builder.run(alloc_resources_refs),
pattern::write_patterns(&mut ctx); })
write_named_destinations(&mut ctx); .phase(|builder| References {
page::write_page_tree(&mut ctx); named_destinations: builder.run(write_named_destinations),
page::write_global_resources(&mut ctx); fonts: builder.run(write_fonts),
write_catalog(&mut ctx, ident, timestamp); color_fonts: builder.run(write_color_fonts),
ctx.pdf.finish() images: builder.run(write_images),
gradients: builder.run(write_gradients),
patterns: builder.run(write_patterns),
ext_gs: builder.run(write_graphic_states),
})
.phase(|builder| builder.run(write_page_tree))
.phase(|builder| builder.run(write_resource_dictionaries))
.export_with(ident, timestamp, write_catalog)
} }
/// Context for exporting a whole PDF document. /// A struct to build a PDF following a fixed succession of phases.
struct PdfContext<'a> { ///
/// The document that we're currently exporting. /// This type uses generics to represent its current state. `S` (for "state") is
document: &'a Document, /// all data that was produced by the previous phases, that is now read-only.
/// The writer we are writing the PDF into. ///
/// Phase after phase, this state will be transformed. Each phase corresponds to
/// a call to the [eponymous function](`PdfBuilder::phase`) and produces a new
/// part of the state, that will be aggregated with all other information, for
/// consumption during the next phase.
///
/// In other words: this struct follows the **typestate pattern**. This prevents
/// you from using data that is not yet available, at the type level.
///
/// Each phase consists of processes, that can read the state of the previous
/// phases, and construct a part of the new state.
///
/// A final step, that has direct access to the global reference allocator and
/// PDF document, can be run with [`PdfBuilder::export_with`].
struct PdfBuilder<S> {
/// The context that has been accumulated so far.
state: S,
/// A global bump allocator.
alloc: Ref,
/// The PDF document that is being written.
pdf: Pdf, pdf: Pdf,
/// Content of exported pages. }
pages: Vec<Option<EncodedPage>>,
/// The initial state: we are exploring the document, collecting all resources
/// that will be necessary later. The content of the pages is also built during
/// this phase.
struct WithDocument<'a> {
/// The Typst document that is exported.
document: &'a Document,
/// Page ranges to export. /// Page ranges to export.
/// When `None`, all pages are exported. /// When `None`, all pages are exported.
exported_pages: Option<PageRanges>, exported_pages: Option<PageRanges>,
/// For each font a mapping from used glyphs to their text representation. }
/// May contain multiple chars in case of ligatures or similar things. The
/// same glyph can have a different text representation within one document,
/// then we just save the first one. The resulting strings are used for the
/// PDF's /ToUnicode map for glyphs that don't have an entry in the font's
/// cmap. This is important for copy-paste and searching.
glyph_sets: HashMap<Font, BTreeMap<u16, EcoString>>,
/// The number of glyphs for all referenced languages in the document.
/// We keep track of this to determine the main document language.
/// BTreeMap is used to write sorted list of languages to metadata.
languages: BTreeMap<Lang, usize>,
/// Allocator for indirect reference IDs. /// At this point, resources were listed, but they don't have any reference
alloc: Ref, /// associated with them.
/// The ID of the page tree. ///
page_tree_ref: Ref, /// This phase allocates some global references.
/// The ID of the globally shared Resources dictionary. struct WithResources<'a> {
global_resources_ref: Ref, document: &'a Document,
/// The ID of the resource dictionary shared by Type3 fonts. exported_pages: Option<PageRanges>,
/// The content of the pages encoded as PDF content streams.
/// ///
/// Type3 fonts cannot use the global resources, as it would create some /// The pages are at the index corresponding to their page number, but they
/// kind of infinite recursion (they are themselves present in that /// may be `None` if they are not in the range specified by
/// dictionary), which Acrobat doesn't appreciate (it fails to parse the /// `exported_pages`.
/// font) even if the specification seems to allow it. pages: Vec<Option<EncodedPage>>,
type3_font_resources_ref: Ref, /// The PDF resources that are used in the content of the pages.
/// The IDs of written fonts. resources: Resources<()>,
font_refs: Vec<Ref>,
/// The IDs of written images.
image_refs: Vec<Ref>,
/// The IDs of written gradients.
gradient_refs: Vec<Ref>,
/// The IDs of written patterns.
pattern_refs: Vec<Ref>,
/// The IDs of written external graphics states.
ext_gs_refs: Vec<Ref>,
/// Handles color space writing.
colors: ColorSpaces,
/// Deduplicates fonts used across the document.
font_map: Remapper<Font>,
/// Deduplicates images used across the document.
image_map: Remapper<Image>,
/// Handles to deferred image conversions.
image_deferred_map: HashMap<usize, Deferred<EncodedImage>>,
/// Deduplicates gradients used across the document.
gradient_map: Remapper<PdfGradient>,
/// Deduplicates patterns used across the document.
pattern_map: Remapper<PdfPattern>,
/// Deduplicates external graphics states used across the document.
extg_map: Remapper<ExtGState>,
/// Deduplicates color glyphs.
color_font_map: ColorFontMap,
/// A sorted list of all named destinations.
dests: Vec<(Label, Ref)>,
/// Maps from locations to named destinations that point to them.
loc_to_dest: HashMap<Location, Label>,
} }
impl<'a> PdfContext<'a> { /// Global references.
fn new(document: &'a Document, page_ranges: Option<PageRanges>) -> Self { struct GlobalRefs {
let mut alloc = Ref::new(1); /// References for color conversion functions.
let page_tree_ref = alloc.bump(); color_functions: ColorFunctionRefs,
let global_resources_ref = alloc.bump(); /// Reference for pages.
let type3_font_resources_ref = alloc.bump(); ///
/// Items of this vector are `None` if the corresponding page is not
/// exported.
pages: Vec<Option<Ref>>,
/// References for the resource dictionaries.
resources: ResourcesRefs,
}
impl<'a> From<(WithDocument<'a>, (Vec<Option<EncodedPage>>, Resources<()>))>
for WithResources<'a>
{
fn from(
(previous, (pages, resources)): (
WithDocument<'a>,
(Vec<Option<EncodedPage>>, Resources<()>),
),
) -> Self {
Self { Self {
document, document: previous.document,
pdf: Pdf::new(), exported_pages: previous.exported_pages,
pages: vec![], pages,
exported_pages: page_ranges, resources,
glyph_sets: HashMap::new(), }
languages: BTreeMap::new(), }
alloc, }
/// At this point, the resources have been collected, and global references have
/// been allocated.
///
/// We are now writing objects corresponding to resources, and giving them references,
/// that will be collected in [`References`].
struct WithGlobalRefs<'a> {
document: &'a Document,
exported_pages: Option<PageRanges>,
pages: Vec<Option<EncodedPage>>,
/// Resources are the same as in previous phases, but each dictionary now has a reference.
resources: Resources,
/// Global references that were just allocated.
globals: GlobalRefs,
}
impl<'a> From<(WithResources<'a>, GlobalRefs)> for WithGlobalRefs<'a> {
fn from((previous, globals): (WithResources<'a>, GlobalRefs)) -> Self {
Self {
document: previous.document,
exported_pages: previous.exported_pages,
pages: previous.pages,
resources: previous.resources.with_refs(&globals.resources),
globals,
}
}
}
/// The references that have been assigned to each object.
struct References {
/// List of named destinations, each with an ID.
named_destinations: NamedDestinations,
/// The IDs of written fonts.
fonts: HashMap<Font, Ref>,
/// The IDs of written color fonts.
color_fonts: HashMap<ColorFontSlice, Ref>,
/// The IDs of written images.
images: HashMap<Image, Ref>,
/// The IDs of written gradients.
gradients: HashMap<PdfGradient, Ref>,
/// The IDs of written patterns.
patterns: HashMap<PdfPattern, Ref>,
/// The IDs of written external graphics states.
ext_gs: HashMap<ExtGState, Ref>,
}
/// At this point, the references have been assigned to all resources. The page
/// tree is going to be written, and given a reference. It is also at this point that
/// the page contents is actually written.
struct WithRefs<'a> {
globals: GlobalRefs,
document: &'a Document,
pages: Vec<Option<EncodedPage>>,
exported_pages: Option<PageRanges>,
resources: Resources,
/// References that were allocated for resources.
references: References,
}
impl<'a> From<(WithGlobalRefs<'a>, References)> for WithRefs<'a> {
fn from((previous, references): (WithGlobalRefs<'a>, References)) -> Self {
Self {
globals: previous.globals,
exported_pages: previous.exported_pages,
document: previous.document,
pages: previous.pages,
resources: previous.resources,
references,
}
}
}
/// In this phase, we write resource dictionaries.
///
/// Each sub-resource gets its own isolated resource dictionary.
struct WithEverything<'a> {
globals: GlobalRefs,
document: &'a Document,
pages: Vec<Option<EncodedPage>>,
exported_pages: Option<PageRanges>,
resources: Resources,
references: References,
/// Reference that was allocated for the page tree.
page_tree_ref: Ref,
}
impl<'a> From<(WithEverything<'a>, ())> for WithEverything<'a> {
fn from((this, _): (WithEverything<'a>, ())) -> Self {
this
}
}
impl<'a> From<(WithRefs<'a>, Ref)> for WithEverything<'a> {
fn from((previous, page_tree_ref): (WithRefs<'a>, Ref)) -> Self {
Self {
exported_pages: previous.exported_pages,
globals: previous.globals,
document: previous.document,
resources: previous.resources,
references: previous.references,
pages: previous.pages,
page_tree_ref, page_tree_ref,
global_resources_ref,
type3_font_resources_ref,
font_refs: vec![],
image_refs: vec![],
gradient_refs: vec![],
pattern_refs: vec![],
ext_gs_refs: vec![],
colors: ColorSpaces::default(),
font_map: Remapper::new(),
image_map: Remapper::new(),
image_deferred_map: HashMap::default(),
gradient_map: Remapper::new(),
pattern_map: Remapper::new(),
extg_map: Remapper::new(),
color_font_map: ColorFontMap::new(),
dests: vec![],
loc_to_dest: HashMap::new(),
} }
} }
} }
/// Write the document catalog. impl<'a> PdfBuilder<WithDocument<'a>> {
fn write_catalog(ctx: &mut PdfContext, ident: Smart<&str>, timestamp: Option<Datetime>) { /// Start building a PDF for a Typst document.
let lang = ctx.languages.iter().max_by_key(|(_, &count)| count).map(|(&l, _)| l); fn new(document: &'a Document, exported_pages: Option<PageRanges>) -> Self {
Self {
let dir = if lang.map(Lang::dir) == Some(Dir::RTL) { alloc: Ref::new(1),
Direction::R2L pdf: Pdf::new(),
} else { state: WithDocument { document, exported_pages },
Direction::L2R
};
// Write the outline tree.
let outline_root_id = outline::write_outline(ctx);
// Write the page labels.
let page_labels = page::write_page_labels(ctx);
// Write the document information.
let mut info = ctx.pdf.document_info(ctx.alloc.bump());
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()]);
}
let creator = eco_format!("Typst {}", env!("CARGO_PKG_VERSION"));
info.creator(TextStr(&creator));
xmp.creator_tool(&creator);
let keywords = &ctx.document.keywords;
if !keywords.is_empty() {
let joined = keywords.join(", ");
info.keywords(TextStr(&joined));
xmp.pdf_keywords(&joined);
}
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);
}
if let Some(xmp_date) = xmp_date(date, tz) {
xmp.create_date(xmp_date);
xmp.modify_date(xmp_date);
} }
} }
info.finish();
// Only count exported pages.
xmp.num_pages(ctx.pages.iter().filter(|page| page.is_some()).count() as u32);
xmp.format("application/pdf");
xmp.language(ctx.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(&ctx.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);
ctx.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 = ctx.alloc.bump();
ctx.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 mut catalog = ctx.pdf.catalog(ctx.alloc.bump());
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.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(outline_root_id) = outline_root_id {
catalog.outlines(outline_root_id);
}
if let Some(lang) = lang {
catalog.lang(TextStr(lang.as_str()));
}
catalog.finish();
} }
/// Fills in the map and vector for named destinations and writes the indirect impl<S> PdfBuilder<S> {
/// destination objects. /// Start a new phase, and save its output in the global state.
fn write_named_destinations(ctx: &mut PdfContext) { fn phase<NS, B, O>(mut self, builder: B) -> PdfBuilder<NS>
let mut seen = HashSet::new(); where
// New state
// Find all headings that have a label and are the first among other NS: From<(S, O)>,
// headings with the same label. // Builder
let mut matches: Vec<_> = ctx B: Fn(&mut Self) -> O,
.document {
.introspector let output = builder(&mut self);
.query(&HeadingElem::elem().select()) PdfBuilder {
.iter() state: NS::from((self.state, output)),
.filter_map(|elem| elem.location().zip(elem.label())) alloc: self.alloc,
.filter(|&(_, label)| seen.insert(label)) pdf: self.pdf,
.collect();
// Named destinations must be sorted by key.
matches.sort_by_key(|&(_, label)| label);
for (loc, label) in matches {
let pos = ctx.document.introspector.position(loc);
let index = pos.page.get() - 1;
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
// If the heading's page exists and is exported, include it.
if let Some(Some(page)) = ctx.pages.get(index) {
let dest_ref = ctx.alloc.bump();
let x = pos.point.x.to_f32();
let y = (page.size.y - y).to_f32();
ctx.dests.push((label, dest_ref));
ctx.loc_to_dest.insert(loc, label);
ctx.pdf
.indirect(dest_ref)
.start::<Destination>()
.page(page.id)
.xyz(x, y, None);
} }
} }
/// Runs a step with the current state, merge its output in the PDF file,
/// and renumber any references it returned.
fn run<P, O>(&mut self, process: P) -> O
where
// Process
P: Fn(&S) -> (PdfChunk, O),
// Output
O: Renumber,
{
let (chunk, mut output) = process(&self.state);
// Allocate a final reference for each temporary one
let allocated = chunk.alloc.get() - TEMPORARY_REFS_START;
let offset = TEMPORARY_REFS_START - self.alloc.get();
// Merge the chunk into the PDF, using the new references
chunk.renumber_into(&mut self.pdf, |mut r| {
r.renumber(offset);
r
});
// Also update the references in the output
output.renumber(offset);
self.alloc = Ref::new(self.alloc.get() + allocated);
output
}
/// Finalize the PDF export and returns the buffer representing the
/// document.
fn export_with<P>(
mut self,
ident: Smart<&str>,
timestamp: Option<Datetime>,
process: P,
) -> Vec<u8>
where
P: Fn(S, Smart<&str>, Option<Datetime>, &mut Pdf, &mut Ref),
{
process(self.state, ident, timestamp, &mut self.pdf, &mut self.alloc);
self.pdf.finish()
}
}
/// A reference or collection of references that can be re-numbered,
/// to become valid in a global scope.
trait Renumber {
/// Renumber this value by shifting any references it contains by `offset`.
fn renumber(&mut self, offset: i32);
}
impl Renumber for () {
fn renumber(&mut self, _offset: i32) {}
}
impl Renumber for Ref {
fn renumber(&mut self, offset: i32) {
if self.get() >= TEMPORARY_REFS_START {
*self = Ref::new(self.get() - offset);
}
}
}
impl<R: Renumber> Renumber for Vec<R> {
fn renumber(&mut self, offset: i32) {
for item in self {
item.renumber(offset);
}
}
}
impl<T: Eq + Hash, R: Renumber> Renumber for HashMap<T, R> {
fn renumber(&mut self, offset: i32) {
for v in self.values_mut() {
v.renumber(offset);
}
}
}
impl<R: Renumber> Renumber for Option<R> {
fn renumber(&mut self, offset: i32) {
if let Some(r) = self {
r.renumber(offset)
}
}
}
impl<T, R: Renumber> Renumber for (T, R) {
fn renumber(&mut self, offset: i32) {
self.1.renumber(offset)
}
}
/// A portion of a PDF file.
struct PdfChunk {
/// The actual chunk.
chunk: Chunk,
/// A local allocator.
alloc: Ref,
}
/// Any reference below that value was already allocated before and
/// should not be rewritten. Anything above was allocated in the current
/// chunk, and should be remapped.
///
/// This is a constant (large enough to avoid collisions) and not
/// dependant on self.alloc to allow for better memoization of steps, if
/// needed in the future.
const TEMPORARY_REFS_START: i32 = 1_000_000_000;
/// A part of a PDF document.
impl PdfChunk {
/// Start writing a new part of the document.
fn new() -> Self {
PdfChunk {
chunk: Chunk::new(),
alloc: Ref::new(TEMPORARY_REFS_START),
}
}
/// Allocate a reference that is valid in the context of this chunk.
///
/// References allocated with this function should be [renumbered](`Renumber::renumber`)
/// before being used in other chunks. This is done automatically if these
/// references are stored in the global `PdfBuilder` state.
fn alloc(&mut self) -> Ref {
self.alloc.bump()
}
}
impl Deref for PdfChunk {
type Target = Chunk;
fn deref(&self) -> &Self::Target {
&self.chunk
}
}
impl DerefMut for PdfChunk {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.chunk
}
} }
/// Compress data with the DEFLATE algorithm. /// Compress data with the DEFLATE algorithm.
@ -378,12 +461,6 @@ fn deflate(data: &[u8]) -> Vec<u8> {
miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL) miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
} }
/// Memoized version of [`deflate`] specialized for a page's content stream.
#[comemo::memoize]
fn deflate_memoized(content: &[u8]) -> Arc<Vec<u8>> {
Arc::new(deflate(content))
}
/// Memoized and deferred version of [`deflate`] specialized for a page's content /// Memoized and deferred version of [`deflate`] specialized for a page's content
/// stream. /// stream.
#[comemo::memoize] #[comemo::memoize]
@ -397,182 +474,6 @@ fn hash_base64<T: Hash>(value: &T) -> String {
.encode(typst::utils::hash128(value).to_be_bytes()) .encode(typst::utils::hash128(value).to_be_bytes())
} }
/// Converts a datetime to a pdf-writer date.
fn pdf_date(datetime: Datetime, tz: bool) -> Option<pdf_writer::Date> {
let year = datetime.year().filter(|&y| y >= 0)? as u16;
let mut pdf_date = pdf_writer::Date::new(year);
if let Some(month) = datetime.month() {
pdf_date = pdf_date.month(month);
}
if let Some(day) = datetime.day() {
pdf_date = pdf_date.day(day);
}
if let Some(h) = datetime.hour() {
pdf_date = pdf_date.hour(h);
}
if let Some(m) = datetime.minute() {
pdf_date = pdf_date.minute(m);
}
if let Some(s) = datetime.second() {
pdf_date = pdf_date.second(s);
}
if tz {
pdf_date = pdf_date.utc_offset_hour(0).utc_offset_minute(0);
}
Some(pdf_date)
}
/// Converts a datetime to an xmp-writer datetime.
fn xmp_date(datetime: Datetime, tz: bool) -> Option<xmp_writer::DateTime> {
let year = datetime.year().filter(|&y| y >= 0)? as u16;
Some(DateTime {
year,
month: datetime.month(),
day: datetime.day(),
hour: datetime.hour(),
minute: datetime.minute(),
second: datetime.second(),
timezone: if tz { Some(Timezone::Utc) } else { None },
})
}
/// Assigns new, consecutive PDF-internal indices to items.
struct Remapper<T> {
/// Forwards from the items to the pdf indices.
to_pdf: HashMap<T, usize>,
/// Backwards from the pdf indices to the items.
to_items: Vec<T>,
}
impl<T> Remapper<T>
where
T: Eq + Hash + Clone,
{
fn new() -> Self {
Self { to_pdf: HashMap::new(), to_items: vec![] }
}
fn insert(&mut self, item: T) -> usize {
let to_layout = &mut self.to_items;
*self.to_pdf.entry(item.clone()).or_insert_with(|| {
let pdf_index = to_layout.len();
to_layout.push(item);
pdf_index
})
}
fn pdf_indices<'a>(
&'a self,
refs: &'a [Ref],
) -> impl Iterator<Item = (Ref, usize)> + 'a {
refs.iter().copied().zip(0..self.to_pdf.len())
}
fn items(&self) -> impl Iterator<Item = &T> + '_ {
self.to_items.iter()
}
}
/// A mapping between `Font`s and all the corresponding `ColorFont`s.
///
/// This mapping is one-to-many because there can only be 256 glyphs in a Type 3
/// font, and fonts generally have more color glyphs than that.
struct ColorFontMap {
/// The mapping itself
map: IndexMap<Font, ColorFont>,
/// A list of all PDF indirect references to Type3 font objects.
all_refs: Vec<Ref>,
}
/// A collection of Type3 font, belonging to the same TTF font.
struct ColorFont {
/// A list of references to Type3 font objects for this font family.
refs: Vec<Ref>,
/// The list of all color glyphs in this family.
///
/// The index in this vector modulo 256 corresponds to the index in one of
/// the Type3 fonts in `refs` (the `n`-th in the vector, where `n` is the
/// quotient of the index divided by 256).
glyphs: Vec<ColorGlyph>,
/// The global bounding box of the font.
bbox: Rect,
/// A mapping between glyph IDs and character indices in the `glyphs`
/// vector.
glyph_indices: HashMap<u16, usize>,
}
/// A single color glyph.
struct ColorGlyph {
/// The ID of the glyph.
gid: u16,
/// A frame that contains the glyph.
frame: Frame,
}
impl ColorFontMap {
/// Creates a new empty mapping
fn new() -> Self {
Self { map: IndexMap::new(), all_refs: Vec::new() }
}
/// Takes the contents of the mapping.
///
/// After calling this function, the mapping will be empty.
fn take_map(&mut self) -> IndexMap<Font, ColorFont> {
std::mem::take(&mut self.map)
}
/// Obtains the reference to a Type3 font, and an index in this font
/// that can be used to draw a color glyph.
///
/// The glyphs will be de-duplicated if needed.
fn get(&mut self, alloc: &mut Ref, font: &Font, gid: u16) -> (Ref, u8) {
let color_font = self.map.entry(font.clone()).or_insert_with(|| {
let global_bbox = font.ttf().global_bounding_box();
let bbox = Rect::new(
font.to_em(global_bbox.x_min).to_font_units(),
font.to_em(global_bbox.y_min).to_font_units(),
font.to_em(global_bbox.x_max).to_font_units(),
font.to_em(global_bbox.y_max).to_font_units(),
);
ColorFont {
bbox,
refs: Vec::new(),
glyphs: Vec::new(),
glyph_indices: HashMap::new(),
}
});
if let Some(index_of_glyph) = color_font.glyph_indices.get(&gid) {
// If we already know this glyph, return it.
(color_font.refs[index_of_glyph / 256], *index_of_glyph as u8)
} else {
// Otherwise, allocate a new ColorGlyph in the font, and a new Type3 font
// if needed
let index = color_font.glyphs.len();
if index % 256 == 0 {
let new_ref = alloc.bump();
self.all_refs.push(new_ref);
color_font.refs.push(new_ref);
}
let instructions = frame_for_glyph(font, gid);
color_font.glyphs.push(ColorGlyph { gid, frame: instructions });
color_font.glyph_indices.insert(gid, index);
(color_font.refs[index / 256], index as u8)
}
}
}
/// Additional methods for [`Abs`]. /// Additional methods for [`Abs`].
trait AbsExt { trait AbsExt {
/// Convert an to a number of points. /// Convert an to a number of points.

View File

@ -0,0 +1,78 @@
use std::collections::{HashMap, HashSet};
use pdf_writer::{writers::Destination, Ref};
use typst::foundations::{Label, NativeElement};
use typst::introspection::Location;
use typst::layout::Abs;
use typst::model::HeadingElem;
use crate::{AbsExt, PdfChunk, Renumber, WithGlobalRefs};
/// A list of destinations in the PDF document (a specific point on a specific
/// page), that have a name associated with them.
///
/// Typst creates a named destination for each heading in the document, that
/// will then be written in the document catalog. PDF readers can then display
/// them to show a clickable outline of the document.
#[derive(Default)]
pub struct NamedDestinations {
/// A map between elements and their associated labels
pub loc_to_dest: HashMap<Location, Label>,
/// A sorted list of all named destinations.
pub dests: Vec<(Label, Ref)>,
}
impl Renumber for NamedDestinations {
fn renumber(&mut self, offset: i32) {
for (_, reference) in &mut self.dests {
reference.renumber(offset);
}
}
}
/// Fills in the map and vector for named destinations and writes the indirect
/// destination objects.
pub fn write_named_destinations(
context: &WithGlobalRefs,
) -> (PdfChunk, NamedDestinations) {
let mut chunk = PdfChunk::new();
let mut out = NamedDestinations::default();
let mut seen = HashSet::new();
// Find all headings that have a label and are the first among other
// headings with the same label.
let mut matches: Vec<_> = context
.document
.introspector
.query(&HeadingElem::elem().select())
.iter()
.filter_map(|elem| elem.location().zip(elem.label()))
.filter(|&(_, label)| seen.insert(label))
.collect();
// Named destinations must be sorted by key.
matches.sort_by_key(|&(_, label)| label);
for (loc, label) in matches {
let pos = context.document.introspector.position(loc);
let index = pos.page.get() - 1;
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
if let Some((Some(page), Some(page_ref))) =
context.pages.get(index).zip(context.globals.pages.get(index))
{
let dest_ref = chunk.alloc();
let x = pos.point.x.to_f32();
let y = (page.content.size.y - y).to_f32();
out.dests.push((label, dest_ref));
out.loc_to_dest.insert(loc, label);
chunk
.indirect(dest_ref)
.start::<Destination>()
.page(*page_ref)
.xyz(x, y, None);
}
}
(chunk, out)
}

View File

@ -1,14 +1,19 @@
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use pdf_writer::{Finish, Ref, TextStr}; use pdf_writer::{Finish, Pdf, Ref, TextStr};
use typst::foundations::{NativeElement, Packed, StyleChain}; use typst::foundations::{NativeElement, Packed, StyleChain};
use typst::layout::Abs; use typst::layout::Abs;
use typst::model::HeadingElem; use typst::model::HeadingElem;
use crate::{AbsExt, PdfContext}; use crate::{AbsExt, WithEverything};
/// Construct the outline for the document. /// Construct the outline for the document.
pub(crate) fn write_outline(ctx: &mut PdfContext) -> Option<Ref> { pub(crate) fn write_outline(
chunk: &mut Pdf,
alloc: &mut Ref,
ctx: &WithEverything,
) -> Option<Ref> {
let mut tree: Vec<HeadingNode> = vec![]; let mut tree: Vec<HeadingNode> = vec![];
// Stores the level of the topmost skipped ancestor of the next bookmarked // Stores the level of the topmost skipped ancestor of the next bookmarked
@ -95,20 +100,28 @@ pub(crate) fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
return None; return None;
} }
let root_id = ctx.alloc.bump(); let root_id = alloc.bump();
let start_ref = ctx.alloc; let start_ref = *alloc;
let len = tree.len(); let len = tree.len();
let mut prev_ref = None; let mut prev_ref = None;
for (i, node) in tree.iter().enumerate() { for (i, node) in tree.iter().enumerate() {
prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len)); prev_ref = Some(write_outline_item(
ctx,
chunk,
alloc,
node,
root_id,
prev_ref,
i + 1 == len,
));
} }
ctx.pdf chunk
.outline(root_id) .outline(root_id)
.first(start_ref) .first(start_ref)
.last(Ref::new( .last(Ref::new(
ctx.alloc.get() - tree.last().map(|child| child.len() as i32).unwrap_or(1), alloc.get() - tree.last().map(|child| child.len() as i32).unwrap_or(1),
)) ))
.count(tree.len() as i32); .count(tree.len() as i32);
@ -116,7 +129,7 @@ pub(crate) fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
} }
/// A heading in the outline panel. /// A heading in the outline panel.
#[derive(Debug, Clone)] #[derive(Debug)]
struct HeadingNode<'a> { struct HeadingNode<'a> {
element: &'a Packed<HeadingElem>, element: &'a Packed<HeadingElem>,
level: NonZeroUsize, level: NonZeroUsize,
@ -144,16 +157,18 @@ impl<'a> HeadingNode<'a> {
/// Write an outline item and all its children. /// Write an outline item and all its children.
fn write_outline_item( fn write_outline_item(
ctx: &mut PdfContext, ctx: &WithEverything,
chunk: &mut Pdf,
alloc: &mut Ref,
node: &HeadingNode, node: &HeadingNode,
parent_ref: Ref, parent_ref: Ref,
prev_ref: Option<Ref>, prev_ref: Option<Ref>,
is_last: bool, is_last: bool,
) -> Ref { ) -> Ref {
let id = ctx.alloc.bump(); let id = alloc.bump();
let next_ref = Ref::new(id.get() + node.len() as i32); let next_ref = Ref::new(id.get() + node.len() as i32);
let mut outline = ctx.pdf.outline_item(id); let mut outline = chunk.outline_item(id);
outline.parent(parent_ref); outline.parent(parent_ref);
if !is_last { if !is_last {
@ -178,11 +193,13 @@ fn write_outline_item(
let index = pos.page.get() - 1; let index = pos.page.get() - 1;
// Don't link to non-exported pages. // Don't link to non-exported pages.
if let Some(Some(page)) = ctx.pages.get(index) { if let Some((Some(page), Some(page_ref))) =
ctx.pages.get(index).zip(ctx.globals.pages.get(index))
{
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
outline.dest().page(page.id).xyz( outline.dest().page(*page_ref).xyz(
pos.point.x.to_f32(), pos.point.x.to_f32(),
(page.size.y - y).to_f32(), (page.content.size.y - y).to_f32(),
None, None,
); );
} }
@ -193,6 +210,8 @@ fn write_outline_item(
for (i, child) in node.children.iter().enumerate() { for (i, child) in node.children.iter().enumerate() {
prev_ref = Some(write_outline_item( prev_ref = Some(write_outline_item(
ctx, ctx,
chunk,
alloc,
child, child,
id, id,
prev_ref, prev_ref,

File diff suppressed because it is too large Load Diff

View File

@ -1,88 +1,72 @@
use std::collections::HashMap;
use ecow::eco_format; use ecow::eco_format;
use pdf_writer::types::{ColorSpaceOperand, PaintType, TilingType}; use pdf_writer::{
use pdf_writer::{Filter, Finish, Name, Rect}; types::{ColorSpaceOperand, PaintType, TilingType},
Filter, Name, Rect, Ref,
};
use typst::layout::{Abs, Ratio, Transform}; use typst::layout::{Abs, Ratio, Transform};
use typst::utils::Numeric; use typst::utils::Numeric;
use typst::visualize::{Pattern, RelativeTo}; use typst::visualize::{Pattern, RelativeTo};
use crate::color::PaintEncode; use crate::{color::PaintEncode, resources::Remapper, Resources, WithGlobalRefs};
use crate::page::{construct_page, PageContext, PageResource, ResourceKind, Transforms}; use crate::{content, resources::ResourcesRefs};
use crate::{transform_to_array, PdfContext}; use crate::{transform_to_array, PdfChunk};
/// Writes the actual patterns (tiling patterns) to the PDF. /// Writes the actual patterns (tiling patterns) to the PDF.
/// This is performed once after writing all pages. /// This is performed once after writing all pages.
pub(crate) fn write_patterns(ctx: &mut PdfContext) { pub fn write_patterns(context: &WithGlobalRefs) -> (PdfChunk, HashMap<PdfPattern, Ref>) {
for PdfPattern { transform, pattern, content, resources } in ctx.pattern_map.items() { let mut chunk = PdfChunk::new();
let tiling = ctx.alloc.bump(); let mut out = HashMap::new();
ctx.pattern_refs.push(tiling); context.resources.traverse(&mut |resources| {
let Some(patterns) = &resources.patterns else {
return;
};
let mut tiling_pattern = ctx.pdf.tiling_pattern(tiling, content); for pdf_pattern in patterns.remapper.items() {
tiling_pattern let PdfPattern { transform, pattern, content, .. } = pdf_pattern;
.tiling_type(TilingType::ConstantSpacing) if out.contains_key(pdf_pattern) {
.paint_type(PaintType::Colored) continue;
.bbox(Rect::new( }
0.0,
0.0,
pattern.size().x.to_pt() as _,
pattern.size().y.to_pt() as _,
))
.x_step((pattern.size().x + pattern.spacing().x).to_pt() as _)
.y_step((pattern.size().y + pattern.spacing().y).to_pt() as _);
let mut resources_map = tiling_pattern.resources(); let tiling = chunk.alloc();
out.insert(pdf_pattern.clone(), tiling);
resources_map.x_objects().pairs( let mut tiling_pattern = chunk.tiling_pattern(tiling, content);
resources tiling_pattern
.iter() .tiling_type(TilingType::ConstantSpacing)
.filter(|(res, _)| res.is_x_object()) .paint_type(PaintType::Colored)
.map(|(res, ref_)| (res.name(), ctx.image_refs[*ref_])), .bbox(Rect::new(
); 0.0,
0.0,
pattern.size().x.to_pt() as _,
pattern.size().y.to_pt() as _,
))
.x_step((pattern.size().x + pattern.spacing().x).to_pt() as _)
.y_step((pattern.size().y + pattern.spacing().y).to_pt() as _);
resources_map.fonts().pairs( // The actual resource dict will be written in a later step
resources tiling_pattern.pair(Name(b"Resources"), patterns.resources.reference);
.iter()
.filter(|(res, _)| res.is_font())
.map(|(res, ref_)| (res.name(), ctx.font_refs[*ref_])),
);
ctx.colors tiling_pattern
.write_color_spaces(resources_map.color_spaces(), &mut ctx.alloc); .matrix(transform_to_array(
transform
.pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
.post_concat(Transform::translate(
Abs::zero(),
pattern.spacing().y,
)),
))
.filter(Filter::FlateDecode);
}
});
resources_map (chunk, out)
.patterns()
.pairs(
resources
.iter()
.filter(|(res, _)| res.is_pattern())
.map(|(res, ref_)| (res.name(), ctx.pattern_refs[*ref_])),
)
.pairs(
resources
.iter()
.filter(|(res, _)| res.is_gradient())
.map(|(res, ref_)| (res.name(), ctx.gradient_refs[*ref_])),
);
resources_map.ext_g_states().pairs(
resources
.iter()
.filter(|(res, _)| res.is_ext_g_state())
.map(|(res, ref_)| (res.name(), ctx.ext_gs_refs[*ref_])),
);
resources_map.finish();
tiling_pattern
.matrix(transform_to_array(
transform
.pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
.post_concat(Transform::translate(Abs::zero(), pattern.spacing().y)),
))
.filter(Filter::FlateDecode);
}
} }
/// A pattern and its transform. /// A pattern and its transform.
#[derive(Clone, PartialEq, Eq, Hash)] #[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub struct PdfPattern { pub struct PdfPattern {
/// The transform to apply to the pattern. /// The transform to apply to the pattern.
pub transform: Transform, pub transform: Transform,
@ -90,17 +74,20 @@ pub struct PdfPattern {
pub pattern: Pattern, pub pattern: Pattern,
/// The rendered pattern. /// The rendered pattern.
pub content: Vec<u8>, pub content: Vec<u8>,
/// The resources used by the pattern.
pub resources: Vec<(PageResource, usize)>,
} }
/// Registers a pattern with the PDF. /// Registers a pattern with the PDF.
fn register_pattern( fn register_pattern(
ctx: &mut PageContext, ctx: &mut content::Builder,
pattern: &Pattern, pattern: &Pattern,
on_text: bool, on_text: bool,
mut transforms: Transforms, mut transforms: content::Transforms,
) -> usize { ) -> usize {
let patterns = ctx
.resources
.patterns
.get_or_insert_with(|| Box::new(PatternRemapper::new()));
// Edge cases for strokes. // Edge cases for strokes.
if transforms.size.x.is_zero() { if transforms.size.x.is_zero() {
transforms.size.x = Abs::pt(1.0); transforms.size.x = Abs::pt(1.0);
@ -116,22 +103,24 @@ fn register_pattern(
}; };
// Render the body. // Render the body.
let content = construct_page(ctx.parent, pattern.frame()); let content = content::build(&mut patterns.resources, pattern.frame(), None);
let mut pdf_pattern = PdfPattern { let pdf_pattern = PdfPattern {
transform, transform,
pattern: pattern.clone(), pattern: pattern.clone(),
content: content.content.wait().clone(), content: content.content.wait().clone(),
resources: content.resources.into_iter().collect(),
}; };
pdf_pattern.resources.sort(); patterns.remapper.insert(pdf_pattern)
ctx.parent.pattern_map.insert(pdf_pattern)
} }
impl PaintEncode for Pattern { impl PaintEncode for Pattern {
fn set_as_fill(&self, ctx: &mut PageContext, on_text: bool, transforms: Transforms) { fn set_as_fill(
&self,
ctx: &mut content::Builder,
on_text: bool,
transforms: content::Transforms,
) {
ctx.reset_fill_color_space(); ctx.reset_fill_color_space();
let index = register_pattern(ctx, self, on_text, transforms); let index = register_pattern(ctx, self, on_text, transforms);
@ -140,15 +129,13 @@ impl PaintEncode for Pattern {
ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern); ctx.content.set_fill_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_fill_pattern(None, name); ctx.content.set_fill_pattern(None, name);
ctx.resources
.insert(PageResource::new(ResourceKind::Pattern, id), index);
} }
fn set_as_stroke( fn set_as_stroke(
&self, &self,
ctx: &mut PageContext, ctx: &mut content::Builder,
on_text: bool, on_text: bool,
transforms: Transforms, transforms: content::Transforms,
) { ) {
ctx.reset_stroke_color_space(); ctx.reset_stroke_color_space();
@ -158,7 +145,30 @@ impl PaintEncode for Pattern {
ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern); ctx.content.set_stroke_color_space(ColorSpaceOperand::Pattern);
ctx.content.set_stroke_pattern(None, name); ctx.content.set_stroke_pattern(None, name);
ctx.resources }
.insert(PageResource::new(ResourceKind::Pattern, id), index); }
/// De-duplicate patterns and the resources they require to be drawn.
pub struct PatternRemapper<R> {
/// Pattern de-duplicator.
pub remapper: Remapper<PdfPattern>,
/// PDF resources that are used by these patterns.
pub resources: Resources<R>,
}
impl PatternRemapper<()> {
pub fn new() -> Self {
Self {
remapper: Remapper::new("P"),
resources: Resources::default(),
}
}
/// Allocate a reference to the resource dictionary of these patterns.
pub fn with_refs(self, refs: &ResourcesRefs) -> PatternRemapper<Ref> {
PatternRemapper {
remapper: self.remapper,
resources: self.resources.with_refs(refs),
}
} }
} }

View File

@ -0,0 +1,325 @@
//! PDF resources.
//!
//! Resources are defined in dictionaries. They map identifiers such as `Im0` to
//! a PDF reference. Each [content stream] is associated with a resource dictionary.
//! The identifiers defined in the resources can then be used in content streams.
//!
//! [content stream]: `crate::content`
use std::collections::{BTreeMap, HashMap};
use std::hash::Hash;
use ecow::{eco_format, EcoString};
use pdf_writer::{Dict, Finish, Name, Ref};
use typst::text::Lang;
use typst::{text::Font, utils::Deferred, visualize::Image};
use crate::{
color::ColorSpaces, color_font::ColorFontMap, extg::ExtGState, gradient::PdfGradient,
image::EncodedImage, pattern::PatternRemapper, PdfChunk, Renumber, WithEverything,
WithResources,
};
/// All the resources that have been collected when traversing the document.
///
/// This does not allocate references to resources, only track what was used
/// and deduplicate what can be deduplicated.
///
/// You may notice that this structure is a tree: [`PatternRemapper`] and
/// [`ColorFontMap`] (that are present in the fields of [`Resources`]),
/// themselves contain [`Resources`] (that will be called "sub-resources" from
/// now on). Because color glyphs and patterns are defined using content
/// streams, just like pages, they can refer to resources too, which are tracked
/// by the respective sub-resources.
///
/// Each instance of this structure will become a `/Resources` dictionary in
/// the final PDF. It is not possible to use a single shared dictionary for all
/// pages, patterns and color fonts, because if a resource is listed in its own
/// `/Resources` dictionary, some PDF readers will fail to open the document.
///
/// Because we need to lazily initialize sub-resources (we don't know how deep
/// the tree will be before reading the document), and that this is done in a
/// context where no PDF reference allocator is available, `Resources` are
/// originally created with the type parameter `R = ()`. The reference for each
/// dictionary will only be allocated in the next phase, once we know the shape
/// of the tree, at which point `R` becomes `Ref`. No other value of `R` should
/// ever exist.
pub struct Resources<R = Ref> {
/// The global reference to this resource dictionary, or `()` if it has not
/// been allocated yet.
pub reference: R,
/// Handles color space writing.
pub colors: ColorSpaces,
/// Deduplicates fonts used across the document.
pub fonts: Remapper<Font>,
/// Deduplicates images used across the document.
pub images: Remapper<Image>,
/// Handles to deferred image conversions.
pub deferred_images: HashMap<usize, Deferred<EncodedImage>>,
/// Deduplicates gradients used across the document.
pub gradients: Remapper<PdfGradient>,
/// Deduplicates patterns used across the document.
pub patterns: Option<Box<PatternRemapper<R>>>,
/// Deduplicates external graphics states used across the document.
pub ext_gs: Remapper<ExtGState>,
/// Deduplicates color glyphs.
pub color_fonts: Option<Box<ColorFontMap<R>>>,
// The fields below do not correspond to actual resources that will be
// written in a dictionary, but are more meta-data about resources that
// can't really live somewhere else.
/// The number of glyphs for all referenced languages in the content stream.
/// We keep track of this to determine the main document language.
/// BTreeMap is used to write sorted list of languages to metadata.
pub languages: BTreeMap<Lang, usize>,
/// For each font a mapping from used glyphs to their text representation.
/// May contain multiple chars in case of ligatures or similar things. The
/// same glyph can have a different text representation within one document,
/// then we just save the first one. The resulting strings are used for the
/// PDF's /ToUnicode map for glyphs that don't have an entry in the font's
/// cmap. This is important for copy-paste and searching.
pub glyph_sets: HashMap<Font, BTreeMap<u16, EcoString>>,
}
impl<R: Renumber> Renumber for Resources<R> {
fn renumber(&mut self, offset: i32) {
self.reference.renumber(offset);
if let Some(color_fonts) = &mut self.color_fonts {
color_fonts.resources.renumber(offset);
}
if let Some(patterns) = &mut self.patterns {
patterns.resources.renumber(offset);
}
}
}
impl Default for Resources<()> {
fn default() -> Self {
Resources {
reference: (),
colors: ColorSpaces::default(),
fonts: Remapper::new("F"),
images: Remapper::new("Im"),
deferred_images: HashMap::new(),
gradients: Remapper::new("Gr"),
patterns: None,
ext_gs: Remapper::new("Gs"),
color_fonts: None,
languages: BTreeMap::new(),
glyph_sets: HashMap::new(),
}
}
}
impl Resources<()> {
/// Associate a reference with this resource dictionary (and do so
/// recursively for sub-resources).
pub fn with_refs(self, refs: &ResourcesRefs) -> Resources<Ref> {
Resources {
reference: refs.reference,
colors: self.colors,
fonts: self.fonts,
images: self.images,
deferred_images: self.deferred_images,
gradients: self.gradients,
patterns: self
.patterns
.zip(refs.patterns.as_ref())
.map(|(p, r)| Box::new(p.with_refs(r))),
ext_gs: self.ext_gs,
color_fonts: self
.color_fonts
.zip(refs.color_fonts.as_ref())
.map(|(c, r)| Box::new(c.with_refs(r))),
languages: self.languages,
glyph_sets: self.glyph_sets,
}
}
}
impl<R> Resources<R> {
/// Run a function on this resource dictionary and all
/// of its sub-resources.
pub fn traverse<P>(&self, process: &mut P)
where
P: FnMut(&Self),
{
process(self);
if let Some(color_fonts) = &self.color_fonts {
color_fonts.resources.traverse(process)
}
if let Some(patterns) = &self.patterns {
patterns.resources.traverse(process)
}
}
}
/// References for a resource tree.
///
/// This structure is a tree too, that should have the same structure as the
/// corresponding `Resources`.
pub struct ResourcesRefs {
pub reference: Ref,
pub color_fonts: Option<Box<ResourcesRefs>>,
pub patterns: Option<Box<ResourcesRefs>>,
}
impl Renumber for ResourcesRefs {
fn renumber(&mut self, offset: i32) {
self.reference.renumber(offset);
if let Some(color_fonts) = &mut self.color_fonts {
color_fonts.renumber(offset);
}
if let Some(patterns) = &mut self.patterns {
patterns.renumber(offset);
}
}
}
/// Allocate references for all resource dictionaries.
pub fn alloc_resources_refs(context: &WithResources) -> (PdfChunk, ResourcesRefs) {
let mut chunk = PdfChunk::new();
/// Recursively explore resource dictionaries and assign them references.
fn refs_for(resources: &Resources<()>, chunk: &mut PdfChunk) -> ResourcesRefs {
ResourcesRefs {
reference: chunk.alloc(),
color_fonts: resources
.color_fonts
.as_ref()
.map(|c| Box::new(refs_for(&c.resources, chunk))),
patterns: resources
.patterns
.as_ref()
.map(|p| Box::new(refs_for(&p.resources, chunk))),
}
}
let refs = refs_for(&context.resources, &mut chunk);
(chunk, refs)
}
/// Write the resource dictionaries that will be referenced by all pages.
///
/// We add a reference to this dictionary to each page individually instead of
/// to the root node of the page tree because using the resource inheritance
/// feature breaks PDF merging with Apple Preview.
///
/// Also write resource dictionaries for Type3 fonts and patterns.
pub fn write_resource_dictionaries(ctx: &WithEverything) -> (PdfChunk, ()) {
let mut chunk = PdfChunk::new();
let mut used_color_spaces = ColorSpaces::default();
ctx.resources.traverse(&mut |resources| {
used_color_spaces.merge(&resources.colors);
let images_ref = chunk.alloc.bump();
let patterns_ref = chunk.alloc.bump();
let ext_gs_states_ref = chunk.alloc.bump();
let color_spaces_ref = chunk.alloc.bump();
let mut color_font_slices = Vec::new();
let mut color_font_numbers = HashMap::new();
if let Some(color_fonts) = &resources.color_fonts {
for (_, font_slice) in color_fonts.iter() {
color_font_numbers.insert(font_slice.clone(), color_font_slices.len());
color_font_slices.push(font_slice);
}
}
let color_font_remapper = Remapper {
prefix: "Cf",
to_pdf: color_font_numbers,
to_items: color_font_slices,
};
resources
.images
.write(&ctx.references.images, &mut chunk.indirect(images_ref).dict());
let mut patterns_dict = chunk.indirect(patterns_ref).dict();
resources
.gradients
.write(&ctx.references.gradients, &mut patterns_dict);
if let Some(p) = &resources.patterns {
p.remapper.write(&ctx.references.patterns, &mut patterns_dict);
}
patterns_dict.finish();
resources
.ext_gs
.write(&ctx.references.ext_gs, &mut chunk.indirect(ext_gs_states_ref).dict());
let mut res_dict = chunk
.indirect(resources.reference)
.start::<pdf_writer::writers::Resources>();
res_dict.pair(Name(b"XObject"), images_ref);
res_dict.pair(Name(b"Pattern"), patterns_ref);
res_dict.pair(Name(b"ExtGState"), ext_gs_states_ref);
res_dict.pair(Name(b"ColorSpace"), color_spaces_ref);
// TODO: can't this be an indirect reference too?
let mut fonts_dict = res_dict.fonts();
resources.fonts.write(&ctx.references.fonts, &mut fonts_dict);
color_font_remapper.write(&ctx.references.color_fonts, &mut fonts_dict);
fonts_dict.finish();
res_dict.finish();
let color_spaces = chunk.indirect(color_spaces_ref).dict();
resources
.colors
.write_color_spaces(color_spaces, &ctx.globals.color_functions);
});
used_color_spaces.write_functions(&mut chunk, &ctx.globals.color_functions);
(chunk, ())
}
/// Assigns new, consecutive PDF-internal indices to items.
pub struct Remapper<T> {
/// The prefix to use when naming these resources.
prefix: &'static str,
/// Forwards from the items to the pdf indices.
to_pdf: HashMap<T, usize>,
/// Backwards from the pdf indices to the items.
to_items: Vec<T>,
}
impl<T> Remapper<T>
where
T: Eq + Hash + Clone,
{
/// Create an empty mapping.
pub fn new(prefix: &'static str) -> Self {
Self { prefix, to_pdf: HashMap::new(), to_items: vec![] }
}
/// Insert an item in the mapping if it was not already present.
pub fn insert(&mut self, item: T) -> usize {
let to_layout = &mut self.to_items;
*self.to_pdf.entry(item.clone()).or_insert_with(|| {
let pdf_index = to_layout.len();
to_layout.push(item);
pdf_index
})
}
/// All items in this
pub fn items(&self) -> impl Iterator<Item = &T> + '_ {
self.to_items.iter()
}
/// Write this list of items in a Resource dictionary.
fn write(&self, mapping: &HashMap<T, Ref>, dict: &mut Dict) {
for (number, item) in self.items().enumerate() {
let name = eco_format!("{}{}", self.prefix, number);
let reference = mapping[item];
dict.pair(Name(name.as_bytes()), reference);
}
}
}