mirror of
https://github.com/typst/typst
synced 2025-06-28 16:22:53 +08:00
Refactor PDF export (#4154)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
This commit is contained in:
parent
6d07f702e1
commit
2946cde6fa
278
crates/typst-pdf/src/catalog.rs
Normal file
278
crates/typst-pdf/src/catalog.rs
Normal 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 },
|
||||||
|
})
|
||||||
|
}
|
@ -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
|
match color_space {
|
||||||
/// The A and B components of the color must be offset by +0.4 before being
|
ColorSpace::Oklch | ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => {
|
||||||
/// encoded into the PDF file.
|
self.use_oklab = true;
|
||||||
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;
|
self.use_linear_rgb = true;
|
||||||
}
|
}
|
||||||
|
ColorSpace::Srgb => {
|
||||||
/// Write the color space on usage.
|
self.use_srgb = true;
|
||||||
pub fn write(
|
}
|
||||||
&mut self,
|
ColorSpace::D65Gray => {
|
||||||
color_space: ColorSpace,
|
self.use_d65_gray = true;
|
||||||
writer: writers::ColorSpace,
|
|
||||||
alloc: &mut Ref,
|
|
||||||
) {
|
|
||||||
match color_space {
|
|
||||||
ColorSpace::Oklab | ColorSpace::Hsl | ColorSpace::Hsv => {
|
|
||||||
let mut oklab = writer.device_n([OKLAB_L, OKLAB_A, OKLAB_B]);
|
|
||||||
self.write(ColorSpace::LinearRgb, oklab.alternate_color_space(), alloc);
|
|
||||||
oklab.tint_ref(self.oklab(alloc));
|
|
||||||
oklab.attrs().subtype(DeviceNSubtype::DeviceN);
|
|
||||||
}
|
}
|
||||||
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);
|
||||||
|
312
crates/typst-pdf/src/color_font.rs
Normal file
312
crates/typst-pdf/src/color_font.rs
Normal 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,
|
||||||
|
}
|
712
crates/typst-pdf/src/content.rs
Normal file
712
crates/typst-pdf/src/content.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
context.resources.traverse(&mut |resources| {
|
||||||
|
for external_gs in resources.ext_gs.items() {
|
||||||
|
if out.contains_key(external_gs) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = chunk.alloc();
|
||||||
|
out.insert(*external_gs, id);
|
||||||
|
chunk
|
||||||
.ext_graphics(id)
|
.ext_graphics(id)
|
||||||
.non_stroking_alpha(external_gs.fill_opacity as f32 / 255.0)
|
.non_stroking_alpha(external_gs.fill_opacity as f32 / 255.0)
|
||||||
.stroking_alpha(external_gs.stroke_opacity as f32 / 255.0);
|
.stroking_alpha(external_gs.stroke_opacity as f32 / 255.0);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(chunk, out)
|
||||||
}
|
}
|
||||||
|
@ -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,18 +26,23 @@ 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();
|
||||||
|
context.resources.traverse(&mut |resources| {
|
||||||
|
for font in resources.fonts.items() {
|
||||||
|
if out.contains_key(font) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
for font in ctx.font_map.items() {
|
let type0_ref = chunk.alloc();
|
||||||
let type0_ref = ctx.alloc.bump();
|
let cid_ref = chunk.alloc();
|
||||||
let cid_ref = ctx.alloc.bump();
|
let descriptor_ref = chunk.alloc();
|
||||||
let descriptor_ref = ctx.alloc.bump();
|
let cmap_ref = chunk.alloc();
|
||||||
let cmap_ref = ctx.alloc.bump();
|
let data_ref = chunk.alloc();
|
||||||
let data_ref = ctx.alloc.bump();
|
out.insert(font.clone(), type0_ref);
|
||||||
ctx.font_refs.push(type0_ref);
|
|
||||||
|
|
||||||
let glyph_set = ctx.glyph_sets.get_mut(font).unwrap();
|
let glyph_set = resources.glyph_sets.get(font).unwrap();
|
||||||
let ttf = font.ttf();
|
let ttf = font.ttf();
|
||||||
|
|
||||||
// Do we have a TrueType or CFF font?
|
// Do we have a TrueType or CFF font?
|
||||||
@ -63,7 +68,7 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Write the base font object referencing the CID font.
|
// Write the base font object referencing the CID font.
|
||||||
ctx.pdf
|
chunk
|
||||||
.type0_font(type0_ref)
|
.type0_font(type0_ref)
|
||||||
.base_font(Name(base_font_type0.as_bytes()))
|
.base_font(Name(base_font_type0.as_bytes()))
|
||||||
.encoding_predefined(Name(b"Identity-H"))
|
.encoding_predefined(Name(b"Identity-H"))
|
||||||
@ -71,7 +76,7 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) {
|
|||||||
.to_unicode(cmap_ref);
|
.to_unicode(cmap_ref);
|
||||||
|
|
||||||
// Write the CID font referencing the font descriptor.
|
// Write the CID font referencing the font descriptor.
|
||||||
let mut cid = ctx.pdf.cid_font(cid_ref);
|
let mut cid = chunk.cid_font(cid_ref);
|
||||||
cid.subtype(if is_cff { CidFontType::Type0 } else { CidFontType::Type2 });
|
cid.subtype(if is_cff { CidFontType::Type0 } else { CidFontType::Type2 });
|
||||||
cid.base_font(Name(base_font.as_bytes()));
|
cid.base_font(Name(base_font.as_bytes()));
|
||||||
cid.system_info(SYSTEM_INFO);
|
cid.system_info(SYSTEM_INFO);
|
||||||
@ -111,13 +116,13 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) {
|
|||||||
// Write the /ToUnicode character map, which maps glyph ids back to
|
// Write the /ToUnicode character map, which maps glyph ids back to
|
||||||
// unicode codepoints to enable copying out of the PDF.
|
// unicode codepoints to enable copying out of the PDF.
|
||||||
let cmap = create_cmap(font, glyph_set);
|
let cmap = create_cmap(font, glyph_set);
|
||||||
ctx.pdf.cmap(cmap_ref, &cmap.finish());
|
chunk.cmap(cmap_ref, &cmap.finish());
|
||||||
|
|
||||||
// Subset and write the font's bytes.
|
// Subset and write the font's bytes.
|
||||||
let glyphs: Vec<_> = glyph_set.keys().copied().collect();
|
let glyphs: Vec<_> = glyph_set.keys().copied().collect();
|
||||||
let data = subset_font(font, &glyphs);
|
let data = subset_font(font, &glyphs);
|
||||||
|
|
||||||
let mut stream = ctx.pdf.stream(data_ref, &data);
|
let mut stream = chunk.stream(data_ref, &data);
|
||||||
stream.filter(Filter::FlateDecode);
|
stream.filter(Filter::FlateDecode);
|
||||||
if is_cff {
|
if is_cff {
|
||||||
stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C"));
|
stream.pair(Name(b"Subtype"), Name(b"CIDFontType0C"));
|
||||||
@ -126,130 +131,22 @@ pub(crate) fn write_fonts(ctx: &mut PdfContext) {
|
|||||||
stream.finish();
|
stream.finish();
|
||||||
|
|
||||||
let mut font_descriptor =
|
let mut font_descriptor =
|
||||||
write_font_descriptor(&mut ctx.pdf, descriptor_ref, font, &base_font);
|
write_font_descriptor(&mut chunk, descriptor_ref, font, &base_font);
|
||||||
if is_cff {
|
if is_cff {
|
||||||
font_descriptor.font_file3(data_ref);
|
font_descriptor.font_file3(data_ref);
|
||||||
} else {
|
} else {
|
||||||
font_descriptor.font_file2(data_ref);
|
font_descriptor.font_file2(data_ref);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
/// Writes color fonts as Type3 fonts
|
(chunk, out)
|
||||||
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 mut pdf_font = ctx.pdf.type3_font(*subfont_id);
|
|
||||||
pdf_font.pair(Name(b"Resources"), ctx.type3_font_resources_ref);
|
|
||||||
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 = 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
|
|
||||||
.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 ctx.pdf, descriptor_ref, &font, &base_font);
|
|
||||||
|
|
||||||
// Write the widths array
|
|
||||||
ctx.pdf.indirect(widths_ref).array().items(widths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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,15 +226,18 @@ 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
|
||||||
|
/// 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();
|
let ttf = font.ttf();
|
||||||
|
|
||||||
// For glyphs that have codepoints mapping to them in the font's cmap table,
|
|
||||||
// we prefer them over pre-existing text mappings from the document. Only
|
|
||||||
// things 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.
|
|
||||||
for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) {
|
for subtable in ttf.tables().cmap.into_iter().flat_map(|table| table.subtables) {
|
||||||
if !subtable.is_unicode() {
|
if !subtable.is_unicode() {
|
||||||
continue;
|
continue;
|
||||||
@ -355,7 +255,11 @@ fn create_cmap(font: &Font, glyph_set: &mut BTreeMap<u16, EcoString>) -> Unicode
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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() {
|
||||||
|
@ -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,12 +36,21 @@ 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() {
|
||||||
|
if out.contains_key(pdf_gradient) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let shading = chunk.alloc();
|
||||||
|
out.insert(pdf_gradient.clone(), shading);
|
||||||
|
|
||||||
|
let PdfGradient { transform, aspect_ratio, gradient, angle } = pdf_gradient;
|
||||||
|
|
||||||
let color_space = if gradient.space().hue_index().is_some() {
|
let color_space = if gradient.space().hue_index().is_some() {
|
||||||
ColorSpace::Oklab
|
ColorSpace::Oklab
|
||||||
@ -47,12 +60,17 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) {
|
|||||||
|
|
||||||
let mut shading_pattern = match &gradient {
|
let mut shading_pattern = match &gradient {
|
||||||
Gradient::Linear(_) => {
|
Gradient::Linear(_) => {
|
||||||
let shading_function = shading_function(ctx, &gradient, color_space);
|
let shading_function =
|
||||||
let mut shading_pattern = ctx.pdf.shading_pattern(shading);
|
shading_function(gradient, &mut chunk, color_space);
|
||||||
|
let mut shading_pattern = chunk.chunk.shading_pattern(shading);
|
||||||
let mut shading = shading_pattern.function_shading();
|
let mut shading = shading_pattern.function_shading();
|
||||||
shading.shading_type(FunctionShadingType::Axial);
|
shading.shading_type(FunctionShadingType::Axial);
|
||||||
|
|
||||||
ctx.colors.write(color_space, shading.color_space(), &mut ctx.alloc);
|
color::write(
|
||||||
|
color_space,
|
||||||
|
shading.color_space(),
|
||||||
|
&context.globals.color_functions,
|
||||||
|
);
|
||||||
|
|
||||||
let (mut sin, mut cos) = (angle.sin(), angle.cos());
|
let (mut sin, mut cos) = (angle.sin(), angle.cos());
|
||||||
|
|
||||||
@ -79,12 +97,17 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) {
|
|||||||
shading_pattern
|
shading_pattern
|
||||||
}
|
}
|
||||||
Gradient::Radial(radial) => {
|
Gradient::Radial(radial) => {
|
||||||
let shading_function = shading_function(ctx, &gradient, color_space);
|
let shading_function =
|
||||||
let mut shading_pattern = ctx.pdf.shading_pattern(shading);
|
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();
|
let mut shading = shading_pattern.function_shading();
|
||||||
shading.shading_type(FunctionShadingType::Radial);
|
shading.shading_type(FunctionShadingType::Radial);
|
||||||
|
|
||||||
ctx.colors.write(color_space, shading.color_space(), &mut ctx.alloc);
|
color::write(
|
||||||
|
color_space,
|
||||||
|
shading.color_space(),
|
||||||
|
&context.globals.color_functions,
|
||||||
|
);
|
||||||
|
|
||||||
shading
|
shading
|
||||||
.anti_alias(gradient.anti_alias())
|
.anti_alias(gradient.anti_alias())
|
||||||
@ -104,16 +127,16 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) {
|
|||||||
shading_pattern
|
shading_pattern
|
||||||
}
|
}
|
||||||
Gradient::Conic(_) => {
|
Gradient::Conic(_) => {
|
||||||
let vertices = compute_vertex_stream(&gradient, aspect_ratio);
|
let vertices = compute_vertex_stream(gradient, *aspect_ratio);
|
||||||
|
|
||||||
let stream_shading_id = ctx.alloc.bump();
|
let stream_shading_id = chunk.alloc();
|
||||||
let mut stream_shading =
|
let mut stream_shading =
|
||||||
ctx.pdf.stream_shading(stream_shading_id, &vertices);
|
chunk.chunk.stream_shading(stream_shading_id, &vertices);
|
||||||
|
|
||||||
ctx.colors.write(
|
color::write(
|
||||||
color_space,
|
color_space,
|
||||||
stream_shading.color_space(),
|
stream_shading.color_space(),
|
||||||
&mut ctx.alloc,
|
&context.globals.color_functions,
|
||||||
);
|
);
|
||||||
|
|
||||||
let range = color_space.range();
|
let range = color_space.range();
|
||||||
@ -131,23 +154,26 @@ pub(crate) fn write_gradients(ctx: &mut PdfContext) {
|
|||||||
|
|
||||||
stream_shading.finish();
|
stream_shading.finish();
|
||||||
|
|
||||||
let mut shading_pattern = ctx.pdf.shading_pattern(shading);
|
let mut shading_pattern = chunk.shading_pattern(shading);
|
||||||
shading_pattern.shading_ref(stream_shading_id);
|
shading_pattern.shading_ref(stream_shading_id);
|
||||||
shading_pattern
|
shading_pattern
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
shading_pattern.matrix(transform_to_array(transform));
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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.
|
|
||||||
#[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)]);
|
|
||||||
}
|
(deferred, color_space)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
|
@ -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.
|
||||||
|
78
crates/typst-pdf/src/named_destination.rs
Normal file
78
crates/typst-pdf/src/named_destination.rs
Normal 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)
|
||||||
|
}
|
@ -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
@ -1,22 +1,39 @@
|
|||||||
|
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() {
|
||||||
|
let PdfPattern { transform, pattern, content, .. } = pdf_pattern;
|
||||||
|
if out.contains_key(pdf_pattern) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let tiling = chunk.alloc();
|
||||||
|
out.insert(pdf_pattern.clone(), tiling);
|
||||||
|
|
||||||
|
let mut tiling_pattern = chunk.tiling_pattern(tiling, content);
|
||||||
tiling_pattern
|
tiling_pattern
|
||||||
.tiling_type(TilingType::ConstantSpacing)
|
.tiling_type(TilingType::ConstantSpacing)
|
||||||
.paint_type(PaintType::Colored)
|
.paint_type(PaintType::Colored)
|
||||||
@ -29,60 +46,27 @@ pub(crate) fn write_patterns(ctx: &mut PdfContext) {
|
|||||||
.x_step((pattern.size().x + pattern.spacing().x).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 _);
|
.y_step((pattern.size().y + pattern.spacing().y).to_pt() as _);
|
||||||
|
|
||||||
let mut resources_map = tiling_pattern.resources();
|
// The actual resource dict will be written in a later step
|
||||||
|
tiling_pattern.pair(Name(b"Resources"), patterns.resources.reference);
|
||||||
|
|
||||||
resources_map.x_objects().pairs(
|
|
||||||
resources
|
|
||||||
.iter()
|
|
||||||
.filter(|(res, _)| res.is_x_object())
|
|
||||||
.map(|(res, ref_)| (res.name(), ctx.image_refs[*ref_])),
|
|
||||||
);
|
|
||||||
|
|
||||||
resources_map.fonts().pairs(
|
|
||||||
resources
|
|
||||||
.iter()
|
|
||||||
.filter(|(res, _)| res.is_font())
|
|
||||||
.map(|(res, ref_)| (res.name(), ctx.font_refs[*ref_])),
|
|
||||||
);
|
|
||||||
|
|
||||||
ctx.colors
|
|
||||||
.write_color_spaces(resources_map.color_spaces(), &mut ctx.alloc);
|
|
||||||
|
|
||||||
resources_map
|
|
||||||
.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
|
tiling_pattern
|
||||||
.matrix(transform_to_array(
|
.matrix(transform_to_array(
|
||||||
transform
|
transform
|
||||||
.pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
|
.pre_concat(Transform::scale(Ratio::one(), -Ratio::one()))
|
||||||
.post_concat(Transform::translate(Abs::zero(), pattern.spacing().y)),
|
.post_concat(Transform::translate(
|
||||||
|
Abs::zero(),
|
||||||
|
pattern.spacing().y,
|
||||||
|
)),
|
||||||
))
|
))
|
||||||
.filter(Filter::FlateDecode);
|
.filter(Filter::FlateDecode);
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
(chunk, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
325
crates/typst-pdf/src/resources.rs
Normal file
325
crates/typst-pdf/src/resources.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user