Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
c750a19
First attempt
LaurenzV Oct 12, 2025
aad07d4
Make even tighter
LaurenzV Oct 12, 2025
c715181
Rename `settings` to `write_settings`
LaurenzV Oct 13, 2025
efc6da8
Add more methods
LaurenzV Oct 13, 2025
6c110b4
Typo
LaurenzV Oct 13, 2025
c0c0e3d
Remove TODO
LaurenzV Oct 13, 2025
159986b
Add some tests
LaurenzV Oct 13, 2025
d510174
Don't write space before operator if not necessary
LaurenzV Oct 13, 2025
d751b0b
Extend a test case
LaurenzV Oct 13, 2025
4b6c85a
More improvements
LaurenzV Oct 13, 2025
6d5ae2c
Re-arrange
LaurenzV Oct 13, 2025
2f0250a
More tweaks
LaurenzV Oct 13, 2025
45612a4
Adapt comment
LaurenzV Oct 13, 2025
556140f
Adapt wording
LaurenzV Oct 13, 2025
0587ed2
Rename
LaurenzV Oct 13, 2025
67c2333
More
LaurenzV Oct 13, 2025
d819ac2
Add test cases
LaurenzV Oct 13, 2025
59c7ad0
Extend test cases a bit
LaurenzV Oct 13, 2025
1788e30
Small fixes
LaurenzV Oct 13, 2025
c67837b
Rearrange again
LaurenzV Oct 13, 2025
ef4e868
Fix tests
LaurenzV Oct 13, 2025
ffda4cb
Update src/chunk.rs
LaurenzV Oct 28, 2025
4536d3b
Rename `new_with` to `with_settings`
LaurenzV Oct 29, 2025
f48faf9
Rename `WriteSettings` to just `Settings`
LaurenzV Oct 29, 2025
71367e7
Wrap at 80
LaurenzV Oct 29, 2025
8c01c52
Update src/content.rs
LaurenzV Oct 29, 2025
d646abd
Merge remote-tracking branch 'mine/compact' into compact
LaurenzV Oct 29, 2025
fcd5098
Fix some comments
LaurenzV Oct 29, 2025
dce89f2
Remove newlines
LaurenzV Oct 29, 2025
b97add3
Fix build
LaurenzV Oct 29, 2025
b2ffbbb
Reformat
LaurenzV Oct 29, 2025
fccf5c0
Document default value
LaurenzV Oct 30, 2025
6b7a034
Reformat
LaurenzV Oct 30, 2025
1fd6096
Turn `Primitive` into a sealed trait
LaurenzV Nov 3, 2025
c701b91
Rewrap some comments
laurmaedje Nov 8, 2025
8e9ffe9
Reexport `Settings`
laurmaedje Nov 8, 2025
58ffa52
Tidy up constructors
laurmaedje Nov 8, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions src/annotations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -796,10 +796,13 @@ mod tests {
#[test]
fn test_annotations() {
test!(
crate::tests::slice(|w| {
w.annotation(Ref::new(1)).rect(Rect::new(0.0, 0.0, 1.0, 1.0));
w.annotation(Ref::new(2)).rect(Rect::new(1.0, 1.0, 0.0, 0.0));
}),
crate::tests::slice(
|w| {
w.annotation(Ref::new(1)).rect(Rect::new(0.0, 0.0, 1.0, 1.0));
w.annotation(Ref::new(2)).rect(Rect::new(1.0, 1.0, 0.0, 0.0));
},
WriteSettings::default()
),
b"1 0 obj",
b"<<",
b" /Type /Annot",
Expand Down
38 changes: 36 additions & 2 deletions src/chunk.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
use super::*;

/// Settings that should be applied while writing a PDF file.
#[derive(Debug, Clone, Copy)]
pub struct WriteSettings {
/// Whether to enable pretty-writing. In this case, `pdf-writer` will serialize PDFs in such
Comment thread
LaurenzV marked this conversation as resolved.
Outdated
/// a way that they are easier to read by humans by applying more padding and indentation, at
/// the cost of larger file sizes. If disabled, `pdf-writer` will serialize objects as compactly
/// as possible, leading to better file sizes but making it harder to inspect the file manually.
pub pretty: bool,
}

impl Default for WriteSettings {
fn default() -> Self {
Self { pretty: true }
}
}

/// A builder for a collection of indirect PDF objects.
///
/// This type holds written top-level indirect PDF objects. Typically, you won't
Expand All @@ -14,6 +30,7 @@ use super::*;
pub struct Chunk {
pub(crate) buf: Buf,
pub(crate) offsets: Vec<(Ref, usize)>,
pub(crate) write_settings: WriteSettings,
}

impl Chunk {
Expand All @@ -25,7 +42,19 @@ impl Chunk {

/// Create a new chunk with the specified initial capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self { buf: Buf::with_capacity(capacity), offsets: vec![] }
Self {
buf: Buf::with_capacity(capacity),
offsets: vec![],
write_settings: Default::default(),
}
}

/// Create a new chunk with the given write settings.
pub fn new_with(write_settings: WriteSettings) -> Self {
Comment thread
LaurenzV marked this conversation as resolved.
Outdated
let mut chunk = Self::new();
chunk.write_settings = write_settings;

Comment thread
LaurenzV marked this conversation as resolved.
Outdated
chunk
}

/// The number of bytes that were written so far.
Expand All @@ -35,6 +64,11 @@ impl Chunk {
self.buf.len()
}

/// Reserve an additional number of bytes in the buffer.
pub fn reserve(&mut self, additional: usize) {
self.buf.reserve(additional);
}

/// The bytes already written so far.
pub fn as_bytes(&self) -> &[u8] {
self.buf.as_slice()
Expand Down Expand Up @@ -148,7 +182,7 @@ impl Chunk {
/// Start writing an indirectly referenceable object.
pub fn indirect(&mut self, id: Ref) -> Obj<'_> {
self.offsets.push((id, self.buf.len()));
Obj::indirect(&mut self.buf, id)
Obj::indirect(&mut self.buf, id, self.write_settings)
}

/// Start writing an indirectly referenceable stream.
Expand Down
106 changes: 93 additions & 13 deletions src/content.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use super::*;
use crate::object::TextStrLike;
use crate::chunk::WriteSettings;
use crate::object::{is_delimiter_character, TextStrLike};

/// A builder for a content stream.
pub struct Content {
buf: Buf,
write_settings: WriteSettings,
q_depth: usize,
}

Expand All @@ -16,15 +18,27 @@ impl Content {
Self::with_capacity(1024)
}

/// Create a new content stream with the given write settings.
pub fn new_with(write_settings: WriteSettings) -> Self {
let mut content = Self::new();
content.write_settings = write_settings;

content
}

/// Create a new content stream with the specified initial buffer capacity.
pub fn with_capacity(capacity: usize) -> Self {
Self { buf: Buf::with_capacity(capacity), q_depth: 0 }
Self {
buf: Buf::with_capacity(capacity),
q_depth: 0,
write_settings: Default::default(),
}
}

/// Start writing an arbitrary operation.
#[inline]
pub fn op<'a>(&'a mut self, operator: &'a str) -> Operation<'a> {
Operation::start(&mut self.buf, operator)
Operation::start(&mut self.buf, operator, self.write_settings)
}

/// Return the buffer of the content stream.
Expand All @@ -51,12 +65,17 @@ pub struct Operation<'a> {
buf: &'a mut Buf,
op: &'a str,
first: bool,
write_settings: WriteSettings,
}

impl<'a> Operation<'a> {
#[inline]
pub(crate) fn start(buf: &'a mut Buf, op: &'a str) -> Self {
Self { buf, op, first: true }
pub(crate) fn start(
buf: &'a mut Buf,
op: &'a str,
write_settings: WriteSettings,
) -> Self {
Self { buf, op, first: true, write_settings }
}

/// Write a primitive operand.
Expand All @@ -79,25 +98,46 @@ impl<'a> Operation<'a> {
self
}

/// Start writing an an arbitrary object operand.
/// Start writing an arbitrary object operand.
#[inline]
pub fn obj(&mut self) -> Obj<'_> {
if !self.first {
self.buf.push(b' ');
}
// In case we are writing the first object, we want a newline to separate it from
// previous operations (looks nicer). Otherwise, a space is sufficient.
let pad_byte = if self.first { b'\n' } else { b' ' };

// Similarly to how chunks are handled, we always add padding when pretty-writing
// is enabled, and only lazily add padding depending on whether it's really necessary
// if not.
let needs_padding = if self.write_settings.pretty {
if !self.buf.is_empty() {
self.buf.push(pad_byte);
}

false
} else {
true
};

self.first = false;
Obj::direct(self.buf, 0)
Obj::direct(self.buf, 0, self.write_settings, needs_padding)
}
}

impl Drop for Operation<'_> {
#[inline]
fn drop(&mut self) {
if !self.first {
self.buf.push(b' ');
let pad_byte = if self.first { b'\n' } else { b' ' };

// For example, in case we previously wrote a BT operator and then a [] operand in the
Comment thread
LaurenzV marked this conversation as resolved.
Outdated
// next operation, we don't need to pad them.
if (self.write_settings.pretty
|| self.buf.last().is_some_and(|b| !is_delimiter_character(*b)))
&& !self.buf.is_empty()
{
self.buf.push(pad_byte);
}

self.buf.extend(self.op.as_bytes());
self.buf.push(b'\n');
}
}

Expand Down Expand Up @@ -1708,4 +1748,44 @@ mod tests {
b"/F1 12 Tf\nBT\n[] TJ\n[(AB) 2 (CD)] TJ\nET"
);
}

#[test]
fn test_content_array_no_pretty() {
let mut content = Content::new_with(WriteSettings { pretty: false });

content.set_font(Name(b"F1"), 12.0);
content.set_font(Name(b"F2"), 15.0);
content.begin_text();
content.show_positioned().items();
content
.show_positioned()
.items()
.show(Str(b"AB"))
.adjust(2.0)
.show(Str(b"CD"))
.adjust(4.0)
.show(Str(b"EF"));
content.end_text();

assert_eq!(
content.finish().into_vec(),
b"/F1 12 Tf/F2 15 Tf\nBT[]TJ[(AB)2(CD)4(EF)]TJ\nET"
);
}

#[test]
fn test_content_dict_no_pretty() {
let mut content = Content::new_with(WriteSettings { pretty: false });

let mut mc = content.begin_marked_content_with_properties(Name(b"Test"));
let mut properties = mc.properties();
properties.actual_text(TextStr("Actual")).identify(1);
properties.artifact().kind(ArtifactType::Background);
mc.finish();

assert_eq!(
content.finish().into_vec(),
b"/Test<</ActualText(Actual)/MCID 1/Type/Background>>BDC"
);
}
}
28 changes: 21 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ use std::fmt::{self, Debug, Formatter};
use std::io::Write;
use std::ops::{Deref, DerefMut};

use crate::chunk::WriteSettings;

use self::writers::*;

/// A builder for a PDF file.
Expand All @@ -227,6 +229,14 @@ impl Pdf {
Self::with_capacity(8 * 1024)
}

/// Create a new PDF with the given write settings.
pub fn new_with(write_settings: WriteSettings) -> Self {
let mut pdf = Self::new();
pdf.write_settings = write_settings;

pdf
}

/// Create a new PDF with the specified initial buffer capacity.
pub fn with_capacity(capacity: usize) -> Self {
let mut chunk = Chunk::with_capacity(capacity);
Expand Down Expand Up @@ -298,7 +308,7 @@ impl Pdf {
///
/// Panics if any indirect reference id was used twice.
pub fn finish(self) -> Vec<u8> {
let Chunk { mut buf, mut offsets } = self.chunk;
let Chunk { mut buf, mut offsets, write_settings } = self.chunk;

offsets.sort();

Expand Down Expand Up @@ -346,7 +356,7 @@ impl Pdf {
// Write the trailer dictionary.
buf.extend(b"trailer\n");

let mut trailer = Obj::direct(&mut buf, 0).dict();
let mut trailer = Obj::direct(&mut buf, 0, write_settings, false).dict();
trailer.pair(Name(b"Size"), xref_len);

if let Some(catalog_id) = self.catalog_id {
Expand Down Expand Up @@ -412,11 +422,11 @@ mod tests {
}

/// Return the slice of bytes written during the execution of `f`.
pub fn slice<F>(f: F) -> Vec<u8>
pub fn slice<F>(f: F, write_settings: WriteSettings) -> Vec<u8>
where
F: FnOnce(&mut Pdf),
{
let mut w = Pdf::new();
let mut w = Pdf::new_with(write_settings);
let start = w.len();
f(&mut w);
let end = w.len();
Expand All @@ -425,12 +435,16 @@ mod tests {
}

/// Return the slice of bytes written for an object.
pub fn slice_obj<F>(f: F) -> Vec<u8>
pub fn slice_obj<F>(f: F, write_settings: WriteSettings) -> Vec<u8>
where
F: FnOnce(Obj<'_>),
{
let buf = slice(|w| f(w.indirect(Ref::new(1))));
buf[8..buf.len() - 9].to_vec()
let buf = slice(|w| f(w.indirect(Ref::new(1))), write_settings);
if write_settings.pretty {
buf[8..buf.len() - 9].to_vec()
} else {
buf[8..buf.len() - 8].to_vec()
}
}

#[test]
Expand Down
10 changes: 9 additions & 1 deletion src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ macro_rules! test {
#[cfg(test)]
macro_rules! test_obj {
(|$obj:ident| $write:expr, $($tts:tt)*) => {{
test!(crate::tests::slice_obj(|$obj| { $write; }), $($tts)*)
test!(crate::tests::slice_obj(|$obj| { $write; }, crate::WriteSettings::default()), $($tts)*)
}}
}

/// Test how an object is written, without pretty-printing.
#[cfg(test)]
macro_rules! test_obj_no_pretty {
(|$obj:ident| $write:expr, $($tts:tt)*) => {{
test!(crate::tests::slice_obj(|$obj| { $write; }, crate::WriteSettings { pretty: false }), $($tts)*)
}}
}

Expand Down
Loading