use colored::Colorize;
use lopdf::{Document, Object as LopdfObject, StringFormat};
use serde_yml::Value;
use std::collections::BTreeMap;
use std::fs::{create_dir_all, OpenOptions};
use std::io;
use std::path::{Path, PathBuf};
use crate::utils::extract_to_end_string;
use async_std::task;
use chromiumoxide::{cdp::browser_protocol::page::PrintToPdfParams, Browser, BrowserConfig};
use pdf_composer_definitions::consts::{CHECK_MARK, CROSS_MARK, PACKAGE_NAME};
use pdf_composer_definitions::fonts::{FontsStandard, GetCssName};
use pdf_composer_definitions::page_properties::{
PageMargins, PaperOrientation, PaperSize, ToDimensions,
};
use pdf_composer_definitions::pdf_version::PDFVersion;
use futures::StreamExt;
pub fn build_pdf(
generated_html: String,
yaml_btreemap: BTreeMap<String, Value>,
dictionary_entries: BTreeMap<String, String>,
instance_data: PDFBuilder,
) -> Result<(), Box<dyn std::error::Error>> {
let PDFBuilder {
orientation,
source_file,
output_directory,
pdf_version,
paper_size,
margins,
font,
} = instance_data;
let (page_width, page_height) = match orientation {
PaperOrientation::Landscape => (paper_size.to_dimensions().1, paper_size.to_dimensions().0),
PaperOrientation::Portrait => paper_size.to_dimensions(),
};
task::block_on(async {
let filename_path = source_file.trim_end_matches(".md");
let extracted_filename = extract_to_end_string(filename_path);
let extracted_filename_as_string = extracted_filename.unwrap().to_string();
let mut string_values_btreemap: BTreeMap<String, String> = BTreeMap::new();
for (key, value) in yaml_btreemap.clone() {
if let Value::String(string_value) = value {
string_values_btreemap.insert(key, string_value);
}
}
let (browser, mut handler) = Browser::launch(BrowserConfig::builder().build()?).await?;
let _handle = async_std::task::spawn(async move {
loop {
let _event = handler.next().await.unwrap();
}
});
let mut css_page = String::from("<style>\n@media print {\n ");
let (css_font_name, css_font_weight, css_font_style) = font.get_css_name();
let css_font = format!(
"body {{ font-family: {}; font-weight: {}; font-style: {} }}\n\n",
css_font_name, css_font_weight, css_font_style
);
let css_at_page = format!("@page {{\nsize: {}in {}in;\n}}", page_width, page_height);
css_page.push_str(&css_font);
css_page.push_str(&css_at_page);
css_page.push_str("\n}\n</style>");
let title_string = yaml_btreemap
.get("title")
.and_then(|value| value.as_str())
.unwrap_or(&extracted_filename_as_string);
let mut html_string = String::new();
let html_before_string = format!(
"<html><head><title>{}</title>{}</head><body>",
title_string, css_page
);
let html_after_string = "</body></html>";
url_escape::encode_query_to_string(generated_html, &mut html_string);
let mut pdf_file = extracted_filename_as_string;
pdf_file.push_str(".pdf");
let pdf_file_path = Path::new(&output_directory).join(pdf_file);
let pdf_file_path_as_string = pdf_file_path
.clone()
.into_os_string()
.into_string()
.unwrap();
let page = browser
.new_page(
format!(
"data:text/html;charset=utf-8,{}{}{}",
html_before_string, html_string, html_after_string
)
.as_str(),
)
.await?;
let _html = page.wait_for_navigation().await?.content().await?;
let paper_settings = PrintToPdfParams {
paper_width: Some(page_width),
paper_height: Some(page_height),
margin_top: Some(margins[0]),
margin_right: Some(margins[1]),
margin_bottom: Some(margins[2]),
margin_left: Some(margins[3]),
prefer_css_page_size: Some(true),
..Default::default()
};
let pdf = page.pdf(paper_settings).await?;
let mut doc: Document = Document::load_mem(&pdf)?;
doc.version = pdf_version.to_string();
doc.compress();
create_dir_all(pdf_file_path.parent().unwrap())?;
doc.save(pdf_file_path.clone()).unwrap();
#[allow(unused_variables)]
let mut object_count: i32 = 0;
for object_element in &mut doc.objects {
let (_key, object) = object_element;
match object {
LopdfObject::Dictionary(dictionary) => {
let mut creator_found = false;
for (key, value) in dictionary.iter_mut() {
let ascii_key = String::from_utf8_lossy(key);
if ascii_key == "Creator" {
let default_creator = &PACKAGE_NAME.to_string();
let ascii_string = string_values_btreemap
.get("generator")
.unwrap_or(default_creator);
let ascii_bytes: Vec<u8> = ascii_string.as_bytes().to_vec();
*value = lopdf::Object::String(ascii_bytes, StringFormat::Literal);
creator_found = true;
}
if ascii_key == "Producer" {
let ascii_string = PACKAGE_NAME;
let ascii_bytes: Vec<u8> = ascii_string.as_bytes().to_vec();
*value = lopdf::Object::String(ascii_bytes, StringFormat::Literal);
}
}
if creator_found {
for entry in &dictionary_entries {
let entry_exists =
check_entry_exists(entry.1.to_string(), &string_values_btreemap);
if entry_exists {
let (_key, value) = populate_dictionary(
entry.1.to_string(),
string_values_btreemap.clone(),
);
dictionary.set(entry.0.as_bytes().to_vec(), value);
}
}
}
object_count += 1;
}
LopdfObject::Stream(_) => {
object_count += 1;
}
_ => {
}
}
}
let mut error_message = "\n".to_owned()
+ &CROSS_MARK.on_red().to_string()
+ &pdf_file_path_as_string.on_red().to_string()
+ "\n";
error_message.push_str(
"Failed to save modified PDF document."
.red()
.to_string()
.as_str(),
);
match is_file_open(&pdf_file_path_as_string) {
Ok(true) => println!("{} is open by another process.", &pdf_file_path_as_string),
Ok(false) => {
doc.save(pdf_file_path.clone()).unwrap();
println!(
"\n{}{} → {}",
CHECK_MARK.to_string().green(),
source_file.green(),
pdf_file_path_as_string.yellow()
);
println!("{}", "PDF document metadata properties".yellow());
for entry in &dictionary_entries {
let entry_exists =
check_entry_exists(entry.1.to_string(), &string_values_btreemap);
if entry_exists {
println!("* {}: {}", entry.0.cyan(), entry.1.green());
}
}
}
Err(error) => println!("{} {}", error_message, error),
}
Ok(())
})
}
#[derive(Debug)]
pub struct PDFBuilder {
pub source_file: String,
pub output_directory: PathBuf,
pub pdf_version: PDFVersion,
pub paper_size: PaperSize,
pub orientation: PaperOrientation,
pub margins: PageMargins,
pub font: FontsStandard,
}
fn populate_dictionary(
yaml_entry: String,
string_values_btreemap: BTreeMap<String, String>,
) -> (Vec<u8>, LopdfObject) {
let key = yaml_entry.as_bytes().to_vec();
let value_string = string_values_btreemap
.get(&yaml_entry.to_lowercase())
.unwrap();
let value_as_bytes: Vec<u8> = value_string.as_bytes().to_vec();
(
key,
LopdfObject::String(value_as_bytes, StringFormat::Literal),
)
}
fn is_file_open(file_path: &str) -> Result<bool, io::Error> {
match OpenOptions::new().write(true).open(file_path) {
Ok(_) => {
Ok(false)
}
Err(error) => {
match error.kind() {
io::ErrorKind::PermissionDenied => {
Ok(true)
}
_ => Err(error),
}
}
}
}
fn check_entry_exists(entry: String, btree: &BTreeMap<String, String>) -> bool {
let mut entry_exists = false;
for (key, _value) in btree.iter() {
if key == &entry {
entry_exists = true;
break;
}
}
entry_exists
}