#![forbid(unsafe_code)]
#![deny(missing_docs)]
#![allow(clippy::wrong_self_convention)]
#[macro_use]
mod macros;
mod annotations;
mod attributes;
mod buf;
mod chunk;
mod color;
mod content;
mod files;
mod font;
mod functions;
mod object;
mod renumber;
mod structure;
mod transitions;
mod xobject;
pub mod writers {
use super::*;
pub use annotations::{Action, Annotation, BorderStyle};
pub use attributes::{
Attributes, FieldAttributes, LayoutAttributes, ListAttributes, TableAttributes,
UserProperty,
};
pub use color::{
ColorSpace, DeviceN, DeviceNAttrs, DeviceNMixingHints, DeviceNProcess,
FunctionShading, IccProfile, OutputIntent, Separation, SeparationInfo,
ShadingPattern, StreamShading, StreamShadingType, TilingPattern,
};
pub use content::{
Artifact, ExtGraphicsState, MarkContent, Operation, PositionedItems,
PropertyList, Resources, ShowPositioned, SoftMask,
};
pub use files::{EmbeddedFile, EmbeddingParams, FileSpec};
pub use font::{
CidFont, Cmap, Differences, Encoding, FontDescriptor, Type0Font, Type1Font,
Type3Font, Widths,
};
pub use functions::{
ExponentialFunction, PostScriptFunction, SampledFunction, StitchingFunction,
};
pub use object::{NameTree, NameTreeEntries, NumberTree, NumberTreeEntries};
pub use structure::{
Catalog, ClassMap, Destination, DeveloperExtension, DocumentInfo, MarkInfo,
MarkedRef, Metadata, Names, ObjectRef, Outline, OutlineItem, Page, PageLabel,
Pages, RoleMap, StructChildren, StructElement, StructTreeRoot, ViewerPreferences,
};
pub use transitions::Transition;
pub use xobject::{FormXObject, Group, ImageXObject, Reference};
}
pub mod types {
use super::*;
pub use annotations::{
ActionType, AnnotationFlags, AnnotationIcon, AnnotationType, BorderType,
HighlightEffect,
};
pub use attributes::{
AttributeOwner, BlockAlign, FieldRole, FieldState, InlineAlign,
LayoutBorderStyle, ListNumbering, Placement, RubyAlign, RubyPosition,
TableHeaderScope, TextAlign, TextDecorationType, WritingMode,
};
pub use color::{
DeviceNSubtype, FunctionShadingType, OutputIntentSubtype, PaintType, TilingType,
};
pub use content::{
ArtifactAttachment, ArtifactSubtype, ArtifactType, BlendMode, ColorSpaceOperand,
LineCapStyle, LineJoinStyle, MaskType, OverprintMode, ProcSet, RenderingIntent,
TextRenderingMode,
};
pub use font::UnicodeCmap;
pub use font::{CidFontType, FontFlags, FontStretch, SystemInfo};
pub use functions::{InterpolationOrder, PostScriptOp};
pub use structure::{
Direction, NumberingStyle, OutlineItemFlags, PageLayout, PageMode, StructRole,
TabOrder, TrappingStatus,
};
pub use transitions::{TransitionAngle, TransitionStyle};
pub use xobject::SMaskInData;
}
pub use self::chunk::Chunk;
pub use self::content::Content;
pub use self::object::{
Array, Date, Dict, Filter, Finish, Name, Null, Obj, Primitive, Rect, Ref, Rewrite,
Str, Stream, TextStr, TypedArray, TypedDict, Writer,
};
use std::fmt::{self, Debug, Formatter};
use std::io::Write;
use std::ops::{Deref, DerefMut};
use self::buf::BufExt;
use self::writers::*;
pub struct Pdf {
chunk: Chunk,
catalog_id: Option<Ref>,
info_id: Option<Ref>,
file_id: Option<(Vec<u8>, Vec<u8>)>,
}
impl Pdf {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
Self::with_capacity(8 * 1024)
}
pub fn with_capacity(capacity: usize) -> Self {
let mut chunk = Chunk::with_capacity(capacity);
chunk.buf.extend(b"%PDF-1.7\n%\x80\x80\x80\x80\n\n");
Self {
chunk,
catalog_id: None,
info_id: None,
file_id: None,
}
}
pub fn set_version(&mut self, major: u8, minor: u8) {
if major < 10 {
self.chunk.buf[5] = b'0' + major;
}
if minor < 10 {
self.chunk.buf[7] = b'0' + minor;
}
}
pub fn set_file_id(&mut self, id: (Vec<u8>, Vec<u8>)) {
self.file_id = Some(id);
}
pub fn catalog(&mut self, id: Ref) -> Catalog<'_> {
self.catalog_id = Some(id);
self.indirect(id).start()
}
pub fn document_info(&mut self, id: Ref) -> DocumentInfo<'_> {
self.info_id = Some(id);
self.indirect(id).start()
}
pub fn finish(self) -> Vec<u8> {
let Chunk { mut buf, mut offsets } = self.chunk;
offsets.sort();
let xref_len = 1 + offsets.last().map_or(0, |p| p.0.get());
let xref_offset = buf.len();
buf.extend(b"xref\n0 ");
buf.push_int(xref_len);
buf.push(b'\n');
if offsets.is_empty() {
write!(buf, "0000000000 65535 f\r\n").unwrap();
}
let mut written = 0;
for (i, (object_id, offset)) in offsets.iter().enumerate() {
if written > object_id.get() {
panic!("duplicate indirect reference id: {}", object_id.get());
}
let start = written;
for free_id in start..object_id.get() {
let mut next = free_id + 1;
if next == object_id.get() {
for (used_id, _) in &offsets[i..] {
if next < used_id.get() {
break;
} else {
next = used_id.get() + 1;
}
}
}
let gen = if free_id == 0 { "65535" } else { "00000" };
write!(buf, "{:010} {} f\r\n", next % xref_len, gen).unwrap();
written += 1;
}
write!(buf, "{:010} 00000 n\r\n", offset).unwrap();
written += 1;
}
buf.extend(b"trailer\n");
let mut trailer = Obj::direct(&mut buf, 0).dict();
trailer.pair(Name(b"Size"), xref_len);
if let Some(catalog_id) = self.catalog_id {
trailer.pair(Name(b"Root"), catalog_id);
}
if let Some(info_id) = self.info_id {
trailer.pair(Name(b"Info"), info_id);
}
if let Some(file_id) = self.file_id {
let mut ids = trailer.insert(Name(b"ID")).array();
ids.item(Str(&file_id.0));
ids.item(Str(&file_id.1));
}
trailer.finish();
buf.extend(b"\nstartxref\n");
write!(buf, "{}", xref_offset).unwrap();
buf.extend(b"\n%%EOF");
buf
}
}
impl Debug for Pdf {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.pad("Pdf(..)")
}
}
impl Deref for Pdf {
type Target = Chunk;
fn deref(&self) -> &Self::Target {
&self.chunk
}
}
impl DerefMut for Pdf {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.chunk
}
}
#[cfg(test)]
mod tests {
use super::*;
#[allow(unused)]
pub fn print_chunk(chunk: &Chunk) {
println!("========== Chunk ==========");
for &(id, offset) in &chunk.offsets {
println!("[{}]: {}", id.get(), offset);
}
println!("---------------------------");
print!("{}", String::from_utf8_lossy(&chunk.buf));
println!("===========================");
}
pub fn slice<F>(f: F) -> Vec<u8>
where
F: FnOnce(&mut Pdf),
{
let mut w = Pdf::new();
let start = w.len();
f(&mut w);
let end = w.len();
let buf = w.finish();
buf[start..end].to_vec()
}
pub fn slice_obj<F>(f: F) -> Vec<u8>
where
F: FnOnce(Obj<'_>),
{
let buf = slice(|w| f(w.indirect(Ref::new(1))));
buf[8..buf.len() - 9].to_vec()
}
#[test]
fn test_minimal() {
let w = Pdf::new();
test!(
w.finish(),
b"%PDF-1.7\n%\x80\x80\x80\x80\n",
b"xref\n0 1\n0000000000 65535 f\r",
b"trailer\n<<\n /Size 1\n>>",
b"startxref\n16\n%%EOF",
);
}
#[test]
fn test_xref_free_list_short() {
let mut w = Pdf::new();
w.indirect(Ref::new(1)).primitive(1);
w.indirect(Ref::new(2)).primitive(2);
test!(
w.finish(),
b"%PDF-1.7\n%\x80\x80\x80\x80\n",
b"1 0 obj\n1\nendobj\n",
b"2 0 obj\n2\nendobj\n",
b"xref",
b"0 3",
b"0000000000 65535 f\r",
b"0000000016 00000 n\r",
b"0000000034 00000 n\r",
b"trailer",
b"<<\n /Size 3\n>>",
b"startxref\n52\n%%EOF",
)
}
#[test]
fn test_xref_free_list_long() {
let mut w = Pdf::new();
w.set_version(1, 4);
w.indirect(Ref::new(1)).primitive(1);
w.indirect(Ref::new(2)).primitive(2);
w.indirect(Ref::new(5)).primitive(5);
test!(
w.finish(),
b"%PDF-1.4\n%\x80\x80\x80\x80\n",
b"1 0 obj\n1\nendobj\n",
b"2 0 obj\n2\nendobj\n",
b"5 0 obj\n5\nendobj\n",
b"xref",
b"0 6",
b"0000000003 65535 f\r",
b"0000000016 00000 n\r",
b"0000000034 00000 n\r",
b"0000000004 00000 f\r",
b"0000000000 00000 f\r",
b"0000000052 00000 n\r",
b"trailer",
b"<<\n /Size 6\n>>",
b"startxref\n70\n%%EOF",
)
}
#[test]
#[should_panic(expected = "duplicate indirect reference id: 3")]
fn test_xref_free_list_duplicate() {
let mut w = Pdf::new();
w.indirect(Ref::new(3)).primitive(1);
w.indirect(Ref::new(5)).primitive(2);
w.indirect(Ref::new(13)).primitive(1);
w.indirect(Ref::new(3)).primitive(1);
w.indirect(Ref::new(6)).primitive(2);
w.finish();
}
}