#![warn(missing_docs, rust_2018_idioms)]
mod wrap;
pub mod elements;
pub mod error;
pub mod fonts;
pub mod render;
pub mod style;
use std::fs;
use std::io;
use std::path;
use derive_more::{
Add, AddAssign, Div, DivAssign, From, Into, Mul, MulAssign, Sub, SubAssign, Sum,
};
use error::Context as _;
#[derive(
Clone,
Copy,
Debug,
Default,
PartialEq,
PartialOrd,
Add,
AddAssign,
Div,
DivAssign,
From,
Into,
Mul,
MulAssign,
Sub,
SubAssign,
Sum,
)]
pub struct Mm(f64);
impl Mm {
pub fn max(self, other: Mm) -> Mm {
Mm(self.0.max(other.0))
}
}
impl From<i8> for Mm {
fn from(mm: i8) -> Mm {
Mm(mm.into())
}
}
impl From<i16> for Mm {
fn from(mm: i16) -> Mm {
Mm(mm.into())
}
}
impl From<i32> for Mm {
fn from(mm: i32) -> Mm {
Mm(mm.into())
}
}
impl From<u8> for Mm {
fn from(mm: u8) -> Mm {
Mm(mm.into())
}
}
impl From<u16> for Mm {
fn from(mm: u16) -> Mm {
Mm(mm.into())
}
}
impl From<u32> for Mm {
fn from(mm: u32) -> Mm {
Mm(mm.into())
}
}
impl From<f32> for Mm {
fn from(mm: f32) -> Mm {
Mm(mm.into())
}
}
impl From<printpdf::Mm> for Mm {
fn from(mm: printpdf::Mm) -> Mm {
Mm(mm.0)
}
}
impl From<printpdf::Pt> for Mm {
fn from(pt: printpdf::Pt) -> Mm {
let mm: printpdf::Mm = pt.into();
mm.into()
}
}
impl From<Mm> for printpdf::Mm {
fn from(mm: Mm) -> printpdf::Mm {
printpdf::Mm(mm.0)
}
}
impl From<Mm> for printpdf::Pt {
fn from(mm: Mm) -> printpdf::Pt {
printpdf::Mm(mm.0).into()
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Alignment {
Left,
Right,
Center,
}
impl Default for Alignment {
fn default() -> Alignment {
Alignment::Left
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Add, AddAssign, Sub, SubAssign)]
pub struct Position {
pub x: Mm,
pub y: Mm,
}
impl Position {
pub fn new(x: impl Into<Mm>, y: impl Into<Mm>) -> Position {
Position {
x: x.into(),
y: y.into(),
}
}
}
impl From<Position> for printpdf::Point {
fn from(pos: Position) -> printpdf::Point {
printpdf::Point::new(pos.x.into(), pos.y.into())
}
}
impl<X: Into<Mm>, Y: Into<Mm>> From<(X, Y)> for Position {
fn from(values: (X, Y)) -> Position {
Position::new(values.0, values.1)
}
}
#[derive(Clone, Copy, Default, Debug, PartialEq, PartialOrd, Add, AddAssign, Sub, SubAssign)]
pub struct Rotation {
degrees: f64,
}
impl Rotation {
pub fn from_degrees(degrees: f64) -> Self {
let degrees = degrees % 360.0;
let degrees = if degrees > 180.0 {
degrees - 360.0
} else if degrees < -180.0 {
360.0 + degrees
} else {
degrees
};
Rotation { degrees }
}
pub fn degrees(&self) -> Option<f64> {
if self.degrees != 0.0 {
Some(self.degrees)
} else {
None
}
}
}
impl From<f64> for Rotation {
fn from(degrees: f64) -> Rotation {
Rotation::from_degrees(degrees)
}
}
impl From<Rotation> for Option<f64> {
fn from(rotation: Rotation) -> Option<f64> {
rotation.degrees()
}
}
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Add, AddAssign, Sub, SubAssign)]
pub struct Scale {
pub x: f64,
pub y: f64,
}
impl Default for Scale {
fn default() -> Scale {
Scale::new(1, 1)
}
}
impl Scale {
pub fn new(x: impl Into<f64>, y: impl Into<f64>) -> Scale {
Scale {
x: x.into(),
y: y.into(),
}
}
}
impl<X: Into<f64>, Y: Into<f64>> From<(X, Y)> for Scale {
fn from(values: (X, Y)) -> Scale {
Scale::new(values.0, values.1)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd, Add, AddAssign, Sub, SubAssign)]
pub struct Size {
pub width: Mm,
pub height: Mm,
}
impl Size {
pub fn new(width: impl Into<Mm>, height: impl Into<Mm>) -> Size {
Size {
width: width.into(),
height: height.into(),
}
}
#[must_use]
pub fn stack_vertical(mut self, other: Size) -> Size {
self.width = self.width.max(other.width);
self.height += other.height;
self
}
}
impl<W: Into<Mm>, H: Into<Mm>> From<(W, H)> for Size {
fn from(values: (W, H)) -> Size {
Size::new(values.0, values.1)
}
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum PaperSize {
A4,
Legal,
Letter,
}
impl From<PaperSize> for Size {
fn from(size: PaperSize) -> Size {
match size {
PaperSize::A4 => Size::new(210, 297),
PaperSize::Legal => Size::new(216, 356),
PaperSize::Letter => Size::new(216, 279),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
pub struct Margins {
top: Mm,
right: Mm,
bottom: Mm,
left: Mm,
}
impl Margins {
pub fn trbl(
top: impl Into<Mm>,
right: impl Into<Mm>,
bottom: impl Into<Mm>,
left: impl Into<Mm>,
) -> Margins {
Margins {
top: top.into(),
right: right.into(),
bottom: bottom.into(),
left: left.into(),
}
}
pub fn vh(vertical: impl Into<Mm>, horizontal: impl Into<Mm>) -> Margins {
let (vertical, horizontal) = (vertical.into(), horizontal.into());
Margins::trbl(vertical, horizontal, vertical, horizontal)
}
pub fn all(all: impl Into<Mm>) -> Margins {
let all = all.into();
Margins::trbl(all, all, all, all)
}
}
impl<T: Into<Mm>, R: Into<Mm>, B: Into<Mm>, L: Into<Mm>> From<(T, R, B, L)> for Margins {
fn from(values: (T, R, B, L)) -> Margins {
Margins::trbl(values.0, values.1, values.2, values.3)
}
}
impl<V: Into<Mm>, H: Into<Mm>> From<(V, H)> for Margins {
fn from(values: (V, H)) -> Margins {
Margins::vh(values.0, values.1)
}
}
impl<T: Into<Mm>> From<T> for Margins {
fn from(value: T) -> Margins {
Margins::all(value)
}
}
pub struct Document {
root: elements::LinearLayout,
title: String,
context: Context,
style: style::Style,
paper_size: Size,
decorator: Option<Box<dyn PageDecorator>>,
conformance: Option<printpdf::PdfConformance>,
}
impl Document {
pub fn new(default_font_family: fonts::FontFamily<fonts::FontData>) -> Document {
let font_cache = fonts::FontCache::new(default_font_family);
Document {
root: elements::LinearLayout::vertical(),
title: String::new(),
context: Context::new(font_cache),
style: style::Style::new(),
paper_size: PaperSize::A4.into(),
decorator: None,
conformance: None,
}
}
pub fn add_font_family(
&mut self,
font_family: fonts::FontFamily<fonts::FontData>,
) -> fonts::FontFamily<fonts::Font> {
self.context.font_cache.add_font_family(font_family)
}
pub fn font_cache(&self) -> &fonts::FontCache {
&self.context.font_cache
}
#[cfg(feature = "hyphenation")]
pub fn set_hyphenator(&mut self, hyphenator: hyphenation::Standard) {
self.context.hyphenator = Some(hyphenator);
}
pub fn set_title(&mut self, title: impl Into<String>) {
self.title = title.into();
}
pub fn set_font_size(&mut self, font_size: u8) {
self.style.set_font_size(font_size);
}
pub fn set_line_spacing(&mut self, line_spacing: f64) {
self.style.set_line_spacing(line_spacing);
}
pub fn set_paper_size(&mut self, paper_size: impl Into<Size>) {
self.paper_size = paper_size.into();
}
pub fn set_page_decorator<D: PageDecorator + 'static>(&mut self, decorator: D) {
self.decorator = Some(Box::new(decorator));
}
pub fn set_conformance(&mut self, conformance: printpdf::PdfConformance) {
self.conformance = Some(conformance);
}
pub fn set_minimal_conformance(&mut self) {
self.set_conformance(printpdf::PdfConformance::Custom(
printpdf::CustomPdfConformance {
requires_icc_profile: false,
requires_xmp_metadata: false,
..Default::default()
},
));
}
pub fn push<E: Element + 'static>(&mut self, element: E) {
self.root.push(element);
}
pub fn render(mut self, w: impl io::Write) -> Result<(), error::Error> {
let mut renderer = render::Renderer::new(self.paper_size, &self.title)?;
if let Some(conformance) = self.conformance {
renderer = renderer.with_conformance(conformance);
}
self.context.font_cache.load_pdf_fonts(&renderer)?;
loop {
let mut area = renderer.last_page().last_layer().area();
if let Some(decorator) = &mut self.decorator {
area = decorator.decorate_page(&self.context, area, self.style)?;
}
let result = self.root.render(&self.context, area, self.style)?;
if result.has_more {
if result.size == Size::new(0, 0) {
return Err(error::Error::new(
"Could not fit an element on a new page",
error::ErrorKind::PageSizeExceeded,
));
}
renderer.add_page(self.paper_size);
} else {
break;
}
}
renderer.write(w)
}
pub fn render_to_file(self, path: impl AsRef<path::Path>) -> Result<(), error::Error> {
let path = path.as_ref();
let file = fs::File::create(path)
.with_context(|| format!("Could not create file {}", path.display()))?;
self.render(file)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
pub struct RenderResult {
pub size: Size,
pub has_more: bool,
}
pub trait PageDecorator {
fn decorate_page<'a>(
&mut self,
context: &Context,
area: render::Area<'a>,
style: style::Style,
) -> Result<render::Area<'a>, error::Error>;
}
type HeaderCallback = Box<dyn Fn(usize) -> Box<dyn Element>>;
#[derive(Default)]
pub struct SimplePageDecorator {
page: usize,
margins: Option<Margins>,
header_cb: Option<HeaderCallback>,
}
impl SimplePageDecorator {
pub fn new() -> SimplePageDecorator {
SimplePageDecorator::default()
}
pub fn set_margins(&mut self, margins: impl Into<Margins>) {
self.margins = Some(margins.into());
}
pub fn set_header<F, E>(&mut self, cb: F)
where
F: Fn(usize) -> E + 'static,
E: Element + 'static,
{
self.header_cb = Some(Box::new(move |page| Box::new(cb(page))));
}
}
impl PageDecorator for SimplePageDecorator {
fn decorate_page<'a>(
&mut self,
context: &Context,
mut area: render::Area<'a>,
style: style::Style,
) -> Result<render::Area<'a>, error::Error> {
self.page += 1;
if let Some(margins) = self.margins {
area.add_margins(margins);
}
if let Some(cb) = &self.header_cb {
let mut element = cb(self.page);
let result = element.render(context, area.clone(), style)?;
area.add_offset(Position::new(0, result.size.height));
}
Ok(area)
}
}
pub trait Element {
fn render(
&mut self,
context: &Context,
area: render::Area<'_>,
style: style::Style,
) -> Result<RenderResult, error::Error>;
fn framed(self) -> elements::FramedElement<Self>
where
Self: Sized,
{
elements::FramedElement::new(self)
}
fn padded(self, padding: impl Into<Margins>) -> elements::PaddedElement<Self>
where
Self: Sized,
{
elements::PaddedElement::new(self, padding)
}
fn styled(self, style: impl Into<style::Style>) -> elements::StyledElement<Self>
where
Self: Sized,
{
elements::StyledElement::new(self, style.into())
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Context {
pub font_cache: fonts::FontCache,
#[cfg(feature = "hyphenation")]
pub hyphenator: Option<hyphenation::Standard>,
}
impl Context {
#[cfg(not(feature = "hyphenation"))]
fn new(font_cache: fonts::FontCache) -> Context {
Context { font_cache }
}
#[cfg(feature = "hyphenation")]
fn new(font_cache: fonts::FontCache) -> Context {
Context {
font_cache,
hyphenator: None,
}
}
}
#[cfg(test)]
mod tests {
impl float_cmp::ApproxEq for super::Mm {
type Margin = float_cmp::F64Margin;
fn approx_eq<M: Into<Self::Margin>>(self, other: Self, margin: M) -> bool {
self.0.approx_eq(other.0, margin)
}
}
impl float_cmp::ApproxEq for super::Size {
type Margin = float_cmp::F64Margin;
fn approx_eq<M: Into<Self::Margin>>(self, other: Self, margin: M) -> bool {
let margin = margin.into();
self.width.approx_eq(other.width, margin) && self.height.approx_eq(other.height, margin)
}
}
impl float_cmp::ApproxEq for super::Position {
type Margin = float_cmp::F64Margin;
fn approx_eq<M: Into<Self::Margin>>(self, other: Self, margin: M) -> bool {
let margin = margin.into();
self.x.approx_eq(other.x, margin) && self.y.approx_eq(other.y, margin)
}
}
#[test]
fn test_rotation() {
use super::Rotation;
assert_eq!(None, Rotation::from(0.0).degrees());
assert_eq!(Some(90.0), Rotation::from(90.0).degrees());
assert_eq!(Some(180.0), Rotation::from(180.0).degrees());
assert_eq!(Some(-90.0), Rotation::from(270.0).degrees());
assert_eq!(None, Rotation::from(360.0).degrees());
assert_eq!(Some(90.0), Rotation::from(450.0).degrees());
assert_eq!(Some(180.0), Rotation::from(540.0).degrees());
assert_eq!(Some(-90.0), Rotation::from(-90.0).degrees());
assert_eq!(Some(-180.0), Rotation::from(-180.0).degrees());
assert_eq!(Some(90.0), Rotation::from(-270.0).degrees());
assert_eq!(None, Rotation::from(-360.0).degrees());
assert_eq!(Some(-90.0), Rotation::from(-450.0).degrees());
assert_eq!(Some(-180.0), Rotation::from(-540.0).degrees());
}
}