Skip to content

Commit 90c074f

Browse files
committed
Add content stream compression
Closes #14
1 parent 639126b commit 90c074f

3 files changed

Lines changed: 70 additions & 12 deletions

File tree

src/defer.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,8 @@ pub(crate) fn write_masks(tree: &Tree, writer: &mut PdfWriter, ctx: &mut Context
238238

239239
let content = content_stream(&mask_node, writer, ctx);
240240

241-
let mut group = form_xobject(writer, gp.reference, &content, gp.bbox, true);
241+
let mut group =
242+
form_xobject(writer, gp.reference, &content, gp.bbox, ctx.compress, true);
242243

243244
if let Some(matrix) = gp.matrix {
244245
group.matrix(matrix);

src/lib.rs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ use std::collections::HashMap;
3939

4040
use pdf_writer::types::ProcSet;
4141
use pdf_writer::writers::{ColorSpace, ExponentialFunction, FormXObject, Resources};
42-
use pdf_writer::{Content, Finish, Name, PdfWriter, Rect, Ref, TextStr, Writer};
42+
use pdf_writer::{Content, Filter, Finish, Name, PdfWriter, Rect, Ref, TextStr, Writer};
4343
use usvg::{NodeExt, NodeKind, Opacity, Stop, Tree};
4444

4545
mod defer;
@@ -91,11 +91,23 @@ pub struct Options {
9191
///
9292
/// _Default:_ `72.0`.
9393
pub dpi: f64,
94+
/// Whether the content streams should be compressed.
95+
///
96+
/// The smaller PDFs generated by this are generally more practical but it
97+
/// increases runtime a bit.
98+
///
99+
/// _Default:_ `true`.
100+
pub compress: bool,
94101
}
95102

96103
impl Default for Options {
97104
fn default() -> Self {
98-
Options { viewport: None, aspect: None, dpi: 72.0 }
105+
Options {
106+
viewport: None,
107+
aspect: None,
108+
dpi: 72.0,
109+
compress: true,
110+
}
99111
}
100112
}
101113

@@ -137,11 +149,13 @@ struct Context<'a> {
137149
/// The mask that needs to be applied at the start of a path drawing
138150
/// operation.
139151
initial_mask: Option<String>,
152+
/// Whether the content streas should be compressed.
153+
compress: bool,
140154
}
141155

142156
impl<'a> Context<'a> {
143157
/// Create a new context.
144-
fn new(tree: &'a Tree, bbox: &'a Rect, c: CoordToPdf) -> Self {
158+
fn new(tree: &'a Tree, compress: bool, bbox: &'a Rect, c: CoordToPdf) -> Self {
145159
Self {
146160
tree,
147161
bbox,
@@ -159,6 +173,7 @@ impl<'a> Context<'a> {
159173
pending_groups: HashMap::new(),
160174
checkpoints: vec![],
161175
initial_mask: None,
176+
compress,
162177
}
163178
}
164179

@@ -253,7 +268,7 @@ pub fn convert_str(src: &str, options: Options) -> Result<Vec<u8>, usvg::Error>
253268
/// Convert a [`usvg` tree](Tree) to a standalone PDF buffer.
254269
pub fn convert_tree(tree: &Tree, options: Options) -> Vec<u8> {
255270
let (c, bbox) = get_sizings(tree, &options);
256-
let mut ctx = Context::new(&tree, &bbox, c);
271+
let mut ctx = Context::new(&tree, options.compress, &bbox, c);
257272

258273
let mut writer = PdfWriter::new();
259274
let catalog_id = ctx.alloc_ref();
@@ -282,7 +297,13 @@ pub fn convert_tree(tree: &Tree, options: Options) -> Vec<u8> {
282297
resources.finish();
283298
page.finish();
284299

285-
writer.stream(content_id, &content);
300+
let mut stream = writer.stream(content_id, &content);
301+
if ctx.compress {
302+
stream.filter(Filter::FlateDecode);
303+
}
304+
305+
stream.finish();
306+
286307
writer.document_info(ctx.alloc_ref()).producer(TextStr("svg2pdf"));
287308

288309
writer.finish()
@@ -381,7 +402,7 @@ pub fn convert_tree_into(
381402
id: Ref,
382403
) -> Ref {
383404
let (c, bbox) = get_sizings(tree, &options);
384-
let mut ctx = Context::new(&tree, &bbox, c);
405+
let mut ctx = Context::new(&tree, options.compress, &bbox, c);
385406

386407
ctx.next_id = id.get() + 1;
387408

@@ -403,6 +424,10 @@ pub fn convert_tree_into(
403424
0.0,
404425
]);
405426

427+
if ctx.compress {
428+
xobject.filter(Filter::FlateDecode);
429+
}
430+
406431
let mut resources = xobject.resources();
407432
ctx.pop(&mut resources);
408433

@@ -483,7 +508,9 @@ fn content_stream<'a>(
483508
}
484509
}
485510

486-
content.finish()
511+
let res = content.finish();
512+
513+
if ctx.compress { deflate(&res) } else { res }
487514
}
488515

489516
/// Draw a clipping path into a content stream.
@@ -695,11 +722,16 @@ fn form_xobject<'a>(
695722
reference: Ref,
696723
content: &'a [u8],
697724
bbox: Rect,
725+
compress: bool,
698726
has_color: bool,
699727
) -> FormXObject<'a> {
700728
let mut form = writer.form_xobject(reference, content);
701729
form.bbox(bbox);
702730

731+
if compress {
732+
form.filter(Filter::FlateDecode);
733+
}
734+
703735
let mut group = form.group();
704736
group.transparency();
705737
group.isolated(true);
@@ -716,6 +748,12 @@ fn form_xobject<'a>(
716748
form
717749
}
718750

751+
/// Compress data with the DEFLATE algorithm.
752+
fn deflate(data: &[u8]) -> Vec<u8> {
753+
const COMPRESSION_LEVEL: u8 = 6;
754+
miniz_oxide::deflate::compress_to_vec_zlib(data, COMPRESSION_LEVEL)
755+
}
756+
719757
#[cfg(test)]
720758
mod tests {
721759
use super::*;

src/render.rs

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ use super::{
2323
apply_clip_path, apply_mask, content_stream, form_xobject, Context, Options,
2424
RgbColor, SRGB,
2525
};
26-
use crate::convert_tree_into;
2726
use crate::defer::{PendingGS, PendingGradient};
2827
use crate::scale::CoordToPdf;
28+
use crate::{convert_tree_into, deflate};
2929

3030
/// Write the appropriate instructions for a node into the content stream.
3131
///
@@ -291,8 +291,14 @@ fn render_path_partial(
291291
// Write the Form XObject if there was a gradient with alpha values.
292292
if let Some((xobj_content, path_no)) = xobj_content {
293293
let path_ref = ctx.alloc_ref();
294-
let data = xobj_content.finish();
295-
let mut form = form_xobject(writer, path_ref, &data, pdf_bbox, true);
294+
let data = if ctx.compress {
295+
deflate(&xobj_content.finish())
296+
} else {
297+
xobj_content.finish()
298+
};
299+
300+
let mut form =
301+
form_xobject(writer, path_ref, &data, pdf_bbox, ctx.compress, true);
296302
let mut resources = form.resources();
297303
ctx.pop(&mut resources);
298304
ctx.pending_xobjects.push((path_no, path_ref));
@@ -379,6 +385,7 @@ fn prep_shading(
379385
&shading_content,
380386
ctx.c.pdf_rect(bbox),
381387
false,
388+
false,
382389
);
383390

384391
let mut resources = smask_form.resources();
@@ -479,6 +486,10 @@ fn prep_pattern(
479486
.x_step(pdf_rect.x2 - pdf_rect.x1)
480487
.y_step(pdf_rect.y2 - pdf_rect.y1);
481488

489+
if ctx.compress {
490+
pdf_pattern.filter(Filter::FlateDecode);
491+
}
492+
482493
let mut resources = pdf_pattern.resources();
483494
ctx.pop(&mut resources);
484495
resources.finish();
@@ -509,7 +520,14 @@ impl Render for usvg::Group {
509520

510521
// Every group is an isolated transparency group, it needs to be painted
511522
// onto its own canvas.
512-
let mut form = form_xobject(writer, group_ref, &child_content, pdf_bbox, true);
523+
let mut form = form_xobject(
524+
writer,
525+
group_ref,
526+
&child_content,
527+
pdf_bbox,
528+
ctx.compress,
529+
true,
530+
);
513531
let mut resources = form.resources();
514532
ctx.pop(&mut resources);
515533

@@ -691,6 +709,7 @@ impl Render for usvg::Image {
691709
viewport: Some((rect.width(), rect.height())),
692710
aspect: Some(self.view_box.aspect),
693711
dpi: ctx.c.dpi(),
712+
compress: ctx.compress,
694713
};
695714

696715
ctx.next_id = convert_tree_into(tree, opt, writer, image_ref).get();

0 commit comments

Comments
 (0)