use genpdf::elements::Paragraph;
use genpdf::fonts::{FontData, FontFamily};
use genpdf::style;
use genpdf::{Document, Element};
use crate::errors::LabscriptError;
use crate::qr;
use crate::types::Prescription;
const FONT_SEARCH: &[(&str, &str, &str, &str, &str)] = &[
(
"/usr/share/fonts/truetype/liberation",
"LiberationSans-Regular.ttf",
"LiberationSans-Bold.ttf",
"LiberationSans-Italic.ttf",
"LiberationSans-BoldItalic.ttf",
),
(
"/usr/share/fonts/liberation-sans",
"LiberationSans-Regular.ttf",
"LiberationSans-Bold.ttf",
"LiberationSans-Italic.ttf",
"LiberationSans-BoldItalic.ttf",
),
(
"/System/Library/Fonts/Supplemental",
"Arial.ttf",
"Arial Bold.ttf",
"Arial Italic.ttf",
"Arial Bold Italic.ttf",
),
(
"/Library/Fonts",
"Arial Unicode.ttf",
"Arial Unicode.ttf",
"Arial Unicode.ttf",
"Arial Unicode.ttf",
),
];
fn load_font_family() -> Result<FontFamily<FontData>, LabscriptError> {
if let Ok(family) = genpdf::fonts::from_files(
"/usr/share/fonts/truetype/liberation",
"LiberationSans",
None,
) {
return Ok(family);
}
for (dir, regular, bold, italic, bold_italic) in FONT_SEARCH {
let dir = std::path::Path::new(dir);
let r_path = dir.join(regular);
let b_path = dir.join(bold);
let i_path = dir.join(italic);
let bi_path = dir.join(bold_italic);
if r_path.exists() && b_path.exists() && i_path.exists() && bi_path.exists() {
let load = |path: &std::path::Path| -> Result<FontData, LabscriptError> {
let data = std::fs::read(path).map_err(|e| {
LabscriptError::Pdf(format!("Failed to read font {}: {e}", path.display()))
})?;
FontData::new(data, None).map_err(|e| {
LabscriptError::Pdf(format!("Failed to parse font {}: {e}", path.display()))
})
};
return Ok(FontFamily {
regular: load(&r_path)?,
bold: load(&b_path)?,
italic: load(&i_path)?,
bold_italic: load(&bi_path)?,
});
}
}
Err(LabscriptError::Pdf(
"Could not find fonts. Install Liberation Sans (Linux) or ensure Arial is available (macOS)."
.into(),
))
}
pub fn generate(
rx: &Prescription,
_signature_path: Option<&str>,
output_path: &str,
) -> Result<(), LabscriptError> {
let font_family = load_font_family()?;
let mut doc = Document::new(font_family);
doc.set_title("Prescription");
doc.set_minimal_conformance();
doc.set_paper_size(genpdf::PaperSize::A4);
let mut decorator = genpdf::SimplePageDecorator::new();
decorator.set_margins(15);
doc.set_page_decorator(decorator);
doc.push(
Paragraph::default()
.styled_string(&rx.prescriber.name, style::Style::new().bold().with_font_size(18)),
);
if let Some(ref creds) = rx.prescriber.credentials {
doc.push(Paragraph::new(creds.as_str()));
}
if let Some(ref license) = rx.prescriber.license {
doc.push(Paragraph::new(format!("License: {license}")));
}
if let Some(ref addr) = rx.prescriber.address {
doc.push(Paragraph::new(addr.as_str()));
}
doc.push(Paragraph::new(
"------------------------------------------------------------------------",
));
doc.push(
Paragraph::default()
.styled_string("Patient: ", style::Style::new().bold())
.string(&rx.patient.name),
);
doc.push(Paragraph::new(format!("DOB: {}", rx.patient.dob)));
if let Some(ref addr) = rx.patient.address {
doc.push(Paragraph::new(format!("Address: {addr}")));
}
doc.push(Paragraph::new(""));
doc.push(Paragraph::new(format!("Date: {}", rx.date)));
doc.push(Paragraph::new(""));
doc.push(
Paragraph::default()
.styled_string("Rx ", style::Style::new().bold().with_font_size(16))
.styled_string("Prescription", style::Style::new().bold().with_font_size(14)),
);
doc.push(Paragraph::new(""));
for (i, med) in rx.medications.iter().enumerate() {
doc.push(
Paragraph::default()
.styled_string(
format!("{}. {} {}", i + 1, med.drug, med.strength),
style::Style::new().bold(),
),
);
let mut details = String::new();
if let Some(ref form) = med.form {
details.push_str(&format!("Form: {form} "));
}
details.push_str(&format!("Qty: {} ", med.quantity));
details.push_str(&format!("Sig: {}", med.sig));
doc.push(Paragraph::new(details));
let refills = med.refills.unwrap_or(0);
doc.push(Paragraph::new(format!("Refills: {refills}")));
doc.push(Paragraph::new(""));
}
if let Some(ref dx) = rx.diagnosis {
doc.push(
Paragraph::default()
.styled_string("Diagnosis: ", style::Style::new().bold())
.string(dx.as_str()),
);
}
if let Some(ref notes) = rx.notes {
doc.push(
Paragraph::default()
.styled_string("Notes: ", style::Style::new().bold())
.string(notes.as_str()),
);
}
doc.push(Paragraph::new(""));
if _signature_path.is_some() {
doc.push(
Paragraph::default()
.styled_string("Signature: ", style::Style::new().bold())
.string("[e-signature attached]"),
);
} else {
doc.push(Paragraph::new("Signature: _______________"));
}
doc.push(Paragraph::new(""));
let qr_lines = qr::render_qr_text(&rx.qr_payload)?;
doc.push(
Paragraph::default()
.styled_string("Verification QR:", style::Style::new().bold()),
);
for line in &qr_lines {
doc.push(
Paragraph::new(line.as_str())
.styled(style::Style::new().with_font_size(6)),
);
}
doc.push(Paragraph::new(""));
doc.push(Paragraph::new(
"------------------------------------------------------------------------",
));
doc.push(Paragraph::new(format!("Rx ID: {}", rx.id)));
doc.push(Paragraph::new(format!(
"Generated by labscript v{}",
env!("CARGO_PKG_VERSION")
)));
doc.render_to_file(output_path)
.map_err(|e| LabscriptError::Pdf(format!("Failed to write PDF: {e}")))?;
Ok(())
}