diff --git a/src/annotations.rs b/src/annotations.rs index ab92417..cfc1d0a 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)); + }, + Settings::default() + ), b"1 0 obj", b"<<", b" /Type /Annot", diff --git a/src/chunk.rs b/src/chunk.rs index b5551d4..89b28f3 100644 --- a/src/chunk.rs +++ b/src/chunk.rs @@ -1,5 +1,25 @@ 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. + /// + /// _Default value_: `true`. + pub pretty: bool, +} + +impl Default for Settings { + 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 @@ -14,18 +34,37 @@ use super::*; pub struct Chunk { pub(crate) buf: Buf, pub(crate) offsets: Vec<(Ref, usize)>, + pub(crate) settings: Settings, } 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 { buf: Buf::with_capacity(capacity), offsets: vec![] } + 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, + } } /// The number of bytes that were written so far. @@ -35,6 +74,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() @@ -148,7 +192,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..2f939fc 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1,9 +1,11 @@ use super::*; -use crate::object::TextStrLike; +use crate::chunk::Settings; +use crate::object::{is_delimiter_character, TextStrLike}; /// A builder for a content stream. pub struct Content { buf: Buf, + settings: Settings, q_depth: usize, } @@ -16,15 +18,26 @@ impl Content { Self::with_capacity(1024) } + /// Create a new content stream with the given settings. + pub fn with_settings(settings: Settings) -> 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 +64,13 @@ pub struct Operation<'a> { buf: &'a mut Buf, op: &'a str, first: bool, + settings: Settings, } 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: Settings) -> Self { + Self { buf, op, first: true, settings } } /// Write a primitive operand. @@ -79,25 +93,47 @@ 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.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.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 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() + { + self.buf.push(pad_byte); } + self.buf.extend(self.op.as_bytes()); - self.buf.push(b'\n'); } } @@ -1708,4 +1744,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::with_settings(Settings { 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::with_settings(Settings { 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/lib.rs b/src/lib.rs index 33e8193..4c83948 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, @@ -221,15 +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 specified initial buffer capacity. + /// Create a new PDF with the given settings and the default buffer capacity + /// (currently 8 KB). + pub fn with_settings(settings: Settings) -> Self { + Self::with_settings_and_capacity(settings, 8 * 1024) + } + + /// 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, @@ -298,7 +312,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 +360,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, false).dict(); trailer.pair(Name(b"Size"), xref_len); if let Some(catalog_id) = self.catalog_id { @@ -412,11 +426,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, settings: Settings) -> Vec where F: FnOnce(&mut Pdf), { - let mut w = Pdf::new(); + let mut w = Pdf::with_settings(settings); let start = w.len(); f(&mut w); let end = w.len(); @@ -425,12 +439,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, settings: Settings) -> 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))), settings); + if 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..3c0a6df 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::Settings::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::Settings { pretty: false }), $($tts)*) }} } diff --git a/src/object.rs b/src/object.rs index 99e8ca6..12751aa 100644 --- a/src/object.rs +++ b/src/object.rs @@ -4,24 +4,41 @@ use std::mem::ManuallyDrop; use std::num::NonZeroI32; use super::*; +use crate::chunk::Settings; +use crate::object::sealed::Sealed; -/// A primitive PDF object. -pub trait Primitive { - /// Write the object into a buffer. - fn write(self, buf: &mut Buf); +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); + } } -impl Primitive for &T +/// A primitive PDF object. +pub trait Primitive: Sealed {} + +impl Sealed for &T where T: Copy, { + const STARTS_WITH_DELIMITER: bool = T::STARTS_WITH_DELIMITER; + #[inline] fn write(self, buf: &mut Buf) { (*self).write(buf); } } -impl Primitive for bool { +impl Primitive for &T where T: Copy {} + +impl Sealed for bool { + const STARTS_WITH_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { if self { @@ -32,20 +49,30 @@ impl Primitive for bool { } } -impl Primitive for i32 { +impl Primitive for bool {} + +impl Sealed for i32 { + const STARTS_WITH_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { buf.push_int(self); } } -impl Primitive for f32 { +impl Primitive for i32 {} + +impl Sealed for f32 { + const STARTS_WITH_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { buf.push_float(self); } } +impl Primitive for f32 {} + /// A string object (any byte sequence). /// /// This is written as `(Thing)`. @@ -68,7 +95,9 @@ impl Str<'_> { } } -impl Primitive for Str<'_> { +impl Sealed for Str<'_> { + const STARTS_WITH_DELIMITER: bool = true; + fn write(self, buf: &mut Buf) { buf.limits.register_str_len(self.0.len()); @@ -120,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 @@ -132,7 +163,9 @@ 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) { buf.limits.register_str_len(self.0.len()); @@ -150,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)] @@ -217,7 +252,9 @@ 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) { let mut len = 0; let mut buf_len = 6; @@ -250,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); @@ -293,7 +332,9 @@ 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) { buf.limits.register_name_len(self.0.len()); @@ -313,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!( @@ -336,17 +379,26 @@ fn is_regular_character(byte: u8) -> bool { ) } +#[inline] +pub(crate) 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; -impl Primitive for Null { +impl Sealed for Null { + const STARTS_WITH_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { buf.extend(b"null"); } } +impl Primitive for Null {} + /// A reference to an indirect object. #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct Ref(NonZeroI32); @@ -389,7 +441,9 @@ impl Ref { } } -impl Primitive for Ref { +impl Sealed for Ref { + const STARTS_WITH_DELIMITER: bool = false; + #[inline] fn write(self, buf: &mut Buf) { buf.push_int(self.0.get()); @@ -397,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 { @@ -425,7 +481,9 @@ impl Rect { } } -impl Primitive for Rect { +impl Sealed for Rect { + const STARTS_WITH_DELIMITER: bool = true; + #[inline] fn write(self, buf: &mut Buf) { buf.push(b'['); @@ -442,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, @@ -540,7 +600,9 @@ impl Date { } } -impl Primitive for Date { +impl Sealed for Date { + const STARTS_WITH_DELIMITER: bool = true; + fn write(self, buf: &mut Buf) { buf.extend(b"(D:"); @@ -569,38 +631,87 @@ 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> { buf: &'a mut Buf, indirect: bool, indent: u8, + settings: Settings, + needs_padding: bool, } 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: Settings, + needs_padding: bool, + ) -> Self { + Self { + buf, + indirect: false, + indent, + settings, + needs_padding, + } } /// 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: Settings) -> 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, + needs_padding: false, + } } /// Write a primitive object. #[inline] pub fn primitive(self, value: T) { + // 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); + + if self.needs_padding && !T::STARTS_WITH_DELIMITER && !ends_with_delimiter { + self.buf.extend(b" "); + } + 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"); + } } } + // 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> { @@ -655,6 +766,7 @@ pub struct Array<'a> { buf: &'a mut Buf, indirect: bool, indent: u8, + settings: Settings, len: i32, } @@ -664,6 +776,7 @@ writer!(Array: |obj| { buf: obj.buf, indirect: obj.indirect, indent: obj.indent, + settings: obj.settings, len: 0, } }); @@ -684,11 +797,20 @@ 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) + + Obj::direct(self.buf, self.indent, self.settings, needs_padding) } /// Write an item with a primitive value. @@ -725,7 +847,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 +928,7 @@ pub struct Dict<'a> { buf: &'a mut Buf, indirect: bool, indent: u8, + settings: Settings, len: i32, } @@ -811,6 +938,7 @@ writer!(Dict: |obj| { buf: obj.buf, indirect: obj.indirect, indent: obj.indent.saturating_add(2), + settings: obj.settings, len: 0, } }); @@ -832,16 +960,27 @@ 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' '); + // 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'); + + for _ in 0..self.indent { + self.buf.push(b' '); + } } self.buf.push_val(key); - self.buf.push(b' '); - Obj::direct(self.buf, self.indent) + let needs_padding = if self.settings.pretty { + self.buf.push(b' '); + false + } else { + true + }; + + Obj::direct(self.buf, self.indent, self.settings, needs_padding) } /// Write a pair with a primitive value. @@ -876,15 +1015,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 +1150,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"); + } } } @@ -1565,4 +1718,45 @@ 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| { + 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]" + ); + } + + #[test] + fn test_dicts_no_pretty() { + test_obj_no_pretty!(|obj| obj.dict(), b"<<>>"); + test_obj_no_pretty!( + |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"<>" + ); + } } 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"); + } } }