Refactor PDF export a bit

This commit is contained in:
Laurenz 2023-10-04 14:41:21 +02:00
parent 7dc74b7281
commit 077218db3a
8 changed files with 133 additions and 161 deletions

View File

@ -1,5 +1,4 @@
use std::sync::Arc; use once_cell::sync::Lazy;
use pdf_writer::types::DeviceNSubtype; use pdf_writer::types::DeviceNSubtype;
use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref}; use pdf_writer::{writers, Chunk, Dict, Filter, Name, Ref};
@ -27,13 +26,18 @@ const HSL_S: Name<'static> = Name(b"S");
const HSL_L: Name<'static> = Name(b"L"); const HSL_L: Name<'static> = Name(b"L");
// The ICC profiles. // The ICC profiles.
const SRGB_ICC: &[u8] = include_bytes!("./icc/sRGB-v4.icc"); static SRGB_ICC_DEFLATED: Lazy<Vec<u8>> =
const GRAY_ICC: &[u8] = include_bytes!("./icc/sGrey-v4.icc"); Lazy::new(|| deflate(include_bytes!("icc/sRGB-v4.icc")));
static GRAY_ICC_DEFLATED: Lazy<Vec<u8>> =
Lazy::new(|| deflate(include_bytes!("icc/sGrey-v4.icc")));
// The PostScript functions for color spaces. // The PostScript functions for color spaces.
const OKLAB_SOURCE: &str = include_str!("./postscript/oklab.ps"); static OKLAB_DEFLATED: Lazy<Vec<u8>> =
const HSL_SOURCE: &str = include_str!("./postscript/hsl.ps"); Lazy::new(|| deflate(minify(include_str!("postscript/oklab.ps")).as_bytes()));
const HSV_SOURCE: &str = include_str!("./postscript/hsv.ps"); static HSV_DEFLATED: Lazy<Vec<u8>> =
Lazy::new(|| deflate(minify(include_str!("postscript/hsl.ps")).as_bytes()));
static HSL_DEFLATED: Lazy<Vec<u8>> =
Lazy::new(|| deflate(minify(include_str!("postscript/hsv.ps")).as_bytes()));
/// The color spaces present in the PDF document /// The color spaces present in the PDF document
#[derive(Default)] #[derive(Default)]
@ -161,87 +165,54 @@ impl ColorSpaces {
/// 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, writer: &mut Chunk) { pub fn write_functions(&self, chunk: &mut Chunk) {
// Write the Oklab function & color space // Write the Oklab function & color space.
if let Some(oklab) = self.oklab { if let Some(oklab) = self.oklab {
let code = oklab_function(); chunk
writer .post_script_function(oklab, &OKLAB_DEFLATED)
.post_script_function(oklab, &code)
.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 HSV function & color space // Write the HSV function & color space.
if let Some(hsv) = self.hsv { if let Some(hsv) = self.hsv {
let code = hsv_function(); chunk
writer .post_script_function(hsv, &HSV_DEFLATED)
.post_script_function(hsv, &code)
.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 HSL function & color space // Write the HSL function & color space.
if let Some(hsl) = self.hsl { if let Some(hsl) = self.hsl {
let code = hsl_function(); chunk
writer .post_script_function(hsl, &HSL_DEFLATED)
.post_script_function(hsl, &code)
.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 let Some(srgb) = self.srgb {
let profile = srgb_icc(); chunk
writer .icc_profile(srgb, &SRGB_ICC_DEFLATED)
.icc_profile(srgb, &profile)
.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);
} }
// Write the gray color space // Write the gray color space.
if let Some(gray) = self.d65_gray { if let Some(gray) = self.d65_gray {
let profile = gray_icc(); chunk
writer.icc_profile(gray, &profile).n(1).range([0.0, 1.0]); .icc_profile(gray, &GRAY_ICC_DEFLATED)
.n(1)
.range([0.0, 1.0])
.filter(Filter::FlateDecode);
} }
} }
} }
/// Deflated sRGB ICC profile
#[comemo::memoize]
fn srgb_icc() -> Arc<Vec<u8>> {
Arc::new(deflate(SRGB_ICC))
}
/// Deflated gray ICC profile
#[comemo::memoize]
fn gray_icc() -> Arc<Vec<u8>> {
Arc::new(deflate(GRAY_ICC))
}
/// Deflated Oklab PostScript function
#[comemo::memoize]
fn oklab_function() -> Arc<Vec<u8>> {
let code = minify(OKLAB_SOURCE);
Arc::new(deflate(code.as_bytes()))
}
/// Deflated HSV PostScript function
#[comemo::memoize]
fn hsv_function() -> Arc<Vec<u8>> {
let code = minify(HSV_SOURCE);
Arc::new(deflate(code.as_bytes()))
}
/// Deflated HSL PostScript function
#[comemo::memoize]
fn hsl_function() -> Arc<Vec<u8>> {
let code = minify(HSL_SOURCE);
Arc::new(deflate(code.as_bytes()))
}
/// This function removes comments, line spaces and carriage returns from a /// This function removes comments, line spaces and carriage returns from a
/// PostScript program. This is necessary to optimize the size of the PDF file. /// PostScript program. This is necessary to optimize the size of the PDF file.
fn minify(source: &str) -> String { fn minify(source: &str) -> String {

View File

@ -1,23 +1,21 @@
use pdf_writer::Finish; use super::PdfContext;
use crate::export::pdf::PdfContext;
/// A PDF external graphics state. /// A PDF external graphics state.
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
pub struct ExternalGraphicsState { pub struct ExtGState {
// In the range 0-255, needs to be divided before being written into the graphics state! // In the range 0-255, needs to be divided before being written into the graphics state!
pub stroke_opacity: u8, pub stroke_opacity: u8,
// In the range 0-255, needs to be divided before being written into the graphics state! // In the range 0-255, needs to be divided before being written into the graphics state!
pub fill_opacity: u8, pub fill_opacity: u8,
} }
impl Default for ExternalGraphicsState { impl Default for ExtGState {
fn default() -> Self { fn default() -> Self {
Self { stroke_opacity: 255, fill_opacity: 255 } Self { stroke_opacity: 255, fill_opacity: 255 }
} }
} }
impl ExternalGraphicsState { impl ExtGState {
pub fn uses_opacities(&self) -> bool { pub fn uses_opacities(&self) -> bool {
self.stroke_opacity != 255 || self.fill_opacity != 255 self.stroke_opacity != 255 || self.fill_opacity != 255
} }
@ -26,13 +24,12 @@ impl ExternalGraphicsState {
/// Embed all used external graphics states into the PDF. /// Embed all used external graphics states into the PDF.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub fn write_external_graphics_states(ctx: &mut PdfContext) { pub fn write_external_graphics_states(ctx: &mut PdfContext) {
for external_gs in ctx.ext_gs_map.items() { for external_gs in ctx.extg_map.items() {
let gs_ref = ctx.alloc.bump(); let id = ctx.alloc.bump();
ctx.ext_gs_refs.push(gs_ref); ctx.ext_gs_refs.push(id);
ctx.pdf
let mut gs = ctx.writer.ext_graphics(gs_ref); .ext_graphics(id)
gs.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);
gs.finish();
} }
} }

View File

@ -58,7 +58,7 @@ pub 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.writer ctx.pdf
.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"))
@ -66,7 +66,7 @@ pub 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.writer.cid_font(cid_ref); let mut cid = ctx.pdf.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);
@ -125,7 +125,7 @@ pub fn write_fonts(ctx: &mut PdfContext) {
let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0); let stem_v = 10.0 + 0.244 * (f32::from(ttf.weight().to_number()) - 50.0);
// Write the font descriptor (contains metrics about the font). // Write the font descriptor (contains metrics about the font).
let mut font_descriptor = ctx.writer.font_descriptor(descriptor_ref); let mut font_descriptor = ctx.pdf.font_descriptor(descriptor_ref);
font_descriptor font_descriptor
.name(Name(base_font.as_bytes())) .name(Name(base_font.as_bytes()))
.flags(flags) .flags(flags)
@ -147,13 +147,13 @@ pub 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(ttf, glyph_set); let cmap = create_cmap(ttf, glyph_set);
ctx.writer.cmap(cmap_ref, &cmap.finish()); ctx.pdf.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.writer.stream(data_ref, &data); let mut stream = ctx.pdf.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"));

View File

@ -35,7 +35,7 @@ pub fn write_gradients(ctx: &mut PdfContext) {
let mut shading_pattern = match &gradient { let mut shading_pattern = match &gradient {
Gradient::Linear(linear) => { Gradient::Linear(linear) => {
let shading_function = shading_function(ctx, &gradient); let shading_function = shading_function(ctx, &gradient);
let mut shading_pattern = ctx.writer.shading_pattern(shading); let mut shading_pattern = ctx.pdf.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);
@ -108,7 +108,7 @@ fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref {
// These need to be individual function to encode 360.0 correctly. // These need to be individual function to encode 360.0 correctly.
let func1 = ctx.alloc.bump(); let func1 = ctx.alloc.bump();
ctx.writer ctx.pdf
.exponential_function(func1) .exponential_function(func1)
.range(gradient.space().range()) .range(gradient.space().range())
.c0(gradient.space().convert(first.0)) .c0(gradient.space().convert(first.0))
@ -117,7 +117,7 @@ fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref {
.n(1.0); .n(1.0);
let func2 = ctx.alloc.bump(); let func2 = ctx.alloc.bump();
ctx.writer ctx.pdf
.exponential_function(func2) .exponential_function(func2)
.range(gradient.space().range()) .range(gradient.space().range())
.c0([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) .c0([1.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
@ -126,7 +126,7 @@ fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref {
.n(1.0); .n(1.0);
let func3 = ctx.alloc.bump(); let func3 = ctx.alloc.bump();
ctx.writer ctx.pdf
.exponential_function(func3) .exponential_function(func3)
.range(gradient.space().range()) .range(gradient.space().range())
.c0([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t]) .c0([0.0, s1 * (1.0 - t) + s2 * t, x1 * (1.0 - t) + x2 * t])
@ -157,7 +157,7 @@ fn shading_function(ctx: &mut PdfContext, gradient: &Gradient) -> Ref {
bounds.pop(); bounds.pop();
// Create the stitching function. // Create the stitching function.
ctx.writer ctx.pdf
.stitching_function(function) .stitching_function(function)
.domain([0.0, 1.0]) .domain([0.0, 1.0])
.range(gradient.space().range()) .range(gradient.space().range())
@ -178,7 +178,7 @@ fn single_gradient(
) -> Ref { ) -> Ref {
let reference = ctx.alloc.bump(); let reference = ctx.alloc.bump();
ctx.writer 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))

View File

@ -15,26 +15,28 @@ use crate::{
pub fn write_images(ctx: &mut PdfContext) { pub fn write_images(ctx: &mut PdfContext) {
for image in ctx.image_map.items() { for image in ctx.image_map.items() {
let image_ref = ctx.alloc.bump(); let image_ref = ctx.alloc.bump();
let icc_ref = ctx.alloc.bump();
ctx.image_refs.push(image_ref); ctx.image_refs.push(image_ref);
let width = image.width();
let height = image.height();
// Add the primary image. // Add the primary image.
match image.kind() { match image.kind() {
ImageKind::Raster(raster) => { ImageKind::Raster(raster) => {
// TODO: Error if image could not be encoded. // TODO: Error if image could not be encoded.
let (data, filter, has_color) = encode_image(raster); let (data, filter, has_color) = encode_raster_image(raster);
let mut image = ctx.writer.image_xobject(image_ref, &data); let width = image.width();
let height = image.height();
let mut image = ctx.pdf.image_xobject(image_ref, &data);
image.filter(filter); image.filter(filter);
image.width(width as i32); image.width(width as i32);
image.height(height as i32); image.height(height as i32);
image.bits_per_component(8); image.bits_per_component(8);
let mut icc_ref = None;
let space = image.color_space(); let space = image.color_space();
if raster.icc().is_some() { if raster.icc().is_some() {
space.icc_based(icc_ref); let id = ctx.alloc.bump();
space.icc_based(id);
icc_ref = Some(id);
} else if has_color { } else if has_color {
ctx.colors.write(ColorSpace::Srgb, space, &mut ctx.alloc); ctx.colors.write(ColorSpace::Srgb, space, &mut ctx.alloc);
} else { } else {
@ -49,7 +51,7 @@ pub fn write_images(ctx: &mut PdfContext) {
image.s_mask(mask_ref); image.s_mask(mask_ref);
image.finish(); image.finish();
let mut mask = ctx.writer.image_xobject(mask_ref, &alpha_data); let mut mask = ctx.pdf.image_xobject(mask_ref, &alpha_data);
mask.filter(alpha_filter); mask.filter(alpha_filter);
mask.width(width as i32); mask.width(width as i32);
mask.height(height as i32); mask.height(height as i32);
@ -59,9 +61,9 @@ pub fn write_images(ctx: &mut PdfContext) {
image.finish(); image.finish();
} }
if let Some(icc) = raster.icc() { if let (Some(icc), Some(icc_ref)) = (raster.icc(), icc_ref) {
let compressed = deflate(icc); let compressed = deflate(icc);
let mut stream = ctx.writer.icc_profile(icc_ref, &compressed); let mut stream = ctx.pdf.icc_profile(icc_ref, &compressed);
stream.filter(Filter::FlateDecode); stream.filter(Filter::FlateDecode);
if has_color { if has_color {
stream.n(3); stream.n(3);
@ -79,7 +81,7 @@ pub fn write_images(ctx: &mut PdfContext) {
let next_ref = svg2pdf::convert_tree_into( let next_ref = svg2pdf::convert_tree_into(
tree, tree,
svg2pdf::Options::default(), svg2pdf::Options::default(),
&mut ctx.writer, &mut ctx.pdf,
image_ref, image_ref,
); );
ctx.alloc = next_ref; ctx.alloc = next_ref;
@ -95,7 +97,7 @@ pub fn write_images(ctx: &mut PdfContext) {
/// Skips the alpha channel as that's encoded separately. /// Skips the alpha channel as that's encoded separately.
#[comemo::memoize] #[comemo::memoize]
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
fn encode_image(image: &RasterImage) -> (Arc<Vec<u8>>, Filter, bool) { fn encode_raster_image(image: &RasterImage) -> (Arc<Vec<u8>>, Filter, bool) {
let dynamic = image.dynamic(); let dynamic = image.dynamic();
match (image.format(), dynamic) { match (image.format(), dynamic) {
// 8-bit gray JPEG. // 8-bit gray JPEG.

View File

@ -30,7 +30,7 @@ use crate::geom::{Abs, Dir, Em};
use crate::image::Image; use crate::image::Image;
use crate::model::Introspector; use crate::model::Introspector;
use extg::ExternalGraphicsState; use extg::ExtGState;
/// Export a document into a PDF file. /// Export a document into a PDF file.
/// ///
@ -45,28 +45,21 @@ pub fn pdf(document: &Document) -> Vec<u8> {
extg::write_external_graphics_states(&mut ctx); extg::write_external_graphics_states(&mut ctx);
page::write_page_tree(&mut ctx); page::write_page_tree(&mut ctx);
write_catalog(&mut ctx); write_catalog(&mut ctx);
ctx.writer.finish() ctx.pdf.finish()
} }
/// Context for exporting a whole PDF document. /// Context for exporting a whole PDF document.
pub struct PdfContext<'a> { pub struct PdfContext<'a> {
/// The document that we're currently exporting.
document: &'a Document, document: &'a Document,
/// An introspector for the document, used to resolve locations links and
/// the document outline.
introspector: Introspector, introspector: Introspector,
writer: Pdf,
colors: ColorSpaces, /// The writer we are writing the PDF into.
pdf: Pdf,
/// Content of exported pages.
pages: Vec<Page>, pages: Vec<Page>,
page_heights: Vec<f32>,
alloc: Ref,
page_tree_ref: Ref,
font_refs: Vec<Ref>,
image_refs: Vec<Ref>,
gradient_refs: Vec<Ref>,
ext_gs_refs: Vec<Ref>,
page_refs: Vec<Ref>,
font_map: Remapper<Font>,
image_map: Remapper<Image>,
gradient_map: Remapper<PdfGradient>,
ext_gs_map: Remapper<ExternalGraphicsState>,
/// For each font a mapping from used glyphs to their text representation. /// For each font a mapping from used glyphs to their text representation.
/// May contain multiple chars in case of ligatures or similar things. The /// May contain multiple chars in case of ligatures or similar things. The
/// same glyph can have a different text representation within one document, /// same glyph can have a different text representation within one document,
@ -74,7 +67,35 @@ pub struct PdfContext<'a> {
/// PDF's /ToUnicode map for glyphs that don't have an entry in the font's /// 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. /// cmap. This is important for copy-paste and searching.
glyph_sets: HashMap<Font, BTreeMap<u16, EcoString>>, 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.
languages: HashMap<Lang, usize>, languages: HashMap<Lang, usize>,
/// Allocator for indirect reference IDs.
alloc: Ref,
/// The ID of the page tree.
page_tree_ref: Ref,
/// The IDs of written pages.
page_refs: Vec<Ref>,
/// The IDs of written fonts.
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 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>,
/// Deduplicates gradients used across the document.
gradient_map: Remapper<PdfGradient>,
/// Deduplicates external graphics states used across the document.
extg_map: Remapper<ExtGState>,
} }
impl<'a> PdfContext<'a> { impl<'a> PdfContext<'a> {
@ -84,10 +105,10 @@ impl<'a> PdfContext<'a> {
Self { Self {
document, document,
introspector: Introspector::new(&document.pages), introspector: Introspector::new(&document.pages),
writer: Pdf::new(), pdf: Pdf::new(),
colors: ColorSpaces::default(),
pages: vec![], pages: vec![],
page_heights: vec![], glyph_sets: HashMap::new(),
languages: HashMap::new(),
alloc, alloc,
page_tree_ref, page_tree_ref,
page_refs: vec![], page_refs: vec![],
@ -95,12 +116,11 @@ impl<'a> PdfContext<'a> {
image_refs: vec![], image_refs: vec![],
gradient_refs: vec![], gradient_refs: vec![],
ext_gs_refs: vec![], ext_gs_refs: vec![],
colors: ColorSpaces::default(),
font_map: Remapper::new(), font_map: Remapper::new(),
image_map: Remapper::new(), image_map: Remapper::new(),
gradient_map: Remapper::new(), gradient_map: Remapper::new(),
ext_gs_map: Remapper::new(), extg_map: Remapper::new(),
glyph_sets: HashMap::new(),
languages: HashMap::new(),
} }
} }
} }
@ -127,7 +147,7 @@ fn write_catalog(ctx: &mut PdfContext) {
let page_labels = write_page_labels(ctx); let page_labels = write_page_labels(ctx);
// Write the document information. // Write the document information.
let mut info = ctx.writer.document_info(ctx.alloc.bump()); let mut info = ctx.pdf.document_info(ctx.alloc.bump());
let mut xmp = XmpWriter::new(); let mut xmp = XmpWriter::new();
if let Some(title) = &ctx.document.title { if let Some(title) = &ctx.document.title {
info.title(TextStr(title)); info.title(TextStr(title));
@ -160,13 +180,13 @@ fn write_catalog(ctx: &mut PdfContext) {
let xmp_buf = xmp.finish(None); let xmp_buf = xmp.finish(None);
let meta_ref = ctx.alloc.bump(); let meta_ref = ctx.alloc.bump();
let mut meta_stream = ctx.writer.stream(meta_ref, xmp_buf.as_bytes()); ctx.pdf
meta_stream.pair(Name(b"Type"), Name(b"Metadata")); .stream(meta_ref, xmp_buf.as_bytes())
meta_stream.pair(Name(b"Subtype"), Name(b"XML")); .pair(Name(b"Type"), Name(b"Metadata"))
meta_stream.finish(); .pair(Name(b"Subtype"), Name(b"XML"));
// Write the document catalog. // Write the document catalog.
let mut catalog = ctx.writer.catalog(ctx.alloc.bump()); let mut catalog = ctx.pdf.catalog(ctx.alloc.bump());
catalog.pages(ctx.page_tree_ref); catalog.pages(ctx.page_tree_ref);
catalog.viewer_preferences().direction(dir); catalog.viewer_preferences().direction(dir);
catalog.pair(Name(b"Metadata"), meta_ref); catalog.pair(Name(b"Metadata"), meta_ref);
@ -215,7 +235,7 @@ fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)> {
} }
let id = ctx.alloc.bump(); let id = ctx.alloc.bump();
let mut entry = ctx.writer.indirect(id).start::<PageLabel>(); let mut entry = ctx.pdf.indirect(id).start::<PageLabel>();
// Only add what is actually provided. Don't add empty prefix string if // Only add what is actually provided. Don't add empty prefix string if
// it wasn't given for example. // it wasn't given for example.
@ -309,17 +329,3 @@ impl EmExt for Em {
1000.0 * self.get() as f32 1000.0 * self.get() as f32
} }
} }
/// Additional methods for [`Ref`].
trait RefExt {
/// Bump the reference up by one and return the previous one.
fn bump(&mut self) -> Self;
}
impl RefExt for Ref {
fn bump(&mut self) -> Self {
let prev = *self;
*self = Self::new(prev.get() + 1);
prev
}
}

View File

@ -92,7 +92,7 @@ pub fn write_outline(ctx: &mut PdfContext) -> Option<Ref> {
prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len)); prev_ref = Some(write_outline_item(ctx, node, root_id, prev_ref, i + 1 == len));
} }
ctx.writer ctx.pdf
.outline(root_id) .outline(root_id)
.first(start_ref) .first(start_ref)
.last(Ref::new(ctx.alloc.get() - 1)) .last(Ref::new(ctx.alloc.get() - 1))
@ -140,7 +140,7 @@ fn write_outline_item(
let id = ctx.alloc.bump(); let id = ctx.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.writer.outline_item(id); let mut outline = ctx.pdf.outline_item(id);
outline.parent(parent_ref); outline.parent(parent_ref);
if !is_last { if !is_last {
@ -164,11 +164,11 @@ fn write_outline_item(
let loc = node.element.location().unwrap(); let loc = node.element.location().unwrap();
let pos = ctx.introspector.position(loc); let pos = ctx.introspector.position(loc);
let index = pos.page.get() - 1; let index = pos.page.get() - 1;
if let Some(&height) = ctx.page_heights.get(index) { if let Some(page) = ctx.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(ctx.page_refs[index]).xyz( outline.dest().page(ctx.page_refs[index]).xyz(
pos.point.x.to_f32(), pos.point.x.to_f32(),
height - y.to_f32(), (page.size.y - y).to_f32(),
None, None,
); );
} }

View File

@ -8,7 +8,7 @@ use pdf_writer::types::{
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str};
use super::color::PaintEncode; use super::color::PaintEncode;
use super::extg::ExternalGraphicsState; use super::extg::ExtGState;
use super::{deflate, AbsExt, EmExt, PdfContext}; use super::{deflate, AbsExt, EmExt, PdfContext};
use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem}; use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem};
use crate::eval::Repr; use crate::eval::Repr;
@ -32,7 +32,6 @@ pub fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {
pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) { pub fn construct_page(ctx: &mut PdfContext, frame: &Frame) {
let page_ref = ctx.alloc.bump(); let page_ref = ctx.alloc.bump();
ctx.page_refs.push(page_ref); ctx.page_refs.push(page_ref);
ctx.page_heights.push(frame.height().to_f32());
let mut ctx = PageContext { let mut ctx = PageContext {
parent: ctx, parent: ctx,
@ -81,7 +80,7 @@ pub fn write_page_tree(ctx: &mut PdfContext) {
write_page(ctx, i); write_page(ctx, i);
} }
let mut pages = ctx.writer.pages(ctx.page_tree_ref); let mut pages = ctx.pdf.pages(ctx.page_tree_ref);
pages pages
.count(ctx.page_refs.len() as i32) .count(ctx.page_refs.len() as i32)
.kids(ctx.page_refs.iter().copied()); .kids(ctx.page_refs.iter().copied());
@ -115,7 +114,7 @@ pub fn write_page_tree(ctx: &mut PdfContext) {
patterns.finish(); patterns.finish();
let mut ext_gs_states = resources.ext_g_states(); let mut ext_gs_states = resources.ext_g_states();
for (gs_ref, gs) in ctx.ext_gs_map.pdf_indices(&ctx.ext_gs_refs) { for (gs_ref, gs) in ctx.extg_map.pdf_indices(&ctx.ext_gs_refs) {
let name = eco_format!("Gs{}", gs); let name = eco_format!("Gs{}", gs);
ext_gs_states.pair(Name(name.as_bytes()), gs_ref); ext_gs_states.pair(Name(name.as_bytes()), gs_ref);
} }
@ -125,7 +124,7 @@ pub fn write_page_tree(ctx: &mut PdfContext) {
pages.finish(); pages.finish();
// Write all of the functions used by the document. // Write all of the functions used by the document.
ctx.colors.write_functions(&mut ctx.writer); ctx.colors.write_functions(&mut ctx.pdf);
} }
/// Write a page tree node. /// Write a page tree node.
@ -134,7 +133,7 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
let page = &ctx.pages[i]; let page = &ctx.pages[i];
let content_id = ctx.alloc.bump(); let content_id = ctx.alloc.bump();
let mut page_writer = ctx.writer.page(page.id); let mut page_writer = ctx.pdf.page(page.id);
page_writer.parent(ctx.page_tree_ref); page_writer.parent(ctx.page_tree_ref);
let w = page.size.x.to_f32(); let w = page.size.x.to_f32();
@ -172,13 +171,13 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
let index = pos.page.get() - 1; let index = pos.page.get() - 1;
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero()); let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
if let Some(&height) = ctx.page_heights.get(index) { if let Some(page) = ctx.pages.get(index) {
annotation annotation
.action() .action()
.action_type(ActionType::GoTo) .action_type(ActionType::GoTo)
.destination() .destination()
.page(ctx.page_refs[index]) .page(ctx.page_refs[index])
.xyz(pos.point.x.to_f32(), height - y.to_f32(), None); .xyz(pos.point.x.to_f32(), (page.size.y - y).to_f32(), None);
} }
} }
@ -186,7 +185,7 @@ fn write_page(ctx: &mut PdfContext, i: usize) {
page_writer.finish(); page_writer.finish();
let data = deflate(&page.content); let data = deflate(&page.content);
ctx.writer.stream(content_id, &data).filter(Filter::FlateDecode); ctx.pdf.stream(content_id, &data).filter(Filter::FlateDecode);
} }
/// Data for an exported page. /// Data for an exported page.
@ -231,7 +230,7 @@ struct State {
font: Option<(Font, Abs)>, font: Option<(Font, Abs)>,
fill: Option<Paint>, fill: Option<Paint>,
fill_space: Option<Name<'static>>, fill_space: Option<Name<'static>>,
external_graphics_state: Option<ExternalGraphicsState>, external_graphics_state: Option<ExtGState>,
stroke: Option<FixedStroke>, stroke: Option<FixedStroke>,
stroke_space: Option<Name<'static>>, stroke_space: Option<Name<'static>>,
} }
@ -287,11 +286,11 @@ impl PageContext<'_, '_> {
self.state = self.saves.pop().expect("missing state save"); self.state = self.saves.pop().expect("missing state save");
} }
fn set_external_graphics_state(&mut self, graphics_state: &ExternalGraphicsState) { fn set_external_graphics_state(&mut self, graphics_state: &ExtGState) {
let current_state = self.state.external_graphics_state.as_ref(); let current_state = self.state.external_graphics_state.as_ref();
if current_state != Some(graphics_state) { if current_state != Some(graphics_state) {
self.parent.ext_gs_map.insert(*graphics_state); self.parent.extg_map.insert(*graphics_state);
let name = eco_format!("Gs{}", self.parent.ext_gs_map.map(graphics_state)); let name = eco_format!("Gs{}", self.parent.extg_map.map(graphics_state));
self.content.set_parameters(Name(name.as_bytes())); self.content.set_parameters(Name(name.as_bytes()));
if graphics_state.uses_opacities() { if graphics_state.uses_opacities() {
@ -321,10 +320,7 @@ impl PageContext<'_, '_> {
color.alpha().map_or(255, |v| (v * 255.0).round() as u8) color.alpha().map_or(255, |v| (v * 255.0).round() as u8)
}) })
.unwrap_or(255); .unwrap_or(255);
self.set_external_graphics_state(&ExternalGraphicsState { self.set_external_graphics_state(&ExtGState { stroke_opacity, fill_opacity });
stroke_opacity,
fill_opacity,
});
} }
fn transform(&mut self, transform: Transform) { fn transform(&mut self, transform: Transform) {