From c750a190fa73baf6b5fa2bd575b0640e0cb05bfd Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:36:40 +0200 Subject: [PATCH 01/36] First attempt --- src/chunk.rs | 28 +++++++++++++++++++++-- src/content.rs | 25 ++++++++++++++++---- src/lib.rs | 4 ++-- src/object.rs | 61 +++++++++++++++++++++++++++++++++++++------------ src/renumber.rs | 6 ++++- 5 files changed, 99 insertions(+), 25 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index b5551d4..a56a7ee 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -1,5 +1,16 @@ use super::*; +#[derive(Debug, Clone, Copy)] +pub struct WriteSettings { + pub pretty: bool, +} + +impl Default for WriteSettings { + fn default() -> Self { + Self { pretty: false } + } +} + /// A builder for a collection of indirect PDF objects. /// /// This type holds written top-level indirect PDF objects. Typically, you won't @@ -14,6 +25,7 @@ use super::*; pub struct Chunk { pub(crate) buf: Buf, pub(crate) offsets: Vec<(Ref, usize)>, + pub(crate) settings: WriteSettings, } impl Chunk { @@ -25,7 +37,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![], + settings: Default::default(), + } + } + + /// TODO + pub fn new_with(settings: WriteSettings) -> Self { + let mut chunk = Self::new(); + chunk.settings = settings; + + chunk } /// The number of bytes that were written so far. @@ -148,7 +172,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.settings) } /// Start writing an indirectly referenceable stream. diff --git a/src/content.rs b/src/content.rs index d9ecc8a..add0d46 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1,9 +1,11 @@ use super::*; +use crate::chunk::WriteSettings; use crate::object::TextStrLike; /// A builder for a content stream. pub struct Content { buf: Buf, + settings: WriteSettings, q_depth: usize, } @@ -16,15 +18,27 @@ impl Content { Self::with_capacity(1024) } + /// TODO + pub fn new_with(settings: WriteSettings) -> Self { + let mut content = Self::new(); + content.settings = 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, + 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.settings) } /// Return the buffer of the content stream. @@ -51,12 +65,13 @@ pub struct Operation<'a> { buf: &'a mut Buf, op: &'a str, first: bool, + 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, settings: WriteSettings) -> Self { + Self { buf, op, first: true, settings } } /// Write a primitive operand. @@ -86,7 +101,7 @@ impl<'a> Operation<'a> { self.buf.push(b' '); } self.first = false; - Obj::direct(self.buf, 0) + Obj::direct(self.buf, 0, self.settings) } } diff --git a/src/lib.rs b/src/lib.rs index 33e8193..b9b9a36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -298,7 +298,7 @@ impl Pdf { /// /// Panics if any indirect reference id was used twice. pub fn finish(self) -> Vec { - let Chunk { mut buf, mut offsets } = self.chunk; + let Chunk { mut buf, mut offsets, settings } = self.chunk; offsets.sort(); @@ -346,7 +346,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, settings).dict(); trailer.pair(Name(b"Size"), xref_len); if let Some(catalog_id) = self.catalog_id { diff --git a/src/object.rs b/src/object.rs index 99e8ca6..8cc87f0 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,3 +1,4 @@ +use crate::chunk::WriteSettings; use std::convert::TryFrom; use std::marker::PhantomData; use std::mem::ManuallyDrop; @@ -575,21 +576,22 @@ pub struct Obj<'a> { buf: &'a mut Buf, indirect: bool, indent: u8, + settings: WriteSettings, } impl<'a> Obj<'a> { /// Start a new direct object. #[inline] - pub(crate) fn direct(buf: &'a mut Buf, indent: u8) -> Self { - Self { buf, indirect: false, indent } + pub(crate) fn direct(buf: &'a mut Buf, indent: u8, settings: WriteSettings) -> Self { + Self { buf, indirect: false, indent, settings } } /// Start a new indirect object. #[inline] - pub(crate) fn indirect(buf: &'a mut Buf, id: Ref) -> Self { + pub(crate) fn indirect(buf: &'a mut Buf, id: Ref, settings: WriteSettings) -> Self { buf.push_int(id.get()); buf.extend(b" 0 obj\n"); - Self { buf, indirect: true, indent: 0 } + Self { buf, indirect: true, indent: 0, settings } } /// Write a primitive object. @@ -597,7 +599,11 @@ impl<'a> Obj<'a> { pub fn primitive(self, value: T) { value.write(self.buf); if self.indirect { - self.buf.extend(b"\nendobj\n\n"); + self.buf.extend(b"\nendobj\n"); + + if self.settings.pretty { + self.buf.extend(b"\n"); + } } } @@ -655,6 +661,7 @@ pub struct Array<'a> { buf: &'a mut Buf, indirect: bool, indent: u8, + settings: WriteSettings, len: i32, } @@ -664,6 +671,7 @@ writer!(Array: |obj| { buf: obj.buf, indirect: obj.indirect, indent: obj.indent, + settings: obj.settings, len: 0, } }); @@ -688,7 +696,7 @@ impl<'a> Array<'a> { self.buf.push(b' '); } self.len += 1; - Obj::direct(self.buf, self.indent) + Obj::direct(self.buf, self.indent, self.settings) } /// Write an item with a primitive value. @@ -725,7 +733,11 @@ impl Drop for Array<'_> { self.buf.limits.register_array_len(self.len() as usize); self.buf.push(b']'); if self.indirect { - self.buf.extend(b"\nendobj\n\n"); + self.buf.extend(b"\nendobj\n"); + + if self.settings.pretty { + self.buf.extend(b"\n"); + } } } } @@ -802,6 +814,7 @@ pub struct Dict<'a> { buf: &'a mut Buf, indirect: bool, indent: u8, + settings: WriteSettings, len: i32, } @@ -811,6 +824,7 @@ writer!(Dict: |obj| { buf: obj.buf, indirect: obj.indirect, indent: obj.indent.saturating_add(2), + settings: obj.settings, len: 0, } }); @@ -832,16 +846,19 @@ impl<'a> Dict<'a> { #[inline] pub fn insert(&mut self, key: Name) -> Obj<'_> { self.len += 1; - self.buf.push(b'\n'); - for _ in 0..self.indent { - self.buf.push(b' '); + if self.settings.pretty { + self.buf.push(b'\n'); + + for _ in 0..self.indent { + self.buf.push(b' '); + } } self.buf.push_val(key); self.buf.push(b' '); - Obj::direct(self.buf, self.indent) + Obj::direct(self.buf, self.indent, self.settings) } /// Write a pair with a primitive value. @@ -876,15 +893,21 @@ impl Drop for Dict<'_> { fn drop(&mut self) { self.buf.limits.register_dict_entries(self.len as usize); - if self.len != 0 { + if self.len != 0 && self.settings.pretty { self.buf.push(b'\n'); for _ in 0..self.indent - 2 { self.buf.push(b' '); } } + self.buf.extend(b">>"); + if self.indirect { - self.buf.extend(b"\nendobj\n\n"); + self.buf.extend(b"\nendobj\n"); + + if self.settings.pretty { + self.buf.extend(b"\n"); + } } } } @@ -1005,11 +1028,19 @@ impl Drop for Stream<'_> { let dict_len = self.dict.len as usize; self.dict.buf.limits.register_dict_entries(dict_len); - self.dict.buf.extend(b"\n>>"); + if self.dict.settings.pretty { + self.dict.buf.extend(b"\n"); + } + + self.dict.buf.extend(b">>"); self.dict.buf.extend(b"\nstream\n"); self.dict.buf.extend(self.data.as_ref()); self.dict.buf.extend(b"\nendstream"); - self.dict.buf.extend(b"\nendobj\n\n"); + self.dict.buf.extend(b"\nendobj\n"); + + if self.dict.settings.pretty { + self.dict.buf.extend(b"\n"); + } } } diff --git a/src/renumber.rs b/src/renumber.rs index c095223..cf81444 100644 --- a/src/renumber.rs +++ b/src/renumber.rs @@ -18,7 +18,11 @@ pub fn renumber(source: &Chunk, target: &mut Chunk, mapping: &mut dyn FnMut(Ref) target.buf.push_int(gen); target.buf.extend(b" obj\n"); patch_object(slice, &mut target.buf, mapping); - target.buf.extend(b"\nendobj\n\n"); + target.buf.extend(b"\nendobj\n"); + + if target.settings.pretty { + target.buf.extend(b"\n"); + } } } From aad07d4e4b9cae21d4cfee7eca839370e75c1bc8 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Sun, 12 Oct 2025 15:50:24 +0200 Subject: [PATCH 02/36] Make even tighter --- src/content.rs | 3 +- src/lib.rs | 2 +- src/object.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++------ 3 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/content.rs b/src/content.rs index add0d46..546e3da 100644 --- a/src/content.rs +++ b/src/content.rs @@ -101,7 +101,8 @@ impl<'a> Operation<'a> { self.buf.push(b' '); } self.first = false; - Obj::direct(self.buf, 0, self.settings) + // TODO: Refine padding? + Obj::direct(self.buf, 0, self.settings, false) } } diff --git a/src/lib.rs b/src/lib.rs index b9b9a36..c9dbe8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -346,7 +346,7 @@ impl Pdf { // Write the trailer dictionary. buf.extend(b"trailer\n"); - let mut trailer = Obj::direct(&mut buf, 0, settings).dict(); + let mut trailer = Obj::direct(&mut buf, 0, settings, false).dict(); trailer.pair(Name(b"Size"), xref_len); if let Some(catalog_id) = self.catalog_id { diff --git a/src/object.rs b/src/object.rs index 8cc87f0..4f1e54a 100644 --- a/src/object.rs +++ b/src/object.rs @@ -8,6 +8,9 @@ use super::*; /// A primitive PDF object. pub trait Primitive { + /// Whether the primitive object starts with one of the delimiters. + const HAS_DELIMITER: bool; + /// Write the object into a buffer. fn write(self, buf: &mut Buf); } @@ -16,6 +19,8 @@ impl Primitive for &T where T: Copy, { + const HAS_DELIMITER: bool = T::HAS_DELIMITER; + #[inline] fn write(self, buf: &mut Buf) { (*self).write(buf); @@ -23,6 +28,8 @@ where } impl Primitive for bool { + const HAS_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { if self { @@ -34,6 +41,8 @@ impl Primitive for bool { } impl Primitive for i32 { + const HAS_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { buf.push_int(self); @@ -41,6 +50,8 @@ impl Primitive for i32 { } impl Primitive for f32 { + const HAS_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { buf.push_float(self); @@ -70,6 +81,8 @@ impl Str<'_> { } impl Primitive for Str<'_> { + const HAS_DELIMITER: bool = true; + fn write(self, buf: &mut Buf) { buf.limits.register_str_len(self.0.len()); @@ -134,6 +147,8 @@ impl Primitive for Str<'_> { pub struct TextStr<'a>(pub &'a str); impl Primitive for TextStr<'_> { + const HAS_DELIMITER: bool = true; + fn write(self, buf: &mut Buf) { buf.limits.register_str_len(self.0.len()); @@ -219,6 +234,8 @@ impl LanguageIdentifier { pub struct TextStrWithLang<'a, 'b>(pub &'b [(LanguageIdentifier, &'a str)]); impl<'a, 'b> Primitive for TextStrWithLang<'a, 'b> { + const HAS_DELIMITER: bool = true; + fn write(self, buf: &mut Buf) { let mut len = 0; let mut buf_len = 6; @@ -295,6 +312,8 @@ impl<'a, 'b> TextStrLike for TextStrWithLang<'a, 'b> {} pub struct Name<'a>(pub &'a [u8]); impl Primitive for Name<'_> { + const HAS_DELIMITER: bool = true; + fn write(self, buf: &mut Buf) { buf.limits.register_name_len(self.0.len()); @@ -342,6 +361,8 @@ fn is_regular_character(byte: u8) -> bool { pub struct Null; impl Primitive for Null { + const HAS_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { buf.extend(b"null"); @@ -391,6 +412,8 @@ impl Ref { } impl Primitive for Ref { + const HAS_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { buf.push_int(self.0.get()); @@ -427,6 +450,8 @@ impl Rect { } impl Primitive for Rect { + const HAS_DELIMITER: bool = true; + #[inline] fn write(self, buf: &mut Buf) { buf.push(b'['); @@ -542,6 +567,8 @@ impl Date { } impl Primitive for Date { + const HAS_DELIMITER: bool = true; + fn write(self, buf: &mut Buf) { buf.extend(b"(D:"); @@ -577,13 +604,25 @@ pub struct Obj<'a> { indirect: bool, indent: u8, settings: WriteSettings, + needs_padding: bool, } impl<'a> Obj<'a> { /// Start a new direct object. #[inline] - pub(crate) fn direct(buf: &'a mut Buf, indent: u8, settings: WriteSettings) -> Self { - Self { buf, indirect: false, indent, settings } + pub(crate) fn direct( + buf: &'a mut Buf, + indent: u8, + settings: WriteSettings, + needs_padding: bool, + ) -> Self { + Self { + buf, + indirect: false, + indent, + settings, + needs_padding, + } } /// Start a new indirect object. @@ -591,12 +630,22 @@ impl<'a> Obj<'a> { pub(crate) fn indirect(buf: &'a mut Buf, id: Ref, settings: WriteSettings) -> Self { buf.push_int(id.get()); buf.extend(b" 0 obj\n"); - Self { buf, indirect: true, indent: 0, settings } + Self { + buf, + indirect: true, + indent: 0, + settings, + needs_padding: false, + } } /// Write a primitive object. #[inline] pub fn primitive(self, value: T) { + if self.needs_padding && !T::HAS_DELIMITER { + self.buf.extend(b" "); + } + value.write(self.buf); if self.indirect { self.buf.extend(b"\nendobj\n"); @@ -692,11 +741,18 @@ impl<'a> Array<'a> { /// Start writing an arbitrary item. #[inline] pub fn push(&mut self) -> Obj<'_> { - if self.len != 0 { - self.buf.push(b' '); - } + let needs_padding = if self.len != 0 { + if self.settings.pretty { + self.buf.push(b' '); + false + } else { + true + } + } else { + false + }; self.len += 1; - Obj::direct(self.buf, self.indent, self.settings) + Obj::direct(self.buf, self.indent, self.settings, needs_padding) } /// Write an item with a primitive value. @@ -856,9 +912,14 @@ impl<'a> Dict<'a> { } self.buf.push_val(key); - self.buf.push(b' '); + let needs_padding = if self.settings.pretty { + self.buf.push(b' '); + false + } else { + true + }; - Obj::direct(self.buf, self.indent, self.settings) + Obj::direct(self.buf, self.indent, self.settings, needs_padding) } /// Write a pair with a primitive value. From c715181c587b3a48d37a14d3594a56b15aa14ccb Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:26:01 +0200 Subject: [PATCH 03/36] Rename `settings` to `write_settings` --- src/chunk.rs | 17 +++++++++++------ src/content.rs | 22 +++++++++++++--------- src/lib.rs | 4 ++-- src/object.rs | 20 ++++++++++++-------- src/renumber.rs | 2 +- 5 files changed, 39 insertions(+), 26 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index a56a7ee..cf1133f 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -1,13 +1,18 @@ 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 + /// a way that they are easier to read by humans by applying more padding and indentation, at + /// the cost of higher 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: false } + Self { pretty: true } } } @@ -25,7 +30,7 @@ impl Default for WriteSettings { pub struct Chunk { pub(crate) buf: Buf, pub(crate) offsets: Vec<(Ref, usize)>, - pub(crate) settings: WriteSettings, + pub(crate) write_settings: WriteSettings, } impl Chunk { @@ -40,14 +45,14 @@ impl Chunk { Self { buf: Buf::with_capacity(capacity), offsets: vec![], - settings: Default::default(), + write_settings: Default::default(), } } /// TODO - pub fn new_with(settings: WriteSettings) -> Self { + pub fn new_with(write_settings: WriteSettings) -> Self { let mut chunk = Self::new(); - chunk.settings = settings; + chunk.write_settings = write_settings; chunk } @@ -172,7 +177,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, self.settings) + Obj::indirect(&mut self.buf, id, self.write_settings) } /// Start writing an indirectly referenceable stream. diff --git a/src/content.rs b/src/content.rs index 546e3da..4471443 100644 --- a/src/content.rs +++ b/src/content.rs @@ -5,7 +5,7 @@ use crate::object::TextStrLike; /// A builder for a content stream. pub struct Content { buf: Buf, - settings: WriteSettings, + write_settings: WriteSettings, q_depth: usize, } @@ -19,9 +19,9 @@ impl Content { } /// TODO - pub fn new_with(settings: WriteSettings) -> Self { + pub fn new_with(write_settings: WriteSettings) -> Self { let mut content = Self::new(); - content.settings = settings; + content.write_settings = write_settings; content } @@ -31,14 +31,14 @@ impl Content { Self { buf: Buf::with_capacity(capacity), q_depth: 0, - settings: Default::default(), + 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, self.settings) + Operation::start(&mut self.buf, operator, self.write_settings) } /// Return the buffer of the content stream. @@ -65,13 +65,17 @@ pub struct Operation<'a> { buf: &'a mut Buf, op: &'a str, first: bool, - settings: WriteSettings, + write_settings: WriteSettings, } impl<'a> Operation<'a> { #[inline] - pub(crate) fn start(buf: &'a mut Buf, op: &'a str, settings: WriteSettings) -> Self { - Self { buf, op, first: true, settings } + 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. @@ -102,7 +106,7 @@ impl<'a> Operation<'a> { } self.first = false; // TODO: Refine padding? - Obj::direct(self.buf, 0, self.settings, false) + Obj::direct(self.buf, 0, self.write_settings, false) } } diff --git a/src/lib.rs b/src/lib.rs index c9dbe8c..5b30ad6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -298,7 +298,7 @@ impl Pdf { /// /// Panics if any indirect reference id was used twice. pub fn finish(self) -> Vec { - let Chunk { mut buf, mut offsets, settings } = self.chunk; + let Chunk { mut buf, mut offsets, write_settings } = self.chunk; offsets.sort(); @@ -346,7 +346,7 @@ impl Pdf { // Write the trailer dictionary. buf.extend(b"trailer\n"); - let mut trailer = Obj::direct(&mut buf, 0, settings, false).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 { diff --git a/src/object.rs b/src/object.rs index 4f1e54a..2db9642 100644 --- a/src/object.rs +++ b/src/object.rs @@ -603,7 +603,7 @@ pub struct Obj<'a> { buf: &'a mut Buf, indirect: bool, indent: u8, - settings: WriteSettings, + write_settings: WriteSettings, needs_padding: bool, } @@ -613,28 +613,32 @@ impl<'a> Obj<'a> { pub(crate) fn direct( buf: &'a mut Buf, indent: u8, - settings: WriteSettings, + write_settings: WriteSettings, needs_padding: bool, ) -> Self { Self { buf, indirect: false, indent, - settings, + write_settings, needs_padding, } } /// Start a new indirect object. #[inline] - pub(crate) fn indirect(buf: &'a mut Buf, id: Ref, settings: WriteSettings) -> Self { + pub(crate) fn indirect( + buf: &'a mut Buf, + id: Ref, + write_settings: WriteSettings, + ) -> Self { buf.push_int(id.get()); buf.extend(b" 0 obj\n"); Self { buf, indirect: true, indent: 0, - settings, + write_settings, needs_padding: false, } } @@ -650,7 +654,7 @@ impl<'a> Obj<'a> { if self.indirect { self.buf.extend(b"\nendobj\n"); - if self.settings.pretty { + if self.write_settings.pretty { self.buf.extend(b"\n"); } } @@ -720,7 +724,7 @@ writer!(Array: |obj| { buf: obj.buf, indirect: obj.indirect, indent: obj.indent, - settings: obj.settings, + settings: obj.write_settings, len: 0, } }); @@ -880,7 +884,7 @@ writer!(Dict: |obj| { buf: obj.buf, indirect: obj.indirect, indent: obj.indent.saturating_add(2), - settings: obj.settings, + settings: obj.write_settings, len: 0, } }); diff --git a/src/renumber.rs b/src/renumber.rs index cf81444..2df1bb1 100644 --- a/src/renumber.rs +++ b/src/renumber.rs @@ -20,7 +20,7 @@ pub fn renumber(source: &Chunk, target: &mut Chunk, mapping: &mut dyn FnMut(Ref) patch_object(slice, &mut target.buf, mapping); target.buf.extend(b"\nendobj\n"); - if target.settings.pretty { + if target.write_settings.pretty { target.buf.extend(b"\n"); } } From efc6da8388edf4d0aeb0bc381cad4ea2a3892613 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:30:05 +0200 Subject: [PATCH 04/36] Add more methods --- src/chunk.rs | 7 ++++++- src/content.rs | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index cf1133f..7fe548f 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -49,7 +49,7 @@ impl Chunk { } } - /// TODO + /// Create a new chunk with the given write settings. pub fn new_with(write_settings: WriteSettings) -> Self { let mut chunk = Self::new(); chunk.write_settings = write_settings; @@ -64,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() diff --git a/src/content.rs b/src/content.rs index 4471443..f2ab9c5 100644 --- a/src/content.rs +++ b/src/content.rs @@ -18,7 +18,7 @@ impl Content { Self::with_capacity(1024) } - /// TODO + /// 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; From 6c110b4a2011ca98245e88b28b1e73692a678e78 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:30:40 +0200 Subject: [PATCH 05/36] Typo --- src/chunk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chunk.rs b/src/chunk.rs index 7fe548f..f9d1a31 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -5,7 +5,7 @@ use super::*; pub struct WriteSettings { /// Whether to enable pretty-writing. In this case, `pdf-writer` will serialize PDFs in such /// a way that they are easier to read by humans by applying more padding and indentation, at - /// the cost of higher file sizes. If disabled, `pdf-writer` will serialize objects as compactly + /// 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, } From c0c0e3defbde7b24a99dc13ac75b76a32de78396 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:34:40 +0200 Subject: [PATCH 06/36] Remove TODO --- src/content.rs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/content.rs b/src/content.rs index f2ab9c5..7823cfa 100644 --- a/src/content.rs +++ b/src/content.rs @@ -98,15 +98,22 @@ 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' '); - } + let needs_padding = if !self.first { + if self.write_settings.pretty { + self.buf.push(b' '); + false + } else { + true + } + } else { + false + }; + self.first = false; - // TODO: Refine padding? - Obj::direct(self.buf, 0, self.write_settings, false) + Obj::direct(self.buf, 0, self.write_settings, needs_padding) } } From 159986ba9cdd9015ab6c5ddc2c1d6ae1f0c077cc Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 10:53:30 +0200 Subject: [PATCH 07/36] Add some tests --- src/content.rs | 41 +++++++++++++++++++++++++++++++++++++++++ src/object.rs | 9 ++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/content.rs b/src/content.rs index 7823cfa..c3be590 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1735,4 +1735,45 @@ 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.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\nBT\n[] TJ\n[(AB)2(CD)4(EF)] TJ\nET" + ); + } + + // TODO: Dont' write newlines between operations if not necessary? + + #[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<> BDC" + ); + } } diff --git a/src/object.rs b/src/object.rs index 2db9642..7364bc9 100644 --- a/src/object.rs +++ b/src/object.rs @@ -356,6 +356,11 @@ fn is_regular_character(byte: u8) -> bool { ) } +#[inline] +fn is_delimiter_character(byte: u8) -> bool { + matches!(byte, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'/' | b'%') +} + /// The null object. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Null; @@ -646,7 +651,9 @@ impl<'a> Obj<'a> { /// Write a primitive object. #[inline] pub fn primitive(self, value: T) { - if self.needs_padding && !T::HAS_DELIMITER { + let ends_with_delimiter = self.buf.last().copied().is_some_and(is_delimiter_character); + + if self.needs_padding && !T::HAS_DELIMITER && !ends_with_delimiter { self.buf.extend(b" "); } From d51017455c73d6144187d4d278d5343744234828 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:08:59 +0200 Subject: [PATCH 08/36] Don't write space before operator if not necessary --- src/content.rs | 15 ++++++++------- src/object.rs | 7 ++++--- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/content.rs b/src/content.rs index c3be590..8002b84 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1,6 +1,6 @@ use super::*; use crate::chunk::WriteSettings; -use crate::object::TextStrLike; +use crate::object::{is_delimiter_character, TextStrLike}; /// A builder for a content stream. pub struct Content { @@ -121,7 +121,11 @@ impl Drop for Operation<'_> { #[inline] fn drop(&mut self) { if !self.first { - self.buf.push(b' '); + if self.write_settings.pretty + || self.buf.last().is_some_and(|b| !is_delimiter_character(*b)) + { + self.buf.push(b' '); + } } self.buf.extend(self.op.as_bytes()); self.buf.push(b'\n'); @@ -1755,12 +1759,9 @@ mod tests { assert_eq!( content.finish().into_vec(), - b"/F1 12 Tf\nBT\n[] TJ\n[(AB)2(CD)4(EF)] TJ\nET" + b"/F1 12 Tf\nBT\n[]TJ\n[(AB)2(CD)4(EF)]TJ\nET" ); } - - // TODO: Dont' write newlines between operations if not necessary? - #[test] fn test_content_dict_no_pretty() { let mut content = Content::new_with(WriteSettings { pretty: false }); @@ -1773,7 +1774,7 @@ mod tests { assert_eq!( content.finish().into_vec(), - b"/Test<> BDC" + b"/Test<>BDC" ); } } diff --git a/src/object.rs b/src/object.rs index 7364bc9..abc5893 100644 --- a/src/object.rs +++ b/src/object.rs @@ -357,7 +357,7 @@ fn is_regular_character(byte: u8) -> bool { } #[inline] -fn is_delimiter_character(byte: u8) -> bool { +pub(crate) fn is_delimiter_character(byte: u8) -> bool { matches!(byte, b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'/' | b'%') } @@ -651,8 +651,9 @@ impl<'a> Obj<'a> { /// Write a primitive object. #[inline] pub fn primitive(self, value: T) { - let ends_with_delimiter = self.buf.last().copied().is_some_and(is_delimiter_character); - + let ends_with_delimiter = + self.buf.last().copied().is_some_and(is_delimiter_character); + if self.needs_padding && !T::HAS_DELIMITER && !ends_with_delimiter { self.buf.extend(b" "); } From d751b0b8526c28722b0adaa451b0ebaf97cc9659 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 11:11:10 +0200 Subject: [PATCH 09/36] Extend a test case --- src/content.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/content.rs b/src/content.rs index 8002b84..1b44038 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1745,6 +1745,7 @@ mod tests { 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 @@ -1759,7 +1760,7 @@ mod tests { assert_eq!( content.finish().into_vec(), - b"/F1 12 Tf\nBT\n[]TJ\n[(AB)2(CD)4(EF)]TJ\nET" + b"/F1 12 Tf\n/F2 15 Tf\nBT\n[]TJ\n[(AB)2(CD)4(EF)]TJ\nET" ); } #[test] From 4b6c85afe52b5de268598023f1afcaeb5b0fd912 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:25:40 +0200 Subject: [PATCH 10/36] More improvements --- src/content.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/content.rs b/src/content.rs index 1b44038..56ac5c2 100644 --- a/src/content.rs +++ b/src/content.rs @@ -109,7 +109,12 @@ impl<'a> Operation<'a> { true } } else { - false + if self.write_settings.pretty { + self.buf.push(b'\n'); + false + } else { + true + } }; self.first = false; @@ -126,9 +131,14 @@ impl Drop for Operation<'_> { { self.buf.push(b' '); } + } else { + if self.write_settings.pretty + || self.buf.last().is_some_and(|b| !is_delimiter_character(*b)) + { + self.buf.push(b'\n'); + } } self.buf.extend(self.op.as_bytes()); - self.buf.push(b'\n'); } } @@ -1760,7 +1770,7 @@ mod tests { assert_eq!( content.finish().into_vec(), - b"/F1 12 Tf\n/F2 15 Tf\nBT\n[]TJ\n[(AB)2(CD)4(EF)]TJ\nET" + b"/F1 12 Tf/F2 15 Tf\nBT[]TJ[(AB)2(CD)4(EF)]TJ\nET" ); } #[test] From 6d5ae2cd0e1a51a7fdcb4e9887e5b782da62281f Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:28:10 +0200 Subject: [PATCH 11/36] Re-arrange --- src/content.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/content.rs b/src/content.rs index 56ac5c2..3c71a51 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1773,6 +1773,7 @@ mod tests { 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 }); From 2f0250a88bd365f1ab8c238de6e725dfb876965c Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:42:40 +0200 Subject: [PATCH 12/36] More tweaks --- src/content.rs | 47 +++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/content.rs b/src/content.rs index 3c71a51..a7aa857 100644 --- a/src/content.rs +++ b/src/content.rs @@ -101,20 +101,18 @@ impl<'a> Operation<'a> { /// Start writing an arbitrary object operand. #[inline] pub fn obj(&mut self) -> Obj<'_> { - let needs_padding = if !self.first { - if self.write_settings.pretty { - self.buf.push(b' '); - false - } else { - true - } + // In case we are writing the first object, we want a newline to separate it from + // previous operations. 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 { + self.buf.push(pad_byte); + false } else { - if self.write_settings.pretty { - self.buf.push(b'\n'); - false - } else { - true - } + true }; self.first = false; @@ -125,19 +123,16 @@ impl<'a> Operation<'a> { impl Drop for Operation<'_> { #[inline] fn drop(&mut self) { - if !self.first { - if self.write_settings.pretty - || self.buf.last().is_some_and(|b| !is_delimiter_character(*b)) - { - self.buf.push(b' '); - } - } else { - if self.write_settings.pretty - || self.buf.last().is_some_and(|b| !is_delimiter_character(*b)) - { - self.buf.push(b'\n'); - } + 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 + // 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.push(pad_byte); } + self.buf.extend(self.op.as_bytes()); } } @@ -1773,7 +1768,7 @@ mod tests { 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 }); From 45612a48090d39ba0a246307f7c4897f7eccd0fb Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:44:11 +0200 Subject: [PATCH 13/36] Adapt comment --- src/content.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content.rs b/src/content.rs index a7aa857..2e6f055 100644 --- a/src/content.rs +++ b/src/content.rs @@ -102,7 +102,7 @@ impl<'a> Operation<'a> { #[inline] pub fn obj(&mut self) -> Obj<'_> { // In case we are writing the first object, we want a newline to separate it from - // previous operations. Otherwise, a space is sufficient. + // 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 From 556140f8a4016a7384b6a16c21d4e40ea111d2dd Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:45:55 +0200 Subject: [PATCH 14/36] Adapt wording --- src/object.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/object.rs b/src/object.rs index abc5893..7660a2e 100644 --- a/src/object.rs +++ b/src/object.rs @@ -8,7 +8,7 @@ use super::*; /// A primitive PDF object. pub trait Primitive { - /// Whether the primitive object starts with one of the delimiters. + /// Whether the primitive object starts with one of the PDF delimiter characters. const HAS_DELIMITER: bool; /// Write the object into a buffer. From 0587ed29d31084db4f581f90794ac3756032688a Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:46:42 +0200 Subject: [PATCH 15/36] Rename --- src/object.rs | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/object.rs b/src/object.rs index 7660a2e..2f40b05 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1,15 +1,15 @@ -use crate::chunk::WriteSettings; use std::convert::TryFrom; use std::marker::PhantomData; use std::mem::ManuallyDrop; use std::num::NonZeroI32; use super::*; +use crate::chunk::WriteSettings; /// A primitive PDF object. pub trait Primitive { /// Whether the primitive object starts with one of the PDF delimiter characters. - const HAS_DELIMITER: bool; + const STARTS_WITH_DELIMITER: bool; /// Write the object into a buffer. fn write(self, buf: &mut Buf); @@ -19,7 +19,7 @@ impl Primitive for &T where T: Copy, { - const HAS_DELIMITER: bool = T::HAS_DELIMITER; + const STARTS_WITH_DELIMITER: bool = T::STARTS_WITH_DELIMITER; #[inline] fn write(self, buf: &mut Buf) { @@ -28,7 +28,7 @@ where } impl Primitive for bool { - const HAS_DELIMITER: bool = false; + const STARTS_WITH_DELIMITER: bool = false; #[inline] fn write(self, buf: &mut Buf) { @@ -41,7 +41,7 @@ impl Primitive for bool { } impl Primitive for i32 { - const HAS_DELIMITER: bool = false; + const STARTS_WITH_DELIMITER: bool = false; #[inline] fn write(self, buf: &mut Buf) { @@ -50,7 +50,7 @@ impl Primitive for i32 { } impl Primitive for f32 { - const HAS_DELIMITER: bool = false; + const STARTS_WITH_DELIMITER: bool = false; #[inline] fn write(self, buf: &mut Buf) { @@ -81,7 +81,7 @@ impl Str<'_> { } impl Primitive for Str<'_> { - const HAS_DELIMITER: bool = true; + const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { buf.limits.register_str_len(self.0.len()); @@ -147,7 +147,7 @@ impl Primitive for Str<'_> { pub struct TextStr<'a>(pub &'a str); impl Primitive for TextStr<'_> { - const HAS_DELIMITER: bool = true; + const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { buf.limits.register_str_len(self.0.len()); @@ -234,7 +234,7 @@ impl LanguageIdentifier { pub struct TextStrWithLang<'a, 'b>(pub &'b [(LanguageIdentifier, &'a str)]); impl<'a, 'b> Primitive for TextStrWithLang<'a, 'b> { - const HAS_DELIMITER: bool = true; + const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { let mut len = 0; @@ -312,7 +312,7 @@ impl<'a, 'b> TextStrLike for TextStrWithLang<'a, 'b> {} pub struct Name<'a>(pub &'a [u8]); impl Primitive for Name<'_> { - const HAS_DELIMITER: bool = true; + const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { buf.limits.register_name_len(self.0.len()); @@ -366,7 +366,7 @@ pub(crate) fn is_delimiter_character(byte: u8) -> bool { pub struct Null; impl Primitive for Null { - const HAS_DELIMITER: bool = false; + const STARTS_WITH_DELIMITER: bool = false; #[inline] fn write(self, buf: &mut Buf) { @@ -417,7 +417,7 @@ impl Ref { } impl Primitive for Ref { - const HAS_DELIMITER: bool = false; + const STARTS_WITH_DELIMITER: bool = false; #[inline] fn write(self, buf: &mut Buf) { @@ -455,7 +455,7 @@ impl Rect { } impl Primitive for Rect { - const HAS_DELIMITER: bool = true; + const STARTS_WITH_DELIMITER: bool = true; #[inline] fn write(self, buf: &mut Buf) { @@ -572,7 +572,7 @@ impl Date { } impl Primitive for Date { - const HAS_DELIMITER: bool = true; + const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { buf.extend(b"(D:"); @@ -654,7 +654,7 @@ impl<'a> Obj<'a> { let ends_with_delimiter = self.buf.last().copied().is_some_and(is_delimiter_character); - if self.needs_padding && !T::HAS_DELIMITER && !ends_with_delimiter { + if self.needs_padding && !T::STARTS_WITH_DELIMITER && !ends_with_delimiter { self.buf.extend(b" "); } From 67c23335062ed656a9653c3a3d3f41600ad106df Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 19:57:17 +0200 Subject: [PATCH 16/36] More --- src/object.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/object.rs b/src/object.rs index 2f40b05..0190517 100644 --- a/src/object.rs +++ b/src/object.rs @@ -651,6 +651,14 @@ impl<'a> Obj<'a> { /// Write a primitive object. #[inline] pub fn primitive(self, value: T) { + // Normally, we need to separate different PDF objects by a whitespace. he key to the + // optimizations applied here are explained in 7.2.3 in the PDF reference: + // > The delimiter characters (, ), <, >, [, ], /, and % are special. They + // > delimit syntactic entities such as arrays, names, and comments. Any of these + // > delimiters terminates the entity preceding it and is not included in the entity. + // Therefore, if either the previous byte is a delimiter character or the current token + // starts with one, we don't need to add a whitespace for padding. + let ends_with_delimiter = self.buf.last().copied().is_some_and(is_delimiter_character); @@ -659,6 +667,7 @@ impl<'a> Obj<'a> { } value.write(self.buf); + if self.indirect { self.buf.extend(b"\nendobj\n"); @@ -668,6 +677,9 @@ impl<'a> Obj<'a> { } } + // Note: Arrays and dictionaries always start with a delimiter, so we don't need to do any case + // distinction, unlike in `primitive`. + /// Start writing an array. #[inline] pub fn array(self) -> Array<'a> { @@ -763,7 +775,9 @@ impl<'a> Array<'a> { } else { false }; + self.len += 1; + Obj::direct(self.buf, self.indent, self.settings, needs_padding) } @@ -915,6 +929,8 @@ impl<'a> Dict<'a> { pub fn insert(&mut self, key: Name) -> Obj<'_> { self.len += 1; + // Keys always start with a delimiter since they are names, so we never need + // padding unless `pretty` is activated. if self.settings.pretty { self.buf.push(b'\n'); @@ -924,6 +940,7 @@ impl<'a> Dict<'a> { } self.buf.push_val(key); + let needs_padding = if self.settings.pretty { self.buf.push(b' '); false From d819ac2156b7d14bcd5d8a2522ad02218dbf0636 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:12:32 +0200 Subject: [PATCH 17/36] Add test cases --- src/annotations.rs | 11 +++++++---- src/lib.rs | 26 +++++++++++++++++++------- src/macros.rs | 10 +++++++++- src/object.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/annotations.rs b/src/annotations.rs index ab92417..50d25ec 100644 --- a/src/annotations.rs +++ b/src/annotations.rs @@ -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", diff --git a/src/lib.rs b/src/lib.rs index 5b30ad6..dd7a1b1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -199,12 +199,12 @@ pub use self::object::{ TypedDict, Writer, }; +use self::writers::*; +use crate::chunk::WriteSettings; use std::fmt::{self, Debug, Formatter}; use std::io::Write; use std::ops::{Deref, DerefMut}; -use self::writers::*; - /// A builder for a PDF file. /// /// This type constructs a PDF file in-memory. Aside from a few specific @@ -227,6 +227,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); @@ -412,11 +420,11 @@ mod tests { } /// Return the slice of bytes written during the execution of `f`. - pub fn slice(f: F) -> Vec + pub fn slice(f: F, write_settings: WriteSettings) -> Vec 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(); @@ -425,12 +433,16 @@ mod tests { } /// Return the slice of bytes written for an object. - pub fn slice_obj(f: F) -> Vec + pub fn slice_obj(f: F, write_settings: WriteSettings) -> Vec 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] diff --git a/src/macros.rs b/src/macros.rs index c7cc4a2..cad7c82 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -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)*) }} } diff --git a/src/object.rs b/src/object.rs index 0190517..a20ebe9 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1686,4 +1686,37 @@ mod tests { b"startxref\n94\n%%EOF", ) } + + #[test] + fn test_arrays_no_pretty() { + test_obj_no_pretty!(|obj| obj.array(), b"[]"); + test_obj_no_pretty!( + |obj| obj + .array() + .item(12) + .item(Name(b"Hi")) + .item(Name(b"Hi2")) + .item(false) + .item(TextStr("A string")) + .item(Null) + .item(23.40), + b"[12/Hi/Hi2 false(A String)null 23.4]" + ); + } + + #[test] + fn test_dicts_no_pretty() { + test_obj_no_pretty!(|obj| obj.dict(), b"<<>>"); + test_obj_no_pretty!( + |obj| obj + .dict() + .pair(Name(b"Key1"), 12) + .pair(Name(b"Key2"), Name(b"Hi")) + .pair(Name(b"Key3"), false) + .pair(Name(b"Key4"), TextStr("A string")) + .pair(Name(b"Key5"), Null) + .pair(Name(b"Key6"), 23.40), + b"<>" + ); + } } From 59c7ad0097ec84170970e2fb97f8d73f28457cc8 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:17:05 +0200 Subject: [PATCH 18/36] Extend test cases a bit --- src/object.rs | 46 +++++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/object.rs b/src/object.rs index a20ebe9..94727e6 100644 --- a/src/object.rs +++ b/src/object.rs @@ -1691,16 +1691,22 @@ mod tests { fn test_arrays_no_pretty() { test_obj_no_pretty!(|obj| obj.array(), b"[]"); test_obj_no_pretty!( - |obj| obj - .array() - .item(12) - .item(Name(b"Hi")) - .item(Name(b"Hi2")) - .item(false) - .item(TextStr("A string")) - .item(Null) - .item(23.40), - b"[12/Hi/Hi2 false(A String)null 23.4]" + |obj| { + let mut arr = obj.array(); + + arr.item(12) + .item(Name(b"Hi")) + .item(Name(b"Hi2")) + .item(false) + .item(TextStr("A string")); + + arr.push().dict().pair(Name(b"Test1"), 23); + + arr.item(Null).item(23.40); + + arr + }, + b"[12/Hi/Hi2 false(A string)<>null 23.4]" ); } @@ -1708,15 +1714,17 @@ mod tests { fn test_dicts_no_pretty() { test_obj_no_pretty!(|obj| obj.dict(), b"<<>>"); test_obj_no_pretty!( - |obj| obj - .dict() - .pair(Name(b"Key1"), 12) - .pair(Name(b"Key2"), Name(b"Hi")) - .pair(Name(b"Key3"), false) - .pair(Name(b"Key4"), TextStr("A string")) - .pair(Name(b"Key5"), Null) - .pair(Name(b"Key6"), 23.40), - b"<>" + |obj| { + let mut dict = obj.dict(); + dict.pair(Name(b"Key1"), 12) + .pair(Name(b"Key2"), Name(b"Hi")) + .pair(Name(b"Key3"), false) + .pair(Name(b"Key4"), TextStr("A string")); + dict.insert(Name(b"Key5")).array().item(23).item(Name(b"Value")); + dict.pair(Name(b"Key6"), Null) + .pair(Name(b"Key7"), 23.40) + }, + b"<>" ); } } From 1788e30bae85f21ae370017b66d97cfb5494e06d Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:22:49 +0200 Subject: [PATCH 19/36] Small fixes --- src/lib.rs | 2 +- src/object.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index dd7a1b1..5d4e51a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -198,8 +198,8 @@ pub use self::object::{ Rect, Ref, Rewrite, Str, Stream, TextStr, TextStrLike, TextStrWithLang, TypedArray, TypedDict, Writer, }; - use self::writers::*; + use crate::chunk::WriteSettings; use std::fmt::{self, Debug, Formatter}; use std::io::Write; diff --git a/src/object.rs b/src/object.rs index 94727e6..1a1329d 100644 --- a/src/object.rs +++ b/src/object.rs @@ -651,7 +651,7 @@ impl<'a> Obj<'a> { /// Write a primitive object. #[inline] pub fn primitive(self, value: T) { - // Normally, we need to separate different PDF objects by a whitespace. he key to the + // Normally, we need to separate different PDF objects by a whitespace. the key to the // optimizations applied here are explained in 7.2.3 in the PDF reference: // > The delimiter characters (, ), <, >, [, ], /, and % are special. They // > delimit syntactic entities such as arrays, names, and comments. Any of these From c67837be11e3b14e499fbb47a2abbe04c2c14256 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:23:47 +0200 Subject: [PATCH 20/36] Rearrange again --- src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5d4e51a..e4fb26e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -198,12 +198,13 @@ pub use self::object::{ Rect, Ref, Rewrite, Str, Stream, TextStr, TextStrLike, TextStrWithLang, TypedArray, TypedDict, Writer, }; -use self::writers::*; -use crate::chunk::WriteSettings; 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. /// From ef4e868929a1586db023f0e25ea4aaffa3fec0de Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Mon, 13 Oct 2025 20:31:20 +0200 Subject: [PATCH 21/36] Fix tests --- src/content.rs | 10 +++++++--- src/lib.rs | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/content.rs b/src/content.rs index 2e6f055..a463bf5 100644 --- a/src/content.rs +++ b/src/content.rs @@ -109,7 +109,10 @@ impl<'a> Operation<'a> { // is enabled, and only lazily add padding depending on whether it's really necessary // if not. let needs_padding = if self.write_settings.pretty { - self.buf.push(pad_byte); + if !self.buf.is_empty() { + self.buf.push(pad_byte); + } + false } else { true @@ -127,8 +130,9 @@ impl Drop for Operation<'_> { // For example, in case we previously wrote a BT operator and then a [] operand in the // 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)) + 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); } diff --git a/src/lib.rs b/src/lib.rs index e4fb26e..e3d1964 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -202,6 +202,7 @@ pub use self::object::{ use std::fmt::{self, Debug, Formatter}; use std::io::Write; use std::ops::{Deref, DerefMut}; + use crate::chunk::WriteSettings; use self::writers::*; From ffda4cb28c69d29cddfb6fea37d650b2d4e94dc7 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Tue, 28 Oct 2025 22:50:32 +0100 Subject: [PATCH 22/36] Update src/chunk.rs Co-authored-by: Laurenz --- src/chunk.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/chunk.rs b/src/chunk.rs index f9d1a31..9bfb217 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -53,7 +53,6 @@ impl Chunk { pub fn new_with(write_settings: WriteSettings) -> Self { let mut chunk = Self::new(); chunk.write_settings = write_settings; - chunk } From 4536d3b0020bd1cb293c6708c3df7d57dddc5725 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Wed, 29 Oct 2025 10:08:02 +0100 Subject: [PATCH 23/36] Rename `new_with` to `with_settings` --- src/chunk.rs | 2 +- src/content.rs | 6 +++--- src/lib.rs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index f9d1a31..f1b19f5 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -50,7 +50,7 @@ impl Chunk { } /// Create a new chunk with the given write settings. - pub fn new_with(write_settings: WriteSettings) -> Self { + pub fn with_settings(write_settings: WriteSettings) -> Self { let mut chunk = Self::new(); chunk.write_settings = write_settings; diff --git a/src/content.rs b/src/content.rs index a463bf5..c20373b 100644 --- a/src/content.rs +++ b/src/content.rs @@ -19,7 +19,7 @@ impl Content { } /// Create a new content stream with the given write settings. - pub fn new_with(write_settings: WriteSettings) -> Self { + pub fn with_settings(write_settings: WriteSettings) -> Self { let mut content = Self::new(); content.write_settings = write_settings; @@ -1751,7 +1751,7 @@ mod tests { #[test] fn test_content_array_no_pretty() { - let mut content = Content::new_with(WriteSettings { pretty: false }); + let mut content = Content::with_settings(WriteSettings { pretty: false }); content.set_font(Name(b"F1"), 12.0); content.set_font(Name(b"F2"), 15.0); @@ -1775,7 +1775,7 @@ mod tests { #[test] fn test_content_dict_no_pretty() { - let mut content = Content::new_with(WriteSettings { pretty: false }); + let mut content = Content::with_settings(WriteSettings { pretty: false }); let mut mc = content.begin_marked_content_with_properties(Name(b"Test")); let mut properties = mc.properties(); diff --git a/src/lib.rs b/src/lib.rs index e3d1964..ed9b01e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -230,7 +230,7 @@ impl Pdf { } /// Create a new PDF with the given write settings. - pub fn new_with(write_settings: WriteSettings) -> Self { + pub fn with_settings(write_settings: WriteSettings) -> Self { let mut pdf = Self::new(); pdf.write_settings = write_settings; @@ -426,7 +426,7 @@ mod tests { where F: FnOnce(&mut Pdf), { - let mut w = Pdf::new_with(write_settings); + let mut w = Pdf::with_settings(write_settings); let start = w.len(); f(&mut w); let end = w.len(); From f48faf92e3de17f4db3292ffbec690a2531c1a72 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Wed, 29 Oct 2025 10:09:37 +0100 Subject: [PATCH 24/36] Rename `WriteSettings` to just `Settings` --- src/chunk.rs | 14 +++++++------- src/content.rs | 32 ++++++++++++++------------------ src/lib.rs | 20 ++++++++++---------- src/macros.rs | 4 ++-- src/object.rs | 26 +++++++++++--------------- src/renumber.rs | 2 +- 6 files changed, 45 insertions(+), 53 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index f1b19f5..39147a4 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -2,7 +2,7 @@ use super::*; /// Settings that should be applied while writing a PDF file. #[derive(Debug, Clone, Copy)] -pub struct WriteSettings { +pub struct Settings { /// Whether to enable pretty-writing. In this case, `pdf-writer` will serialize PDFs in such /// 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 @@ -10,7 +10,7 @@ pub struct WriteSettings { pub pretty: bool, } -impl Default for WriteSettings { +impl Default for Settings { fn default() -> Self { Self { pretty: true } } @@ -30,7 +30,7 @@ impl Default for WriteSettings { pub struct Chunk { pub(crate) buf: Buf, pub(crate) offsets: Vec<(Ref, usize)>, - pub(crate) write_settings: WriteSettings, + pub(crate) settings: Settings, } impl Chunk { @@ -45,14 +45,14 @@ impl Chunk { Self { buf: Buf::with_capacity(capacity), offsets: vec![], - write_settings: Default::default(), + settings: Default::default(), } } /// Create a new chunk with the given write settings. - pub fn with_settings(write_settings: WriteSettings) -> Self { + pub fn with_settings(settings: Settings) -> Self { let mut chunk = Self::new(); - chunk.write_settings = write_settings; + chunk.settings = settings; chunk } @@ -182,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, self.write_settings) + Obj::indirect(&mut self.buf, id, self.settings) } /// Start writing an indirectly referenceable stream. diff --git a/src/content.rs b/src/content.rs index c20373b..9163c7c 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1,11 +1,11 @@ use super::*; -use crate::chunk::WriteSettings; +use crate::chunk::Settings; use crate::object::{is_delimiter_character, TextStrLike}; /// A builder for a content stream. pub struct Content { buf: Buf, - write_settings: WriteSettings, + settings: Settings, q_depth: usize, } @@ -19,9 +19,9 @@ impl Content { } /// Create a new content stream with the given write settings. - pub fn with_settings(write_settings: WriteSettings) -> Self { + pub fn with_settings(settings: Settings) -> Self { let mut content = Self::new(); - content.write_settings = write_settings; + content.settings = settings; content } @@ -31,14 +31,14 @@ impl Content { Self { buf: Buf::with_capacity(capacity), q_depth: 0, - write_settings: Default::default(), + 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, self.write_settings) + Operation::start(&mut self.buf, operator, self.settings) } /// Return the buffer of the content stream. @@ -65,17 +65,13 @@ pub struct Operation<'a> { buf: &'a mut Buf, op: &'a str, first: bool, - write_settings: WriteSettings, + settings: Settings, } impl<'a> Operation<'a> { #[inline] - pub(crate) fn start( - buf: &'a mut Buf, - op: &'a str, - write_settings: WriteSettings, - ) -> Self { - Self { buf, op, first: true, write_settings } + pub(crate) fn start(buf: &'a mut Buf, op: &'a str, settings: Settings) -> Self { + Self { buf, op, first: true, settings } } /// Write a primitive operand. @@ -108,7 +104,7 @@ impl<'a> Operation<'a> { // 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 { + let needs_padding = if self.settings.pretty { if !self.buf.is_empty() { self.buf.push(pad_byte); } @@ -119,7 +115,7 @@ impl<'a> Operation<'a> { }; self.first = false; - Obj::direct(self.buf, 0, self.write_settings, needs_padding) + Obj::direct(self.buf, 0, self.settings, needs_padding) } } @@ -130,7 +126,7 @@ impl Drop for Operation<'_> { // For example, in case we previously wrote a BT operator and then a [] operand in the // next operation, we don't need to pad them. - if (self.write_settings.pretty + if (self.settings.pretty || self.buf.last().is_some_and(|b| !is_delimiter_character(*b))) && !self.buf.is_empty() { @@ -1751,7 +1747,7 @@ mod tests { #[test] fn test_content_array_no_pretty() { - let mut content = Content::with_settings(WriteSettings { pretty: false }); + let mut content = Content::with_settings(Settings { pretty: false }); content.set_font(Name(b"F1"), 12.0); content.set_font(Name(b"F2"), 15.0); @@ -1775,7 +1771,7 @@ mod tests { #[test] fn test_content_dict_no_pretty() { - let mut content = Content::with_settings(WriteSettings { pretty: false }); + let mut content = Content::with_settings(Settings { pretty: false }); let mut mc = content.begin_marked_content_with_properties(Name(b"Test")); let mut properties = mc.properties(); diff --git a/src/lib.rs b/src/lib.rs index ed9b01e..dfc900a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -203,7 +203,7 @@ use std::fmt::{self, Debug, Formatter}; use std::io::Write; use std::ops::{Deref, DerefMut}; -use crate::chunk::WriteSettings; +use crate::chunk::Settings; use self::writers::*; @@ -230,9 +230,9 @@ impl Pdf { } /// Create a new PDF with the given write settings. - pub fn with_settings(write_settings: WriteSettings) -> Self { + pub fn with_settings(settings: Settings) -> Self { let mut pdf = Self::new(); - pdf.write_settings = write_settings; + pdf.settings = settings; pdf } @@ -308,7 +308,7 @@ impl Pdf { /// /// Panics if any indirect reference id was used twice. pub fn finish(self) -> Vec { - let Chunk { mut buf, mut offsets, write_settings } = self.chunk; + let Chunk { mut buf, mut offsets, settings } = self.chunk; offsets.sort(); @@ -356,7 +356,7 @@ impl Pdf { // Write the trailer dictionary. buf.extend(b"trailer\n"); - let mut trailer = Obj::direct(&mut buf, 0, write_settings, false).dict(); + let mut trailer = Obj::direct(&mut buf, 0, settings, false).dict(); trailer.pair(Name(b"Size"), xref_len); if let Some(catalog_id) = self.catalog_id { @@ -422,11 +422,11 @@ mod tests { } /// Return the slice of bytes written during the execution of `f`. - pub fn slice(f: F, write_settings: WriteSettings) -> Vec + pub fn slice(f: F, settings: Settings) -> Vec where F: FnOnce(&mut Pdf), { - let mut w = Pdf::with_settings(write_settings); + let mut w = Pdf::with_settings(settings); let start = w.len(); f(&mut w); let end = w.len(); @@ -435,12 +435,12 @@ mod tests { } /// Return the slice of bytes written for an object. - pub fn slice_obj(f: F, write_settings: WriteSettings) -> Vec + pub fn slice_obj(f: F, settings: Settings) -> Vec where F: FnOnce(Obj<'_>), { - let buf = slice(|w| f(w.indirect(Ref::new(1))), write_settings); - if write_settings.pretty { + let buf = slice(|w| f(w.indirect(Ref::new(1))), settings); + if settings.pretty { buf[8..buf.len() - 9].to_vec() } else { buf[8..buf.len() - 8].to_vec() diff --git a/src/macros.rs b/src/macros.rs index cad7c82..3c0a6df 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -19,7 +19,7 @@ macro_rules! test { #[cfg(test)] macro_rules! test_obj { (|$obj:ident| $write:expr, $($tts:tt)*) => {{ - test!(crate::tests::slice_obj(|$obj| { $write; }, crate::WriteSettings::default()), $($tts)*) + test!(crate::tests::slice_obj(|$obj| { $write; }, crate::Settings::default()), $($tts)*) }} } @@ -27,7 +27,7 @@ macro_rules! test_obj { #[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)*) + test!(crate::tests::slice_obj(|$obj| { $write; }, crate::Settings { pretty: false }), $($tts)*) }} } diff --git a/src/object.rs b/src/object.rs index 1a1329d..9d6e372 100644 --- a/src/object.rs +++ b/src/object.rs @@ -4,7 +4,7 @@ use std::mem::ManuallyDrop; use std::num::NonZeroI32; use super::*; -use crate::chunk::WriteSettings; +use crate::chunk::Settings; /// A primitive PDF object. pub trait Primitive { @@ -608,7 +608,7 @@ pub struct Obj<'a> { buf: &'a mut Buf, indirect: bool, indent: u8, - write_settings: WriteSettings, + settings: Settings, needs_padding: bool, } @@ -618,32 +618,28 @@ impl<'a> Obj<'a> { pub(crate) fn direct( buf: &'a mut Buf, indent: u8, - write_settings: WriteSettings, + settings: Settings, needs_padding: bool, ) -> Self { Self { buf, indirect: false, indent, - write_settings, + settings, needs_padding, } } /// Start a new indirect object. #[inline] - pub(crate) fn indirect( - buf: &'a mut Buf, - id: Ref, - write_settings: WriteSettings, - ) -> Self { + pub(crate) fn indirect(buf: &'a mut Buf, id: Ref, settings: Settings) -> Self { buf.push_int(id.get()); buf.extend(b" 0 obj\n"); Self { buf, indirect: true, indent: 0, - write_settings, + settings, needs_padding: false, } } @@ -671,7 +667,7 @@ impl<'a> Obj<'a> { if self.indirect { self.buf.extend(b"\nendobj\n"); - if self.write_settings.pretty { + if self.settings.pretty { self.buf.extend(b"\n"); } } @@ -734,7 +730,7 @@ pub struct Array<'a> { buf: &'a mut Buf, indirect: bool, indent: u8, - settings: WriteSettings, + settings: Settings, len: i32, } @@ -744,7 +740,7 @@ writer!(Array: |obj| { buf: obj.buf, indirect: obj.indirect, indent: obj.indent, - settings: obj.write_settings, + settings: obj.settings, len: 0, } }); @@ -896,7 +892,7 @@ pub struct Dict<'a> { buf: &'a mut Buf, indirect: bool, indent: u8, - settings: WriteSettings, + settings: Settings, len: i32, } @@ -906,7 +902,7 @@ writer!(Dict: |obj| { buf: obj.buf, indirect: obj.indirect, indent: obj.indent.saturating_add(2), - settings: obj.write_settings, + settings: obj.settings, len: 0, } }); diff --git a/src/renumber.rs b/src/renumber.rs index 2df1bb1..cf81444 100644 --- a/src/renumber.rs +++ b/src/renumber.rs @@ -20,7 +20,7 @@ pub fn renumber(source: &Chunk, target: &mut Chunk, mapping: &mut dyn FnMut(Ref) patch_object(slice, &mut target.buf, mapping); target.buf.extend(b"\nendobj\n"); - if target.write_settings.pretty { + if target.settings.pretty { target.buf.extend(b"\n"); } } From 71367e784172d4dff0bc4408f7e0667c597021bd Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Wed, 29 Oct 2025 10:12:30 +0100 Subject: [PATCH 25/36] Wrap at 80 --- src/chunk.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index 39147a4..1b52d2f 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -3,10 +3,12 @@ use super::*; /// Settings that should be applied while writing a PDF file. #[derive(Debug, Clone, Copy)] pub struct Settings { - /// Whether to enable pretty-writing. In this case, `pdf-writer` will serialize PDFs in such - /// 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. + /// Whether to enable pretty-writing. In this case, `pdf-writer` will + /// serialize PDFs in such 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, } From 8c01c520b2f833882833e4284908d5f0c3d6e377 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Wed, 29 Oct 2025 10:12:53 +0100 Subject: [PATCH 26/36] Update src/content.rs Co-authored-by: Laurenz --- src/content.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/content.rs b/src/content.rs index a463bf5..5e74fac 100644 --- a/src/content.rs +++ b/src/content.rs @@ -128,7 +128,7 @@ impl Drop for Operation<'_> { fn drop(&mut self) { 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 + // For example, in case we previously wrote a BT operator and then a `[]` operand in the // 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))) From fcd50987b0ab3129f4df4771b51d382960f28754 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Wed, 29 Oct 2025 10:14:38 +0100 Subject: [PATCH 27/36] Fix some comments --- src/chunk.rs | 2 +- src/content.rs | 2 +- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index e15e7c5..dbd4b99 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -51,7 +51,7 @@ impl Chunk { } } - /// Create a new chunk with the given write settings. + /// Create a new chunk with the given settings. pub fn with_settings(settings: Settings) -> Self { let mut chunk = Self::new(); chunk.settings = settings; diff --git a/src/content.rs b/src/content.rs index c8b5f22..564e5cd 100644 --- a/src/content.rs +++ b/src/content.rs @@ -18,7 +18,7 @@ impl Content { Self::with_capacity(1024) } - /// Create a new content stream with the given write settings. + /// Create a new content stream with the given settings. pub fn with_settings(settings: Settings) -> Self { let mut content = Self::new(); content.settings = settings; diff --git a/src/lib.rs b/src/lib.rs index dfc900a..324557a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -229,7 +229,7 @@ impl Pdf { Self::with_capacity(8 * 1024) } - /// Create a new PDF with the given write settings. + /// Create a new PDF with the given settings. pub fn with_settings(settings: Settings) -> Self { let mut pdf = Self::new(); pdf.settings = settings; From dce89f2a194b06a9416028c4efa0f53120b9bce3 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Wed, 29 Oct 2025 10:15:21 +0100 Subject: [PATCH 28/36] Remove newlines --- src/content.rs | 1 - src/lib.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/content.rs b/src/content.rs index 564e5cd..e2a0cc6 100644 --- a/src/content.rs +++ b/src/content.rs @@ -22,7 +22,6 @@ impl Content { pub fn with_settings(settings: Settings) -> Self { let mut content = Self::new(); content.settings = settings; - content } diff --git a/src/lib.rs b/src/lib.rs index 324557a..3992708 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -233,7 +233,6 @@ impl Pdf { pub fn with_settings(settings: Settings) -> Self { let mut pdf = Self::new(); pdf.settings = settings; - pdf } From b97add381345cf2641948840f9c2c534f4188acc Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Wed, 29 Oct 2025 10:19:17 +0100 Subject: [PATCH 29/36] Fix build --- src/annotations.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/annotations.rs b/src/annotations.rs index 50d25ec..cfc1d0a 100644 --- a/src/annotations.rs +++ b/src/annotations.rs @@ -801,7 +801,7 @@ mod tests { 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() + Settings::default() ), b"1 0 obj", b"<<", From b2ffbbbf127c2054ddfc64851e72792f44e458f3 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Wed, 29 Oct 2025 10:34:07 +0100 Subject: [PATCH 30/36] Reformat --- src/chunk.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index dbd4b99..0a74915 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -3,11 +3,11 @@ use super::*; /// Settings that should be applied while writing a PDF file. #[derive(Debug, Clone, Copy)] pub struct Settings { - /// Whether to enable pretty-writing. In this case, `pdf-writer` will - /// serialize PDFs in such a way that they are easier to read by humans by + /// Whether to enable pretty-writing. In this case, `pdf-writer` will + /// serialize PDFs in such 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 + /// 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, } From fccf5c0f15a67a550083bffee2460415413265f8 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:36:07 +0100 Subject: [PATCH 31/36] Document default value --- src/chunk.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/chunk.rs b/src/chunk.rs index 0a74915..68d0569 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -9,6 +9,8 @@ pub struct Settings { /// 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. + /// + /// _Default value_: `true`. pub pretty: bool, } From 6b7a03422275e6106f1ab927f9f869d4813ac073 Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl <47084093+LaurenzV@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:39:16 +0100 Subject: [PATCH 32/36] Reformat --- src/chunk.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chunk.rs b/src/chunk.rs index 68d0569..d6a5821 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -9,7 +9,7 @@ pub struct Settings { /// 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. - /// + /// /// _Default value_: `true`. pub pretty: bool, } From 1fd6096dee2bd4f2752b413267ba4423be64d98a Mon Sep 17 00:00:00 2001 From: Laurenz Stampfl Date: Mon, 3 Nov 2025 15:48:40 +0100 Subject: [PATCH 33/36] Turn `Primitive` into a sealed trait --- src/object.rs | 67 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/src/object.rs b/src/object.rs index 9d6e372..d9c2456 100644 --- a/src/object.rs +++ b/src/object.rs @@ -5,17 +5,24 @@ use std::num::NonZeroI32; use super::*; use crate::chunk::Settings; +use crate::object::sealed::Sealed; -/// A primitive PDF object. -pub trait Primitive { - /// Whether the primitive object starts with one of the PDF delimiter characters. - const STARTS_WITH_DELIMITER: bool; +mod sealed { + use crate::Buf; + + pub trait Sealed { + /// Whether the primitive object starts with one of the PDF delimiter characters. + const STARTS_WITH_DELIMITER: bool; - /// Write the object into a buffer. - fn write(self, buf: &mut Buf); + /// Write the object into a buffer. + fn write(self, buf: &mut Buf); + } } -impl Primitive for &T +/// A primitive PDF object. +pub trait Primitive: Sealed {} + +impl Sealed for &T where T: Copy, { @@ -27,7 +34,9 @@ where } } -impl Primitive for bool { +impl Primitive for &T where T: Copy {} + +impl Sealed for bool { const STARTS_WITH_DELIMITER: bool = false; #[inline] @@ -40,7 +49,9 @@ impl Primitive for bool { } } -impl Primitive for i32 { +impl Primitive for bool {} + +impl Sealed for i32 { const STARTS_WITH_DELIMITER: bool = false; #[inline] @@ -49,7 +60,9 @@ impl Primitive for i32 { } } -impl Primitive for f32 { +impl Primitive for i32 {} + +impl Sealed for f32 { const STARTS_WITH_DELIMITER: bool = false; #[inline] @@ -58,6 +71,8 @@ impl Primitive for f32 { } } +impl Primitive for f32 {} + /// A string object (any byte sequence). /// /// This is written as `(Thing)`. @@ -80,7 +95,7 @@ impl Str<'_> { } } -impl Primitive for Str<'_> { +impl Sealed for Str<'_> { const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { @@ -134,6 +149,8 @@ impl Primitive for Str<'_> { } } +impl Primitive for Str<'_> {} + /// A unicode text string object. /// /// This is written as a [`Str`] containing either bare ASCII (if possible) or a @@ -146,7 +163,7 @@ impl Primitive for Str<'_> { #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct TextStr<'a>(pub &'a str); -impl Primitive for TextStr<'_> { +impl Sealed for TextStr<'_> { const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { @@ -166,6 +183,8 @@ impl Primitive for TextStr<'_> { } } +impl Primitive for TextStr<'_> {} + /// An identifier for the natural language in a section of a /// [`TextStrWithLang`]. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] @@ -233,7 +252,7 @@ impl LanguageIdentifier { #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct TextStrWithLang<'a, 'b>(pub &'b [(LanguageIdentifier, &'a str)]); -impl<'a, 'b> Primitive for TextStrWithLang<'a, 'b> { +impl<'a, 'b> Sealed for TextStrWithLang<'a, 'b> { const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { @@ -268,6 +287,8 @@ impl<'a, 'b> Primitive for TextStrWithLang<'a, 'b> { } } +impl Primitive for TextStrWithLang<'_, '_> {} + fn write_utf16be_text_str_header(buf: &mut Buf) { buf.push(b'<'); buf.push_hex(254); @@ -311,7 +332,7 @@ impl<'a, 'b> TextStrLike for TextStrWithLang<'a, 'b> {} #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Name<'a>(pub &'a [u8]); -impl Primitive for Name<'_> { +impl Sealed for Name<'_> { const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { @@ -333,6 +354,8 @@ impl Primitive for Name<'_> { } } +impl Primitive for Name<'_> {} + /// Regular characters are a PDF concept. fn is_regular_character(byte: u8) -> bool { !matches!( @@ -365,7 +388,7 @@ pub(crate) fn is_delimiter_character(byte: u8) -> bool { #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Null; -impl Primitive for Null { +impl Sealed for Null { const STARTS_WITH_DELIMITER: bool = false; #[inline] @@ -374,6 +397,8 @@ impl Primitive for Null { } } +impl Primitive for Null {} + /// A reference to an indirect object. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Ref(NonZeroI32); @@ -416,7 +441,7 @@ impl Ref { } } -impl Primitive for Ref { +impl Sealed for Ref { const STARTS_WITH_DELIMITER: bool = false; #[inline] @@ -426,6 +451,8 @@ impl Primitive for Ref { } } +impl Primitive for Ref {} + /// A rectangle, specified by two opposite corners. #[derive(Debug, Copy, Clone, PartialEq)] pub struct Rect { @@ -454,7 +481,7 @@ impl Rect { } } -impl Primitive for Rect { +impl Sealed for Rect { const STARTS_WITH_DELIMITER: bool = true; #[inline] @@ -473,6 +500,8 @@ impl Primitive for Rect { } } +impl Primitive for Rect {} + /// A date, written as a text string. /// /// A field is only respected if all superior fields are supplied. For example, @@ -571,7 +600,7 @@ impl Date { } } -impl Primitive for Date { +impl Sealed for Date { const STARTS_WITH_DELIMITER: bool = true; fn write(self, buf: &mut Buf) { @@ -602,6 +631,8 @@ impl Primitive for Date { } } +impl Primitive for Date {} + /// Writer for an arbitrary object. #[must_use = "not consuming this leaves the writer in an inconsistent state"] pub struct Obj<'a> { From c701b91bc691e3bdf7c87991a351b796e27f922e Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 8 Nov 2025 12:47:18 +0100 Subject: [PATCH 34/36] Rewrap some comments --- src/content.rs | 15 ++++++++------- src/object.rs | 23 ++++++++++++++--------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/content.rs b/src/content.rs index e2a0cc6..2f939fc 100644 --- a/src/content.rs +++ b/src/content.rs @@ -96,13 +96,14 @@ impl<'a> Operation<'a> { /// Start writing an arbitrary object operand. #[inline] pub fn obj(&mut self) -> Obj<'_> { - // 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. + // 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. + // 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.settings.pretty { if !self.buf.is_empty() { self.buf.push(pad_byte); @@ -123,8 +124,8 @@ impl Drop for Operation<'_> { fn drop(&mut self) { 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 - // next operation, we don't need to pad them. + // For example, in case we previously wrote a BT operator and then a + // `[]` operand in the next operation, we don't need to pad them. if (self.settings.pretty || self.buf.last().is_some_and(|b| !is_delimiter_character(*b))) && !self.buf.is_empty() diff --git a/src/object.rs b/src/object.rs index d9c2456..12751aa 100644 --- a/src/object.rs +++ b/src/object.rs @@ -678,13 +678,18 @@ impl<'a> Obj<'a> { /// Write a primitive object. #[inline] pub fn primitive(self, value: T) { - // Normally, we need to separate different PDF objects by a whitespace. the key to the - // optimizations applied here are explained in 7.2.3 in the PDF reference: - // > The delimiter characters (, ), <, >, [, ], /, and % are special. They - // > delimit syntactic entities such as arrays, names, and comments. Any of these - // > delimiters terminates the entity preceding it and is not included in the entity. - // Therefore, if either the previous byte is a delimiter character or the current token - // starts with one, we don't need to add a whitespace for padding. + // Usually, we need to separate different PDF objects by a whitespace. + // The key to the optimizations applied here are explained in 7.2.3 in + // the PDF reference: + // + // > The delimiter characters (, ), <, >, [, ], /, and % are special. + // > They delimit syntactic entities such as arrays, names, and + // > comments. Any of these delimiters terminates the entity preceding + // > it and is not included in the entity. + // + // Therefore, if either the previous byte is a delimiter character or + // the current token starts with one, we don't need to add a whitespace + // for padding. let ends_with_delimiter = self.buf.last().copied().is_some_and(is_delimiter_character); @@ -704,8 +709,8 @@ impl<'a> Obj<'a> { } } - // Note: Arrays and dictionaries always start with a delimiter, so we don't need to do any case - // distinction, unlike in `primitive`. + // Note: Arrays and dictionaries always start with a delimiter, so we don't + // need to do any case distinction, unlike in `primitive`. /// Start writing an array. #[inline] From 8e9ffe9429aba47b73905924074f7b864ef44470 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 8 Nov 2025 12:52:47 +0100 Subject: [PATCH 35/36] Reexport `Settings` --- src/lib.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 3992708..6e98983 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,7 +191,7 @@ pub mod types { } pub use self::buf::{Buf, Limits}; -pub use self::chunk::Chunk; +pub use self::chunk::{Chunk, Settings}; pub use self::content::Content; pub use self::object::{ Array, Date, Dict, Filter, Finish, LanguageIdentifier, Name, Null, Obj, Primitive, @@ -203,8 +203,6 @@ use std::fmt::{self, Debug, Formatter}; use std::io::Write; use std::ops::{Deref, DerefMut}; -use crate::chunk::Settings; - use self::writers::*; /// A builder for a PDF file. From 58ffa5276ace9d99baf5abdb577f9ffd1d6bf149 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Sat, 8 Nov 2025 12:53:18 +0100 Subject: [PATCH 36/36] Tidy up constructors Also add `with_settings_and_capacity` after all. It does not hurt and the with_settings + reserve route can incur an extra allocation. --- src/chunk.rs | 29 ++++++++++++++++++----------- src/lib.rs | 23 +++++++++++++++-------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/src/chunk.rs b/src/chunk.rs index d6a5821..89b28f3 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -38,28 +38,35 @@ pub struct Chunk { } impl Chunk { - /// Create a new chunk with the default capacity (currently 1 KB). + /// Create a new chunk with the default settings and buffer capacity + /// (currently 1 KB). #[allow(clippy::new_without_default)] pub fn new() -> Self { - Self::with_capacity(1024) + Self::with_settings(Settings::default()) } - /// Create a new chunk with the specified initial capacity. + /// Create a new chunk with the given settings and the default buffer + /// capacity (currently 1 KB). + pub fn with_settings(settings: Settings) -> Self { + Self::with_settings_and_capacity(settings, 1204) + } + + /// Create a new chunk with the default settings and the specified initial + /// capacity. pub fn with_capacity(capacity: usize) -> Self { + Self::with_settings_and_capacity(Settings::default(), capacity) + } + + /// Create a new chunk with the given settings and the specified initial + /// buffer capacity. + pub fn with_settings_and_capacity(settings: Settings, capacity: usize) -> Self { Self { buf: Buf::with_capacity(capacity), offsets: vec![], - settings: Default::default(), + settings, } } - /// Create a new chunk with the given settings. - pub fn with_settings(settings: Settings) -> Self { - let mut chunk = Self::new(); - chunk.settings = settings; - chunk - } - /// The number of bytes that were written so far. #[inline] #[allow(clippy::len_without_is_empty)] diff --git a/src/lib.rs b/src/lib.rs index 6e98983..4c83948 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -221,22 +221,29 @@ pub struct Pdf { } impl Pdf { - /// Create a new PDF with the default buffer capacity (currently 8 KB). + /// Create a new PDF with the default settings and buffer capacity + /// (currently 8 KB). #[allow(clippy::new_without_default)] pub fn new() -> Self { - Self::with_capacity(8 * 1024) + Self::with_settings(Settings::default()) } - /// Create a new PDF with the given settings. + /// Create a new PDF with the given settings and the default buffer capacity + /// (currently 8 KB). pub fn with_settings(settings: Settings) -> Self { - let mut pdf = Self::new(); - pdf.settings = settings; - pdf + Self::with_settings_and_capacity(settings, 8 * 1024) } - /// Create a new PDF with the specified initial buffer capacity. + /// Create a new PDF with the default settings and the specified initial + /// buffer capacity. pub fn with_capacity(capacity: usize) -> Self { - let mut chunk = Chunk::with_capacity(capacity); + Self::with_settings_and_capacity(Settings::default(), capacity) + } + + /// Create a new PDF with the given settings and the specified initial + /// buffer capacity. + pub fn with_settings_and_capacity(settings: Settings, capacity: usize) -> Self { + let mut chunk = Chunk::with_settings_and_capacity(settings, capacity); chunk.buf.extend(b"%PDF-1.7\n%\x80\x80\x80\x80\n\n"); Self { chunk,