pub mod elements;
pub mod flex;
pub mod fonts;
pub mod image;
pub mod serde_elements;
pub mod test_utils;
mod text;
pub mod utils;
use chrono::{Datelike, Timelike, Utc};
use elements::padding::Padding;
use fonts::Font;
use pdf_writer::{Content, Date, Name, Rect, Ref, TextStr};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use xmp_writer::{DateTime, LangId, Timezone, XmpWriter};
pub use crate::text::TextPiecesCache;
pub type Color = u32;
#[derive(Copy, Clone, Serialize, Deserialize)]
pub enum LineCapStyle {
Butt,
Round,
ProjectingSquare,
}
impl Into<pdf_writer::types::LineCapStyle> for LineCapStyle {
fn into(self) -> pdf_writer::types::LineCapStyle {
match self {
LineCapStyle::Butt => pdf_writer::types::LineCapStyle::ButtCap,
LineCapStyle::Round => pdf_writer::types::LineCapStyle::RoundCap,
LineCapStyle::ProjectingSquare => pdf_writer::types::LineCapStyle::ProjectingSquareCap,
}
}
}
#[derive(Copy, Clone, Serialize, Deserialize)]
pub struct LineDashPattern {
pub offset: u16,
pub dashes: [u16; 2],
}
#[derive(Copy, Clone, Serialize, Deserialize)]
pub struct LineStyle {
pub thickness: f32,
pub color: Color,
pub dash_pattern: Option<LineDashPattern>,
pub cap_style: LineCapStyle,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum LinkTarget<'a> {
Uri(&'a str),
}
pub struct Layer {
pub content: Content,
pub graphics_state_restore_required: bool,
}
pub struct Page {
pub annotations: Vec<Ref>,
pub ext_g_states: Vec<Ref>, pub x_objects: Vec<Ref>,
pub layers: Vec<Layer>,
pub size: (f32, f32),
}
impl Page {
pub fn add_ext_g_state(&mut self, resource: Ref) -> usize {
self.ext_g_states.push(resource);
self.ext_g_states.len() - 1
}
pub fn add_x_object(&mut self, resource: Ref) -> String {
self.x_objects.push(resource);
(self.x_objects.len() - 1).to_string()
}
}
#[derive(Clone)]
pub struct Metadata {
pub title: String,
pub language: String,
pub keywords: Option<String>,
pub producer: Option<String>,
pub creation_date: chrono::DateTime<Utc>,
pub identifier: String,
}
impl Metadata {
pub fn new() -> Self {
Metadata {
title: "".to_string(),
language: "en".to_string(),
keywords: None,
producer: None,
creation_date: Utc::now(),
identifier: Uuid::new_v4().to_string(),
}
}
fn fixed() -> Self {
Metadata {
title: "".to_string(),
language: "en".to_string(),
keywords: None,
producer: None,
creation_date: chrono::DateTime::UNIX_EPOCH,
identifier: "00000000-0000-0000-0000-000000000000".to_string(),
}
}
}
pub struct Pdf {
pub alloc: Ref,
pub pdf: pdf_writer::Pdf,
pub pages: Vec<Page>,
pub fonts: Vec<Ref>,
pub metadata: Metadata,
truetype_fonts: Vec<fonts::truetype::TruetypeFontState>,
}
impl Pdf {
pub fn new(metadata: Metadata) -> Self {
let pdf = pdf_writer::Pdf::new();
Pdf {
alloc: pdf_writer::Ref::new(1),
pdf,
pages: Vec::new(),
fonts: Vec::new(),
metadata,
truetype_fonts: Vec::new(),
}
}
pub fn alloc(&mut self) -> Ref {
self.alloc.bump()
}
pub fn add_page(&mut self, size: (f32, f32)) -> Location {
self.pages.push(Page {
ext_g_states: Vec::new(),
x_objects: Vec::new(),
annotations: Vec::new(),
layers: vec![Layer {
content: Content::new(),
graphics_state_restore_required: false,
}],
size,
});
Location {
page_idx: self.pages.len() - 1,
layer_idx: 0,
pos: (0., size.1),
scale_factor: 1.,
}
}
pub fn add_element(&mut self, page_size: (f32, f32), element: impl Element) {
let text_pieces_cache = TextPiecesCache::new();
self.add_element_with_text_pieces_cache(page_size, &text_pieces_cache, element);
}
pub fn add_element_with_text_pieces_cache(
&mut self,
page_size: (f32, f32),
text_pieces_cache: &TextPiecesCache,
element: impl Element,
) {
let mut page_idx = self.pages.len() as u32;
let location = self.add_page((page_size.0, page_size.1));
let entry_page = page_idx;
let do_break = &mut |pdf: &mut Pdf, location_idx, _height| {
while page_idx <= entry_page + location_idx {
pdf.add_page((page_size.0, page_size.1));
page_idx += 1;
}
Location {
page_idx: (entry_page + location_idx + 1) as usize,
layer_idx: 0,
pos: (0., page_size.1),
scale_factor: 1.,
}
};
let ctx = DrawCtx {
pdf: self,
text_pieces_cache,
width: WidthConstraint {
max: page_size.0,
expand: true,
},
location,
first_height: page_size.1,
preferred_height: None,
breakable: Some(BreakableDraw {
full_height: page_size.1,
preferred_height_break_count: 0,
do_break,
}),
};
element.draw(ctx);
}
pub fn finish(mut self) -> Vec<u8> {
let catalog_ref = self.alloc();
let page_tree_ref = self.alloc();
{
let mut writer = XmpWriter::new();
let identifier: Vec<u8> = self.metadata.identifier.clone().into();
self.pdf.set_file_id((identifier.clone(), identifier));
{
let id = self.alloc();
let mut document_info = self.pdf.document_info(id);
document_info.title(TextStr(self.metadata.title.clone().as_str()));
if let Some(keywords) = &self.metadata.keywords {
document_info.keywords(TextStr(keywords));
}
if let Some(producer) = &self.metadata.producer {
document_info.producer(TextStr(producer));
}
document_info.creation_date(
Date::new(self.metadata.creation_date.year() as u16)
.month(self.metadata.creation_date.month() as u8)
.day(self.metadata.creation_date.day() as u8)
.hour(self.metadata.creation_date.hour() as u8)
.minute(self.metadata.creation_date.minute() as u8)
.second(self.metadata.creation_date.second() as u8),
);
}
writer.title([(None, self.metadata.title.as_str())]);
writer.language([LangId(&self.metadata.language.as_str())]);
if let Some(ref keywords) = self.metadata.keywords {
writer.pdf_keywords(keywords);
}
if let Some(producer) = &self.metadata.producer {
writer.producer(producer);
}
writer.create_date(DateTime::new(
self.metadata.creation_date.year() as u16,
self.metadata.creation_date.month() as u8,
self.metadata.creation_date.day() as u8,
self.metadata.creation_date.hour() as u8,
self.metadata.creation_date.minute() as u8,
self.metadata.creation_date.second() as u8,
Timezone::Utc,
));
writer.xmp_identifier([self.metadata.identifier.as_str()]);
writer.pdfa_part(2);
writer.pdfa_conformance("U");
writer.pdf_version("1.7");
let finished = writer.finish(None);
let id = self.alloc();
let icc_profile_ref = self.alloc();
self.pdf.metadata(id, finished.as_bytes());
self.pdf
.icc_profile(
icc_profile_ref,
include_bytes!("../assets/icc_profiles/sRGB-v4.icc"),
)
.n(3);
let mut catalog = self.pdf.catalog(catalog_ref);
catalog.metadata(id).pages(page_tree_ref);
catalog
.output_intents()
.push()
.subtype(pdf_writer::types::OutputIntentSubtype::PDFA)
.dest_output_profile(icc_profile_ref)
.output_condition_identifier(TextStr("sRGB-v4"));
}
for mut truetype_font in self.truetype_fonts {
truetype_font.finish(&mut self.pdf, &mut self.alloc);
}
let pages = self
.pages
.iter()
.scan(self.alloc, |state, _| Some(state.bump()));
self.pdf
.pages(page_tree_ref)
.kids(pages)
.count(self.pages.len() as i32);
let mut page_alloc = self.alloc;
self.alloc = Ref::new(self.alloc.get() + self.pages.len() as i32);
for page in self.pages {
let mut page_writer = self.pdf.page(page_alloc.bump());
page_writer
.parent(page_tree_ref)
.media_box(Rect::new(
0.,
0.,
(page.size.0 * 72. / 25.4) as f32,
(page.size.1 * 72. / 25.4) as f32,
))
.contents_array(
page.layers
.iter()
.scan(self.alloc, |state, _| Some(state.bump())),
);
if !page.annotations.is_empty() {
page_writer.annotations(page.annotations);
}
let mut resources = page_writer.resources();
let mut ext_g_states = resources.ext_g_states();
for (i, ext_g_state) in page.ext_g_states.iter().enumerate() {
ext_g_states.pair(Name(format!("{i}").as_bytes()), ext_g_state);
}
drop(ext_g_states);
if !page.x_objects.is_empty() {
let mut x_objects = resources.x_objects();
for (i, x_object) in page.x_objects.iter().enumerate() {
x_objects.pair(Name(format!("{i}").as_bytes()), x_object);
}
}
let mut fonts = resources.fonts();
for (i, &font) in self.fonts.iter().enumerate() {
fonts.pair(Name(&format!("F{}", i).as_bytes()), font);
}
drop(fonts);
drop(resources);
drop(page_writer);
for mut layer in page.layers {
if layer.graphics_state_restore_required {
layer.content.restore_state();
}
self.pdf.stream(self.alloc.bump(), &layer.content.finish());
}
}
self.pdf.finish()
}
}
#[derive(Clone, Debug)]
pub struct Location {
pub page_idx: usize,
pub layer_idx: usize,
pub pos: (f32, f32),
pub scale_factor: f32,
}
impl Location {
pub fn layer<'a>(&self, pdf: &'a mut Pdf) -> &'a mut Content {
&mut pdf.pages[self.page_idx].layers[self.layer_idx].content
}
pub fn next_layer(&self, pdf: &mut Pdf) -> Location {
let page = &mut pdf.pages[self.page_idx];
let mut content = Content::new();
let graphics_state_restore_required = if self.scale_factor != 1. {
content
.save_state()
.transform(utils::scale(self.scale_factor));
true
} else {
false
};
page.layers.push(Layer {
content,
graphics_state_restore_required,
});
Location {
layer_idx: page.layers.len() - 1,
..*self
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WidthConstraint {
pub max: f32,
pub expand: bool,
}
impl WidthConstraint {
pub fn constrain(&self, width: f32) -> f32 {
if self.expand {
self.max
} else {
width.min(self.max)
}
}
pub fn max(&self, width: f32) -> f32 {
if self.expand {
width.max(self.max)
} else {
width
}
}
}
pub type Pos = (f32, f32);
pub type Size = (f32, f32);
pub type Break<'a> = &'a mut dyn FnMut(&mut Pdf, u32, Option<f32>) -> Location;
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum FirstLocationUsage {
NoneHeight,
WillUse,
WillSkip,
}
pub struct FirstLocationUsageCtx<'a> {
pub text_pieces_cache: &'a TextPiecesCache,
pub width: WidthConstraint,
pub first_height: f32,
pub full_height: f32,
}
impl<'a> FirstLocationUsageCtx<'a> {
pub fn break_appropriate_for_min_height(&self, height: f32) -> bool {
height > self.first_height && self.full_height > self.first_height
}
}
pub struct BreakableMeasure<'a> {
pub full_height: f32,
pub break_count: &'a mut u32,
pub extra_location_min_height: &'a mut Option<f32>,
}
pub struct MeasureCtx<'a> {
pub text_pieces_cache: &'a TextPiecesCache,
pub width: WidthConstraint,
pub first_height: f32,
pub breakable: Option<BreakableMeasure<'a>>,
}
impl<'a> MeasureCtx<'a> {
pub fn break_if_appropriate_for_min_height(&mut self, height: f32) -> bool {
if let Some(ref mut breakable) = self.breakable {
if height > self.first_height && breakable.full_height > self.first_height {
*breakable.break_count = 1;
return true;
}
}
false
}
}
pub struct BreakableDraw<'a> {
pub full_height: f32,
pub preferred_height_break_count: u32,
pub do_break: Break<'a>,
}
pub struct DrawCtx<'a, 'b> {
pub pdf: &'a mut Pdf,
pub text_pieces_cache: &'a TextPiecesCache,
pub location: Location,
pub width: WidthConstraint,
pub first_height: f32,
pub preferred_height: Option<f32>,
pub breakable: Option<BreakableDraw<'b>>,
}
impl<'a, 'b> DrawCtx<'a, 'b> {
pub fn break_if_appropriate_for_min_height(&mut self, height: f32) -> bool {
if let Some(ref mut breakable) = self.breakable {
if height > self.first_height && breakable.full_height > self.first_height {
self.location = (breakable.do_break)(self.pdf, 0, None);
return true;
}
}
false
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ElementSize {
pub width: Option<f32>,
pub height: Option<f32>,
}
impl ElementSize {
pub fn new(width: Option<f32>, height: Option<f32>) -> Self {
ElementSize { width, height }
}
}
pub trait Element {
#[allow(unused_variables)]
fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
FirstLocationUsage::WillUse
}
fn measure(&self, ctx: MeasureCtx) -> ElementSize;
fn draw(&self, ctx: DrawCtx) -> ElementSize;
fn with_padding_top(self, padding: f32) -> Padding<Self>
where
Self: Sized,
{
Padding {
left: 0.,
right: 0.,
top: padding,
bottom: 0.,
element: self,
}
}
fn with_padding_bottom(self, padding: f32) -> Padding<Self>
where
Self: Sized,
{
Padding {
left: 0.,
right: 0.,
top: 0.,
bottom: padding,
element: self,
}
}
fn with_vertical_padding(self, padding: f32) -> Padding<Self>
where
Self: Sized,
{
Padding {
left: 0.,
right: 0.,
top: padding,
bottom: padding,
element: self,
}
}
fn with_padding_left(self, padding: f32) -> Padding<Self>
where
Self: Sized,
{
Padding {
left: padding,
right: 0.,
top: 0.,
bottom: 0.,
element: self,
}
}
fn with_padding_right(self, padding: f32) -> Padding<Self>
where
Self: Sized,
{
Padding {
left: 0.,
right: padding,
top: 0.,
bottom: 0.,
element: self,
}
}
fn with_horizontal_padding(self, padding: f32) -> Padding<Self>
where
Self: Sized,
{
Padding {
left: padding,
right: padding,
top: 0.,
bottom: 0.,
element: self,
}
}
fn debug(self, color: u8) -> elements::debug::Debug<Self>
where
Self: Sized,
{
elements::debug::Debug {
element: self,
color,
show_max_width: false,
show_last_location_max_height: false,
}
}
}
pub trait CompositeElementCallback {
fn call(self, element: &impl Element);
}
pub trait CompositeElement {
fn element(&self, callback: impl CompositeElementCallback);
}
impl<C: CompositeElement> Element for C {
fn first_location_usage(&self, ctx: FirstLocationUsageCtx) -> FirstLocationUsage {
struct Callback<'a> {
ctx: FirstLocationUsageCtx<'a>,
ret: &'a mut FirstLocationUsage,
}
impl<'a> CompositeElementCallback for Callback<'a> {
fn call(self, element: &impl Element) {
*self.ret = element.first_location_usage(self.ctx);
}
}
let mut ret = FirstLocationUsage::NoneHeight;
self.element(Callback { ctx, ret: &mut ret });
ret
}
fn measure(&self, ctx: MeasureCtx) -> ElementSize {
struct Callback<'a> {
ctx: MeasureCtx<'a>,
ret: &'a mut ElementSize,
}
impl<'a> CompositeElementCallback for Callback<'a> {
fn call(self, element: &impl Element) {
*self.ret = element.measure(self.ctx);
}
}
let mut ret = ElementSize {
width: None,
height: None,
};
self.element(Callback { ctx, ret: &mut ret });
ret
}
fn draw(&self, ctx: DrawCtx) -> ElementSize {
struct Callback<'pdf, 'a, 'r> {
ctx: DrawCtx<'pdf, 'a>,
ret: &'r mut ElementSize,
}
impl<'pdf, 'a, 'r> CompositeElementCallback for Callback<'pdf, 'a, 'r> {
fn call(self, element: &impl Element) {
*self.ret = element.draw(self.ctx);
}
}
let mut ret = ElementSize {
width: None,
height: None,
};
self.element(Callback { ctx, ret: &mut ret });
ret
}
}