rustant_tools/
pdf_generate.rs1use async_trait::async_trait;
4use genpdf::elements::{Break, Paragraph};
5use genpdf::{Document, SimplePageDecorator};
6use rustant_core::error::ToolError;
7use rustant_core::types::{RiskLevel, ToolOutput};
8use serde_json::{Value, json};
9use std::path::PathBuf;
10
11use crate::registry::Tool;
12
13pub struct PdfGenerateTool {
14 workspace: PathBuf,
15}
16
17impl PdfGenerateTool {
18 pub fn new(workspace: PathBuf) -> Self {
19 Self { workspace }
20 }
21}
22
23#[async_trait]
24impl Tool for PdfGenerateTool {
25 fn name(&self) -> &str {
26 "pdf_generate"
27 }
28 fn description(&self) -> &str {
29 "Generate PDF documents from text content. Provide title, content lines, and output path."
30 }
31 fn parameters_schema(&self) -> Value {
32 json!({
33 "type": "object",
34 "properties": {
35 "action": {
36 "type": "string",
37 "enum": ["generate"],
38 "description": "Action to perform"
39 },
40 "title": { "type": "string", "description": "Document title" },
41 "content": { "type": "string", "description": "Document content (plain text, paragraphs separated by blank lines)" },
42 "output": { "type": "string", "description": "Output file path (e.g., 'report.pdf')" }
43 },
44 "required": ["action", "output"]
45 })
46 }
47 fn risk_level(&self) -> RiskLevel {
48 RiskLevel::Write
49 }
50
51 async fn execute(&self, args: Value) -> Result<ToolOutput, ToolError> {
52 let action = args
53 .get("action")
54 .and_then(|v| v.as_str())
55 .unwrap_or("generate");
56 if action != "generate" {
57 return Ok(ToolOutput::text(format!(
58 "Unknown action: {}. Use: generate",
59 action
60 )));
61 }
62
63 let title = args
64 .get("title")
65 .and_then(|v| v.as_str())
66 .unwrap_or("Document");
67 let content = args.get("content").and_then(|v| v.as_str()).unwrap_or("");
68 let output_str = args
69 .get("output")
70 .and_then(|v| v.as_str())
71 .unwrap_or("output.pdf");
72 let output_path = self.workspace.join(output_str);
73
74 let font_family =
76 genpdf::fonts::from_files("", "LiberationSans", None).unwrap_or_else(|_| {
77 genpdf::fonts::from_files(
78 "/usr/share/fonts/truetype/liberation",
79 "LiberationSans",
80 None,
81 )
82 .unwrap_or_else(|_| {
83 genpdf::fonts::from_files("/System/Library/Fonts", "Helvetica", None)
84 .unwrap_or_else(|_| {
85 genpdf::fonts::from_files("/Library/Fonts", "Arial", None)
86 .expect("No suitable font found on this system")
87 })
88 })
89 });
90
91 let mut doc = Document::new(font_family);
92 doc.set_title(title);
93
94 let mut decorator = SimplePageDecorator::new();
95 decorator.set_margins(30);
96 doc.set_page_decorator(decorator);
97
98 let title_style = genpdf::style::Style::new().bold().with_font_size(18);
100 doc.push(Paragraph::new(genpdf::style::StyledString::new(
101 title.to_string(),
102 title_style,
103 )));
104 doc.push(Break::new(1));
105
106 for paragraph in content.split("\n\n") {
108 let trimmed = paragraph.trim();
109 if !trimmed.is_empty() {
110 doc.push(Paragraph::new(trimmed));
111 doc.push(Break::new(0.5));
112 }
113 }
114
115 if let Some(parent) = output_path.parent() {
116 std::fs::create_dir_all(parent).ok();
117 }
118
119 doc.render_to_file(&output_path)
120 .map_err(|e| ToolError::ExecutionFailed {
121 name: "pdf_generate".into(),
122 message: format!("Failed to render PDF: {}", e),
123 })?;
124
125 let size = std::fs::metadata(&output_path)
126 .map(|m| m.len())
127 .unwrap_or(0);
128 Ok(ToolOutput::text(format!(
129 "Generated PDF: {} ({} bytes)",
130 output_str, size
131 )))
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[tokio::test]
140 async fn test_pdf_generate_schema() {
141 let dir = tempfile::TempDir::new().unwrap();
142 let tool = PdfGenerateTool::new(dir.path().to_path_buf());
143 assert_eq!(tool.name(), "pdf_generate");
144 assert_eq!(tool.risk_level(), RiskLevel::Write);
145 let schema = tool.parameters_schema();
146 assert!(schema.get("properties").is_some());
147 }
148
149 }