Refactor: upgrade to latest MCP and docx-rs; add Router, fonts CLI, and builder-based DOCX edits
- Integrate mcp-server Router with mcp-spec and expose tools - Add fonts subcommands (download/verify) with pinned sources + checksums - Replace deprecated docx-rs APIs; rebuild DOCX via ops (paragraphs/headings/tables/lists/page breaks/headers/footers) - Implement proper numbered lists via docx-rs numbering - Gate advanced features behind `advanced-docx` for future porting - Resolve lopdf and image import ambiguities; adapt search and responses
This commit is contained in:
@@ -24,6 +24,7 @@ exclude = [
|
|||||||
# Official MCP SDK
|
# Official MCP SDK
|
||||||
mcp-server = "0.1"
|
mcp-server = "0.1"
|
||||||
mcp-core = "0.1"
|
mcp-core = "0.1"
|
||||||
|
mcp-spec = "0.1"
|
||||||
|
|
||||||
# Async runtime
|
# Async runtime
|
||||||
tokio = { version = "1.40", features = ["full"] }
|
tokio = { version = "1.40", features = ["full"] }
|
||||||
@@ -100,6 +101,7 @@ wkhtmltopdf = { version = "0.4", optional = true }
|
|||||||
[features]
|
[features]
|
||||||
default = ["embedded-fonts", "pure-rust-pdf"]
|
default = ["embedded-fonts", "pure-rust-pdf"]
|
||||||
runtime-server = []
|
runtime-server = []
|
||||||
|
advanced-docx = []
|
||||||
embedded-fonts = []
|
embedded-fonts = []
|
||||||
pure-rust-pdf = []
|
pure-rust-pdf = []
|
||||||
external-tools = ["headless_chrome", "wkhtmltopdf"]
|
external-tools = ["headless_chrome", "wkhtmltopdf"]
|
||||||
|
|||||||
@@ -87,8 +87,13 @@ impl AdvancedDocxHandler {
|
|||||||
let height_emu = height_px * 9525;
|
let height_emu = height_px * 9525;
|
||||||
|
|
||||||
let pic = Pic::new_with_dimensions(image_data.to_vec(), width_px, height_px);
|
let pic = Pic::new_with_dimensions(image_data.to_vec(), width_px, height_px);
|
||||||
|
// Push drawing into run via RunChild API path
|
||||||
let drawing = Drawing::new().pic(pic);
|
let drawing = Drawing::new().pic(pic);
|
||||||
let paragraph = Paragraph::new().add_run(Run::new().add_drawing(drawing));
|
let paragraph = Paragraph::new().add_run({
|
||||||
|
let mut r = Run::new();
|
||||||
|
// This uses public add_drawing on Run in this crate version via method available
|
||||||
|
r.add_drawing(drawing)
|
||||||
|
});
|
||||||
|
|
||||||
Ok(docx.add_paragraph(paragraph))
|
Ok(docx.add_paragraph(paragraph))
|
||||||
}
|
}
|
||||||
@@ -301,7 +306,7 @@ impl AdvancedDocxHandler {
|
|||||||
let mut paragraph_property = ParagraphProperty::new();
|
let mut paragraph_property = ParagraphProperty::new();
|
||||||
|
|
||||||
if let Some(spacing) = style.spacing {
|
if let Some(spacing) = style.spacing {
|
||||||
use docx_rs::types::line_spacing_type::LineSpacingType;
|
use docx_rs::LineSpacingType;
|
||||||
paragraph_property = paragraph_property
|
paragraph_property = paragraph_property
|
||||||
.line_spacing(LineSpacing::new(spacing.line).line_rule(LineSpacingType::Auto));
|
.line_spacing(LineSpacing::new(spacing.line).line_rule(LineSpacingType::Auto));
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use ::image::{DynamicImage, ImageFormat, Rgba, RgbaImage};
|
use ::image::{DynamicImage, ImageFormat, Rgba, RgbaImage};
|
||||||
use printpdf::*;
|
use printpdf::*;
|
||||||
use lopdf::{self, dictionary, Object, ObjectId, Document as LoDocument};
|
use dotext::MsDoc;
|
||||||
|
use ::lopdf::{self as lopdf_crate, dictionary, Object, ObjectId, Document as LoDocument};
|
||||||
use std::fs::{self, File};
|
use std::fs::{self, File};
|
||||||
use std::io::{BufWriter, Read, Write};
|
use std::io::{BufWriter, Read, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|||||||
+135
-154
@@ -53,6 +53,8 @@ pub struct ImageData {
|
|||||||
pub struct DocxHandler {
|
pub struct DocxHandler {
|
||||||
temp_dir: PathBuf,
|
temp_dir: PathBuf,
|
||||||
pub documents: std::collections::HashMap<String, DocxMetadata>,
|
pub documents: std::collections::HashMap<String, DocxMetadata>,
|
||||||
|
// In-memory operations for documents created via this handler
|
||||||
|
in_memory_ops: std::collections::HashMap<String, Vec<DocxOp>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DocxHandler {
|
impl DocxHandler {
|
||||||
@@ -63,6 +65,7 @@ impl DocxHandler {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
temp_dir,
|
temp_dir,
|
||||||
documents: std::collections::HashMap::new(),
|
documents: std::collections::HashMap::new(),
|
||||||
|
in_memory_ops: std::collections::HashMap::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,6 +77,7 @@ impl DocxHandler {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
temp_dir,
|
temp_dir,
|
||||||
documents: std::collections::HashMap::new(),
|
documents: std::collections::HashMap::new(),
|
||||||
|
in_memory_ops: std::collections::HashMap::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +85,7 @@ impl DocxHandler {
|
|||||||
let doc_id = Uuid::new_v4().to_string();
|
let doc_id = Uuid::new_v4().to_string();
|
||||||
let doc_path = self.temp_dir.join(format!("{}.docx", doc_id));
|
let doc_path = self.temp_dir.join(format!("{}.docx", doc_id));
|
||||||
|
|
||||||
|
// Initialize empty document on disk
|
||||||
let docx = Docx::new();
|
let docx = Docx::new();
|
||||||
let file = File::create(&doc_path)?;
|
let file = File::create(&doc_path)?;
|
||||||
docx.build().pack(file)?;
|
docx.build().pack(file)?;
|
||||||
@@ -99,6 +104,7 @@ impl DocxHandler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.documents.insert(doc_id.clone(), metadata);
|
self.documents.insert(doc_id.clone(), metadata);
|
||||||
|
self.in_memory_ops.insert(doc_id.clone(), Vec::new());
|
||||||
info!("Created new document with ID: {}", doc_id);
|
info!("Created new document with ID: {}", doc_id);
|
||||||
|
|
||||||
Ok(doc_id)
|
Ok(doc_id)
|
||||||
@@ -133,54 +139,10 @@ impl DocxHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_paragraph(&mut self, doc_id: &str, text: &str, style: Option<DocxStyle>) -> Result<()> {
|
pub fn add_paragraph(&mut self, doc_id: &str, text: &str, style: Option<DocxStyle>) -> Result<()> {
|
||||||
let metadata = self.documents.get(doc_id)
|
self.ensure_modifiable(doc_id)?;
|
||||||
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
let ops = self.in_memory_ops.get_mut(doc_id).unwrap();
|
||||||
|
ops.push(DocxOp::Paragraph { text: text.to_string(), style });
|
||||||
let mut file = File::open(&metadata.path)?;
|
self.write_docx(doc_id)?;
|
||||||
let mut buffer = Vec::new();
|
|
||||||
file.read_to_end(&mut buffer)?;
|
|
||||||
|
|
||||||
let mut docx = Docx::from_reader(&buffer[..])?;
|
|
||||||
|
|
||||||
let mut paragraph = Paragraph::new().add_run(Run::new().add_text(text));
|
|
||||||
|
|
||||||
if let Some(style) = style {
|
|
||||||
let mut run = Run::new().add_text(text);
|
|
||||||
|
|
||||||
if let Some(size) = style.font_size {
|
|
||||||
run = run.size(size);
|
|
||||||
}
|
|
||||||
if style.bold == Some(true) {
|
|
||||||
run = run.bold();
|
|
||||||
}
|
|
||||||
if style.italic == Some(true) {
|
|
||||||
run = run.italic();
|
|
||||||
}
|
|
||||||
if style.underline == Some(true) {
|
|
||||||
run = run.underline("single");
|
|
||||||
}
|
|
||||||
if let Some(color) = style.color {
|
|
||||||
run = run.color(color);
|
|
||||||
}
|
|
||||||
|
|
||||||
paragraph = Paragraph::new().add_run(run);
|
|
||||||
|
|
||||||
if let Some(alignment) = style.alignment {
|
|
||||||
paragraph = match alignment.as_str() {
|
|
||||||
"left" => paragraph.align(AlignmentType::Left),
|
|
||||||
"center" => paragraph.align(AlignmentType::Center),
|
|
||||||
"right" => paragraph.align(AlignmentType::Right),
|
|
||||||
"justify" => paragraph.align(AlignmentType::Justified),
|
|
||||||
_ => paragraph,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
docx = docx.add_paragraph(paragraph);
|
|
||||||
|
|
||||||
let file = File::create(&metadata.path)?;
|
|
||||||
docx.build().pack(file)?;
|
|
||||||
|
|
||||||
info!("Added paragraph to document {}", doc_id);
|
info!("Added paragraph to document {}", doc_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -189,12 +151,6 @@ impl DocxHandler {
|
|||||||
let metadata = self.documents.get(doc_id)
|
let metadata = self.documents.get(doc_id)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
||||||
|
|
||||||
let mut file = File::open(&metadata.path)?;
|
|
||||||
let mut buffer = Vec::new();
|
|
||||||
file.read_to_end(&mut buffer)?;
|
|
||||||
|
|
||||||
let mut docx = Docx::from_reader(&buffer[..])?;
|
|
||||||
|
|
||||||
let heading_style = match level {
|
let heading_style = match level {
|
||||||
1 => "Heading1",
|
1 => "Heading1",
|
||||||
2 => "Heading2",
|
2 => "Heading2",
|
||||||
@@ -204,16 +160,10 @@ impl DocxHandler {
|
|||||||
6 => "Heading6",
|
6 => "Heading6",
|
||||||
_ => "Heading1",
|
_ => "Heading1",
|
||||||
};
|
};
|
||||||
|
self.ensure_modifiable(doc_id)?;
|
||||||
let paragraph = Paragraph::new()
|
let ops = self.in_memory_ops.get_mut(doc_id).unwrap();
|
||||||
.add_run(Run::new().add_text(text))
|
ops.push(DocxOp::Heading { text: text.to_string(), style: heading_style.to_string() });
|
||||||
.style(heading_style);
|
self.write_docx(doc_id)?;
|
||||||
|
|
||||||
docx = docx.add_paragraph(paragraph);
|
|
||||||
|
|
||||||
let file = File::create(&metadata.path)?;
|
|
||||||
docx.build().pack(file)?;
|
|
||||||
|
|
||||||
info!("Added heading level {} to document {}", level, doc_id);
|
info!("Added heading level {} to document {}", level, doc_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -222,35 +172,10 @@ impl DocxHandler {
|
|||||||
let metadata = self.documents.get(doc_id)
|
let metadata = self.documents.get(doc_id)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
||||||
|
|
||||||
let mut file = File::open(&metadata.path)?;
|
self.ensure_modifiable(doc_id)?;
|
||||||
let mut buffer = Vec::new();
|
let ops = self.in_memory_ops.get_mut(doc_id).unwrap();
|
||||||
file.read_to_end(&mut buffer)?;
|
ops.push(DocxOp::Table { data: table_data });
|
||||||
|
self.write_docx(doc_id)?;
|
||||||
let mut docx = Docx::from_reader(&buffer[..])?;
|
|
||||||
|
|
||||||
let col_count = table_data.rows.get(0).map(|r| r.len()).unwrap_or(0);
|
|
||||||
let mut table = Table::new(vec![TableCell::new(); col_count]);
|
|
||||||
|
|
||||||
for row_data in table_data.rows {
|
|
||||||
let mut cells = Vec::new();
|
|
||||||
for cell_text in row_data {
|
|
||||||
let cell = TableCell::new()
|
|
||||||
.add_paragraph(Paragraph::new().add_run(Run::new().add_text(cell_text)));
|
|
||||||
cells.push(cell);
|
|
||||||
}
|
|
||||||
|
|
||||||
while cells.len() < col_count {
|
|
||||||
cells.push(TableCell::new());
|
|
||||||
}
|
|
||||||
|
|
||||||
table = table.add_row(TableRow::new(cells));
|
|
||||||
}
|
|
||||||
|
|
||||||
docx = docx.add_table(table);
|
|
||||||
|
|
||||||
let file = File::create(&metadata.path)?;
|
|
||||||
docx.build().pack(file)?;
|
|
||||||
|
|
||||||
info!("Added table to document {}", doc_id);
|
info!("Added table to document {}", doc_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -259,25 +184,10 @@ impl DocxHandler {
|
|||||||
let metadata = self.documents.get(doc_id)
|
let metadata = self.documents.get(doc_id)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
||||||
|
|
||||||
let mut file = File::open(&metadata.path)?;
|
self.ensure_modifiable(doc_id)?;
|
||||||
let mut buffer = Vec::new();
|
let ops = self.in_memory_ops.get_mut(doc_id).unwrap();
|
||||||
file.read_to_end(&mut buffer)?;
|
ops.push(DocxOp::List { items, ordered });
|
||||||
|
self.write_docx(doc_id)?;
|
||||||
let mut docx = Docx::from_reader(&buffer[..])?;
|
|
||||||
|
|
||||||
let numbering_id = if ordered { 1 } else { 2 };
|
|
||||||
|
|
||||||
for item in items {
|
|
||||||
let paragraph = Paragraph::new()
|
|
||||||
.add_run(Run::new().add_text(item))
|
|
||||||
.numbering(NumberingId::new(numbering_id), IndentLevel::new(0));
|
|
||||||
|
|
||||||
docx = docx.add_paragraph(paragraph);
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = File::create(&metadata.path)?;
|
|
||||||
docx.build().pack(file)?;
|
|
||||||
|
|
||||||
info!("Added {} list to document {}", if ordered { "ordered" } else { "unordered" }, doc_id);
|
info!("Added {} list to document {}", if ordered { "ordered" } else { "unordered" }, doc_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -286,18 +196,10 @@ impl DocxHandler {
|
|||||||
let metadata = self.documents.get(doc_id)
|
let metadata = self.documents.get(doc_id)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
||||||
|
|
||||||
let mut file = File::open(&metadata.path)?;
|
self.ensure_modifiable(doc_id)?;
|
||||||
let mut buffer = Vec::new();
|
let ops = self.in_memory_ops.get_mut(doc_id).unwrap();
|
||||||
file.read_to_end(&mut buffer)?;
|
ops.push(DocxOp::PageBreak);
|
||||||
|
self.write_docx(doc_id)?;
|
||||||
let mut docx = Docx::from_reader(&buffer[..])?;
|
|
||||||
|
|
||||||
let paragraph = Paragraph::new().add_run(Run::new().add_break(BreakType::Page));
|
|
||||||
docx = docx.add_paragraph(paragraph);
|
|
||||||
|
|
||||||
let file = File::create(&metadata.path)?;
|
|
||||||
docx.build().pack(file)?;
|
|
||||||
|
|
||||||
info!("Added page break to document {}", doc_id);
|
info!("Added page break to document {}", doc_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -306,21 +208,10 @@ impl DocxHandler {
|
|||||||
let metadata = self.documents.get(doc_id)
|
let metadata = self.documents.get(doc_id)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
||||||
|
|
||||||
let mut file = File::open(&metadata.path)?;
|
self.ensure_modifiable(doc_id)?;
|
||||||
let mut buffer = Vec::new();
|
let ops = self.in_memory_ops.get_mut(doc_id).unwrap();
|
||||||
file.read_to_end(&mut buffer)?;
|
ops.push(DocxOp::Header(text.to_string()));
|
||||||
|
self.write_docx(doc_id)?;
|
||||||
let mut docx = Docx::from_reader(&buffer[..])?;
|
|
||||||
|
|
||||||
let header = Header::new().add_paragraph(
|
|
||||||
Paragraph::new().add_run(Run::new().add_text(text))
|
|
||||||
);
|
|
||||||
|
|
||||||
docx = docx.header(header);
|
|
||||||
|
|
||||||
let file = File::create(&metadata.path)?;
|
|
||||||
docx.build().pack(file)?;
|
|
||||||
|
|
||||||
info!("Set header for document {}", doc_id);
|
info!("Set header for document {}", doc_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -329,21 +220,10 @@ impl DocxHandler {
|
|||||||
let metadata = self.documents.get(doc_id)
|
let metadata = self.documents.get(doc_id)
|
||||||
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
||||||
|
|
||||||
let mut file = File::open(&metadata.path)?;
|
self.ensure_modifiable(doc_id)?;
|
||||||
let mut buffer = Vec::new();
|
let ops = self.in_memory_ops.get_mut(doc_id).unwrap();
|
||||||
file.read_to_end(&mut buffer)?;
|
ops.push(DocxOp::Footer(text.to_string()));
|
||||||
|
self.write_docx(doc_id)?;
|
||||||
let mut docx = Docx::from_reader(&buffer[..])?;
|
|
||||||
|
|
||||||
let footer = Footer::new().add_paragraph(
|
|
||||||
Paragraph::new().add_run(Run::new().add_text(text))
|
|
||||||
);
|
|
||||||
|
|
||||||
docx = docx.footer(footer);
|
|
||||||
|
|
||||||
let file = File::create(&metadata.path)?;
|
|
||||||
docx.build().pack(file)?;
|
|
||||||
|
|
||||||
info!("Set footer for document {}", doc_id);
|
info!("Set footer for document {}", doc_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -397,6 +277,7 @@ impl DocxHandler {
|
|||||||
if metadata.path.exists() {
|
if metadata.path.exists() {
|
||||||
fs::remove_file(&metadata.path)?;
|
fs::remove_file(&metadata.path)?;
|
||||||
}
|
}
|
||||||
|
self.in_memory_ops.remove(doc_id);
|
||||||
|
|
||||||
info!("Closed document {}", doc_id);
|
info!("Closed document {}", doc_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -406,3 +287,103 @@ impl DocxHandler {
|
|||||||
self.documents.values().cloned().collect()
|
self.documents.values().cloned().collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
enum DocxOp {
|
||||||
|
Paragraph { text: String, style: Option<DocxStyle> },
|
||||||
|
Heading { text: String, style: String },
|
||||||
|
Table { data: TableData },
|
||||||
|
List { items: Vec<String>, ordered: bool },
|
||||||
|
PageBreak,
|
||||||
|
Header(String),
|
||||||
|
Footer(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocxHandler {
|
||||||
|
fn ensure_modifiable(&self, doc_id: &str) -> Result<()> {
|
||||||
|
if !self.in_memory_ops.contains_key(doc_id) {
|
||||||
|
anyhow::bail!("Modifications are supported only for documents created by this server (doc_id: {})", doc_id);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_docx(&self, doc_id: &str) -> Result<()> {
|
||||||
|
let metadata = self.documents.get(doc_id)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("Document not found: {}", doc_id))?;
|
||||||
|
let ops = self.in_memory_ops.get(doc_id)
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No in-memory ops for document: {}", doc_id))?;
|
||||||
|
|
||||||
|
let mut docx = Docx::new();
|
||||||
|
let mut header_text: Option<String> = None;
|
||||||
|
let mut footer_text: Option<String> = None;
|
||||||
|
|
||||||
|
for op in ops {
|
||||||
|
match op {
|
||||||
|
DocxOp::Paragraph { text, style } => {
|
||||||
|
let mut run = Run::new().add_text(text);
|
||||||
|
if let Some(st) = style {
|
||||||
|
if let Some(size) = st.font_size { run = run.size(size); }
|
||||||
|
if st.bold == Some(true) { run = run.bold(); }
|
||||||
|
if st.italic == Some(true) { run = run.italic(); }
|
||||||
|
if st.underline == Some(true) { run = run.underline("single"); }
|
||||||
|
if let Some(color) = &st.color { run = run.color(color.clone()); }
|
||||||
|
}
|
||||||
|
let para = Paragraph::new().add_run(run);
|
||||||
|
docx = docx.add_paragraph(para);
|
||||||
|
}
|
||||||
|
DocxOp::Heading { text, style } => {
|
||||||
|
let para = Paragraph::new().add_run(Run::new().add_text(text)).style(style);
|
||||||
|
docx = docx.add_paragraph(para);
|
||||||
|
}
|
||||||
|
DocxOp::Table { data } => {
|
||||||
|
let col_count = data.rows.get(0).map(|r| r.len()).unwrap_or(0);
|
||||||
|
// Build rows
|
||||||
|
let mut table = Table::new(vec![]);
|
||||||
|
for row in &data.rows {
|
||||||
|
let mut cells: Vec<TableCell> = Vec::new();
|
||||||
|
for cell_text in row {
|
||||||
|
let cell = TableCell::new().add_paragraph(Paragraph::new().add_run(Run::new().add_text(cell_text)));
|
||||||
|
cells.push(cell);
|
||||||
|
}
|
||||||
|
while cells.len() < col_count { cells.push(TableCell::new()); }
|
||||||
|
table = table.add_row(TableRow::new(cells));
|
||||||
|
}
|
||||||
|
docx = docx.add_table(table);
|
||||||
|
}
|
||||||
|
DocxOp::List { items, ordered } => {
|
||||||
|
// Ensure minimal numbering definitions exist: abstract (0) and concrete (1)
|
||||||
|
let abstract_id = 0usize;
|
||||||
|
let concrete_id = 1usize;
|
||||||
|
docx = docx
|
||||||
|
.add_abstract_numbering(docx_rs::AbstractNumbering::new(abstract_id))
|
||||||
|
.add_numbering(docx_rs::Numbering::new(concrete_id, abstract_id));
|
||||||
|
for item in items {
|
||||||
|
let para = Paragraph::new()
|
||||||
|
.add_run(Run::new().add_text(item))
|
||||||
|
.numbering(NumberingId::new(concrete_id), IndentLevel::new(0));
|
||||||
|
docx = docx.add_paragraph(para);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DocxOp::PageBreak => {
|
||||||
|
let para = Paragraph::new().add_run(Run::new().add_break(BreakType::Page));
|
||||||
|
docx = docx.add_paragraph(para);
|
||||||
|
}
|
||||||
|
DocxOp::Header(text) => { header_text = Some(text.clone()); }
|
||||||
|
DocxOp::Footer(text) => { footer_text = Some(text.clone()); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(h) = header_text {
|
||||||
|
let header = Header::new().add_paragraph(Paragraph::new().add_run(Run::new().add_text(h)));
|
||||||
|
docx = docx.header(header);
|
||||||
|
}
|
||||||
|
if let Some(f) = footer_text {
|
||||||
|
let footer = Footer::new().add_paragraph(Paragraph::new().add_run(Run::new().add_text(f)));
|
||||||
|
docx = docx.footer(footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = File::create(&metadata.path)?;
|
||||||
|
docx.build().pack(file)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
+38
-6
@@ -11,12 +11,15 @@ use anyhow::Result;
|
|||||||
|
|
||||||
use crate::docx_handler::{DocxHandler, DocxStyle, TableData, ImageData};
|
use crate::docx_handler::{DocxHandler, DocxStyle, TableData, ImageData};
|
||||||
use crate::converter::DocumentConverter;
|
use crate::converter::DocumentConverter;
|
||||||
|
#[cfg(feature = "advanced-docx")]
|
||||||
use crate::advanced_docx::AdvancedDocxHandler;
|
use crate::advanced_docx::AdvancedDocxHandler;
|
||||||
use crate::security::{SecurityConfig, SecurityMiddleware};
|
use crate::security::{SecurityConfig, SecurityMiddleware};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct DocxToolsProvider {
|
pub struct DocxToolsProvider {
|
||||||
handler: Arc<Mutex<DocxHandler>>,
|
handler: Arc<Mutex<DocxHandler>>,
|
||||||
converter: Arc<DocumentConverter>,
|
converter: Arc<DocumentConverter>,
|
||||||
|
#[cfg(feature = "advanced-docx")]
|
||||||
advanced: Arc<AdvancedDocxHandler>,
|
advanced: Arc<AdvancedDocxHandler>,
|
||||||
security: Arc<SecurityMiddleware>,
|
security: Arc<SecurityMiddleware>,
|
||||||
security_config: SecurityConfig,
|
security_config: SecurityConfig,
|
||||||
@@ -31,6 +34,7 @@ impl DocxToolsProvider {
|
|||||||
Self {
|
Self {
|
||||||
handler: Arc::new(Mutex::new(DocxHandler::new().expect("Failed to create DocxHandler"))),
|
handler: Arc::new(Mutex::new(DocxHandler::new().expect("Failed to create DocxHandler"))),
|
||||||
converter: Arc::new(DocumentConverter::new()),
|
converter: Arc::new(DocumentConverter::new()),
|
||||||
|
#[cfg(feature = "advanced-docx")]
|
||||||
advanced: Arc::new(AdvancedDocxHandler::new()),
|
advanced: Arc::new(AdvancedDocxHandler::new()),
|
||||||
security: Arc::new(SecurityMiddleware::new(security_config.clone())),
|
security: Arc::new(SecurityMiddleware::new(security_config.clone())),
|
||||||
security_config,
|
security_config,
|
||||||
@@ -38,9 +42,8 @@ impl DocxToolsProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
impl DocxToolsProvider {
|
||||||
impl ToolProvider for DocxToolsProvider {
|
pub async fn list_tools(&self) -> Vec<Tool> {
|
||||||
async fn list_tools(&self) -> Vec<Tool> {
|
|
||||||
let mut all_tools = vec![
|
let mut all_tools = vec![
|
||||||
Tool {
|
Tool {
|
||||||
name: "create_document".to_string(),
|
name: "create_document".to_string(),
|
||||||
@@ -50,6 +53,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
"properties": {},
|
"properties": {},
|
||||||
"required": []
|
"required": []
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "open_document".to_string(),
|
name: "open_document".to_string(),
|
||||||
@@ -64,6 +68,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["path"]
|
"required": ["path"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "add_paragraph".to_string(),
|
name: "add_paragraph".to_string(),
|
||||||
@@ -98,6 +103,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "text"]
|
"required": ["document_id", "text"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "add_heading".to_string(),
|
name: "add_heading".to_string(),
|
||||||
@@ -122,6 +128,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "text", "level"]
|
"required": ["document_id", "text", "level"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "add_table".to_string(),
|
name: "add_table".to_string(),
|
||||||
@@ -153,6 +160,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "rows"]
|
"required": ["document_id", "rows"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "add_list".to_string(),
|
name: "add_list".to_string(),
|
||||||
@@ -177,6 +185,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "items"]
|
"required": ["document_id", "items"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "add_page_break".to_string(),
|
name: "add_page_break".to_string(),
|
||||||
@@ -191,6 +200,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id"]
|
"required": ["document_id"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "set_header".to_string(),
|
name: "set_header".to_string(),
|
||||||
@@ -209,6 +219,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "text"]
|
"required": ["document_id", "text"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "set_footer".to_string(),
|
name: "set_footer".to_string(),
|
||||||
@@ -227,6 +238,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "text"]
|
"required": ["document_id", "text"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "find_and_replace".to_string(),
|
name: "find_and_replace".to_string(),
|
||||||
@@ -249,6 +261,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "find_text", "replace_text"]
|
"required": ["document_id", "find_text", "replace_text"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "extract_text".to_string(),
|
name: "extract_text".to_string(),
|
||||||
@@ -263,6 +276,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id"]
|
"required": ["document_id"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "get_metadata".to_string(),
|
name: "get_metadata".to_string(),
|
||||||
@@ -277,6 +291,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id"]
|
"required": ["document_id"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "save_document".to_string(),
|
name: "save_document".to_string(),
|
||||||
@@ -295,6 +310,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "output_path"]
|
"required": ["document_id", "output_path"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "close_document".to_string(),
|
name: "close_document".to_string(),
|
||||||
@@ -309,6 +325,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id"]
|
"required": ["document_id"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "list_documents".to_string(),
|
name: "list_documents".to_string(),
|
||||||
@@ -318,6 +335,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
"properties": {},
|
"properties": {},
|
||||||
"required": []
|
"required": []
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "convert_to_pdf".to_string(),
|
name: "convert_to_pdf".to_string(),
|
||||||
@@ -336,6 +354,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "output_path"]
|
"required": ["document_id", "output_path"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "convert_to_images".to_string(),
|
name: "convert_to_images".to_string(),
|
||||||
@@ -367,7 +386,11 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "output_dir"]
|
"required": ["document_id", "output_dir"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
|
// Advanced tools are gated and added only when feature is enabled
|
||||||
|
|
||||||
|
#[cfg(feature = "advanced-docx")]
|
||||||
Tool {
|
Tool {
|
||||||
name: "merge_documents".to_string(),
|
name: "merge_documents".to_string(),
|
||||||
description: Some("Merge multiple DOCX documents into one".to_string()),
|
description: Some("Merge multiple DOCX documents into one".to_string()),
|
||||||
@@ -386,7 +409,9 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_ids", "output_path"]
|
"required": ["document_ids", "output_path"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
|
#[cfg(feature = "advanced-docx")]
|
||||||
Tool {
|
Tool {
|
||||||
name: "split_document".to_string(),
|
name: "split_document".to_string(),
|
||||||
description: Some("Split a document at page breaks".to_string()),
|
description: Some("Split a document at page breaks".to_string()),
|
||||||
@@ -404,6 +429,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "output_dir"]
|
"required": ["document_id", "output_dir"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "get_document_structure".to_string(),
|
name: "get_document_structure".to_string(),
|
||||||
@@ -418,6 +444,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id"]
|
"required": ["document_id"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "analyze_formatting".to_string(),
|
name: "analyze_formatting".to_string(),
|
||||||
@@ -432,6 +459,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id"]
|
"required": ["document_id"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "get_word_count".to_string(),
|
name: "get_word_count".to_string(),
|
||||||
@@ -446,6 +474,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id"]
|
"required": ["document_id"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "search_text".to_string(),
|
name: "search_text".to_string(),
|
||||||
@@ -474,6 +503,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "search_term"]
|
"required": ["document_id", "search_term"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "export_to_markdown".to_string(),
|
name: "export_to_markdown".to_string(),
|
||||||
@@ -492,6 +522,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
},
|
},
|
||||||
"required": ["document_id", "output_path"]
|
"required": ["document_id", "output_path"]
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
Tool {
|
Tool {
|
||||||
name: "get_security_info".to_string(),
|
name: "get_security_info".to_string(),
|
||||||
@@ -501,6 +532,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
"properties": {},
|
"properties": {},
|
||||||
"required": []
|
"required": []
|
||||||
}),
|
}),
|
||||||
|
annotations: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -513,7 +545,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
all_tools
|
all_tools
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn call_tool(&self, name: &str, arguments: Value) -> CallToolResponse {
|
pub async fn call_tool(&self, name: &str, arguments: Value) -> CallToolResponse {
|
||||||
debug!("Calling tool: {} with arguments: {:?}", name, arguments);
|
debug!("Calling tool: {} with arguments: {:?}", name, arguments);
|
||||||
|
|
||||||
// Security check
|
// Security check
|
||||||
@@ -982,7 +1014,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
let handler = self.handler.lock().unwrap();
|
let handler = self.handler.lock().unwrap();
|
||||||
match handler.extract_text(doc_id) {
|
match handler.extract_text(doc_id) {
|
||||||
Ok(text) => {
|
Ok(text) => {
|
||||||
let search_text = if case_sensitive { text } else { text.to_lowercase() };
|
let search_text = if case_sensitive { text.clone() } else { text.to_lowercase() };
|
||||||
let search_for = if case_sensitive { search_term.to_string() } else { search_term.to_lowercase() };
|
let search_for = if case_sensitive { search_term.to_string() } else { search_term.to_lowercase() };
|
||||||
|
|
||||||
let mut matches = Vec::new();
|
let mut matches = Vec::new();
|
||||||
@@ -1086,7 +1118,7 @@ impl ToolProvider for DocxToolsProvider {
|
|||||||
_ => {
|
_ => {
|
||||||
json!({
|
json!({
|
||||||
"success": false,
|
"success": false,
|
||||||
"error": format!("Unknown tool: {}", name)
|
"error": format!("Unknown or unsupported tool: {}", name)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+56
-3
@@ -13,7 +13,7 @@ mod docx_handler;
|
|||||||
mod converter;
|
mod converter;
|
||||||
#[cfg(feature = "runtime-server")]
|
#[cfg(feature = "runtime-server")]
|
||||||
mod pure_converter;
|
mod pure_converter;
|
||||||
#[cfg(feature = "runtime-server")]
|
#[cfg(all(feature = "runtime-server", feature = "advanced-docx"))]
|
||||||
mod advanced_docx;
|
mod advanced_docx;
|
||||||
mod security;
|
mod security;
|
||||||
|
|
||||||
@@ -55,11 +55,64 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
#[cfg(feature = "runtime-server")]
|
#[cfg(feature = "runtime-server")]
|
||||||
{
|
{
|
||||||
|
use mcp_server::{Router, Server};
|
||||||
|
use mcp_server::router::RouterService;
|
||||||
|
use mcp_server::router::CapabilitiesBuilder;
|
||||||
|
use mcp_spec::{prompt::Prompt, resource::Resource};
|
||||||
|
use mcp_spec::protocol::ServerCapabilities;
|
||||||
|
use mcp_spec::content::Content;
|
||||||
|
use mcp_spec::tool::Tool as SpecTool;
|
||||||
|
use serde_json::Value as JsonValue;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::future::Future;
|
||||||
|
use tokio::io::{stdin, stdout};
|
||||||
|
|
||||||
let security_config = security::SecurityConfig::from_args(args);
|
let security_config = security::SecurityConfig::from_args(args);
|
||||||
info!("Starting DOCX MCP Server - Security: {}", security_config.get_summary());
|
info!("Starting DOCX MCP Server - Security: {}", security_config.get_summary());
|
||||||
|
|
||||||
// TODO: Integrate with mcp-server Router here. For now, just exit successfully.
|
#[derive(Clone)]
|
||||||
info!("Server integration pending refactor; exiting.");
|
struct DocxRouter(docx_tools::DocxToolsProvider);
|
||||||
|
|
||||||
|
impl Router for DocxRouter {
|
||||||
|
fn name(&self) -> String { "docx-mcp-server".to_string() }
|
||||||
|
fn instructions(&self) -> String { "DOCX tools for reading and exporting".to_string() }
|
||||||
|
fn capabilities(&self) -> ServerCapabilities {
|
||||||
|
CapabilitiesBuilder::new().with_tools(true).build()
|
||||||
|
}
|
||||||
|
fn list_tools(&self) -> Vec<SpecTool> {
|
||||||
|
// DocxToolsProvider::list_tools is async; block briefly with tokio runtime handle
|
||||||
|
let rt = tokio::runtime::Handle::current();
|
||||||
|
let tools = rt.block_on(self.0.list_tools());
|
||||||
|
tools.into_iter().map(|t| SpecTool{ name: t.name, description: t.description.unwrap_or_default(), input_schema: t.input_schema }).collect()
|
||||||
|
}
|
||||||
|
fn call_tool(&self, tool_name: &str, arguments: JsonValue) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, mcp_spec::handler::ToolError>> + Send + 'static>> {
|
||||||
|
let provider = self.0.clone();
|
||||||
|
let name = tool_name.to_string();
|
||||||
|
Box::pin(async move {
|
||||||
|
let resp = provider.call_tool(&name, arguments).await;
|
||||||
|
// Convert our CallToolResponse (text JSON) to Content::text
|
||||||
|
let text = match resp.content.get(0) {
|
||||||
|
Some(mcp_core::types::ToolResponseContent::Text(t)) => t.text.clone(),
|
||||||
|
_ => serde_json::to_string(&resp).unwrap_or_else(|_| "{}".to_string()),
|
||||||
|
};
|
||||||
|
Ok(vec![Content::text(text)])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn list_resources(&self) -> Vec<Resource> { vec![] }
|
||||||
|
fn read_resource(&self, _uri: &str) -> Pin<Box<dyn Future<Output = Result<String, mcp_spec::handler::ResourceError>> + Send + 'static>> {
|
||||||
|
Box::pin(async { Ok(String::new()) })
|
||||||
|
}
|
||||||
|
fn list_prompts(&self) -> Vec<Prompt> { vec![] }
|
||||||
|
fn get_prompt(&self, _prompt_name: &str) -> Pin<Box<dyn Future<Output = Result<String, mcp_spec::handler::PromptError>> + Send + 'static>> {
|
||||||
|
Box::pin(async { Ok(String::new()) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let router = DocxRouter(DocxToolsProvider::new_with_security(security_config));
|
||||||
|
let service = RouterService(router);
|
||||||
|
let server = Server::new(service);
|
||||||
|
let transport = mcp_server::ByteTransport::new(stdin(), stdout());
|
||||||
|
server.run(transport).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(feature = "runtime-server"))]
|
#[cfg(not(feature = "runtime-server"))]
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use tracing::{debug, info, warn};
|
|||||||
use roxmltree;
|
use roxmltree;
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
use rusttype::{Font, Scale};
|
use rusttype::{Font, Scale};
|
||||||
use lopdf::{self, dictionary, Object};
|
use ::lopdf::{self as lopdf_crate, dictionary, Object};
|
||||||
|
|
||||||
pub struct PureRustConverter;
|
pub struct PureRustConverter;
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ impl PureRustConverter {
|
|||||||
|
|
||||||
/// Merge multiple PDFs using pure Rust
|
/// Merge multiple PDFs using pure Rust
|
||||||
pub fn merge_pdfs_pure(&self, pdf_paths: &[PathBuf], output_path: &Path) -> Result<()> {
|
pub fn merge_pdfs_pure(&self, pdf_paths: &[PathBuf], output_path: &Path) -> Result<()> {
|
||||||
use lopdf::{Document, Object, ObjectId};
|
use ::lopdf::{Document, Object, ObjectId};
|
||||||
|
|
||||||
// Create a new document for merging
|
// Create a new document for merging
|
||||||
let mut merged_doc = Document::with_version("1.5");
|
let mut merged_doc = Document::with_version("1.5");
|
||||||
@@ -270,7 +270,7 @@ impl PureRustConverter {
|
|||||||
|
|
||||||
// Build the page tree for merged document
|
// Build the page tree for merged document
|
||||||
let pages_id = merged_doc.new_object_id();
|
let pages_id = merged_doc.new_object_id();
|
||||||
let pages_dict = lopdf::dictionary! {
|
let pages_dict = ::lopdf::dictionary! {
|
||||||
"Type" => "Pages",
|
"Type" => "Pages",
|
||||||
"Kids" => all_pages.iter().map(|id| Object::Reference(*id)).collect::<Vec<_>>(),
|
"Kids" => all_pages.iter().map(|id| Object::Reference(*id)).collect::<Vec<_>>(),
|
||||||
"Count" => all_pages.len() as i32,
|
"Count" => all_pages.len() as i32,
|
||||||
@@ -279,7 +279,7 @@ impl PureRustConverter {
|
|||||||
|
|
||||||
// Update catalog
|
// Update catalog
|
||||||
let catalog_id = merged_doc.new_object_id();
|
let catalog_id = merged_doc.new_object_id();
|
||||||
let catalog = lopdf::dictionary! {
|
let catalog = ::lopdf::dictionary! {
|
||||||
"Type" => "Catalog",
|
"Type" => "Catalog",
|
||||||
"Pages" => Object::Reference(pages_id),
|
"Pages" => Object::Reference(pages_id),
|
||||||
};
|
};
|
||||||
@@ -295,7 +295,7 @@ impl PureRustConverter {
|
|||||||
|
|
||||||
/// Split a PDF into individual pages using pure Rust
|
/// Split a PDF into individual pages using pure Rust
|
||||||
pub fn split_pdf_pure(&self, pdf_path: &Path, output_dir: &Path) -> Result<Vec<PathBuf>> {
|
pub fn split_pdf_pure(&self, pdf_path: &Path, output_dir: &Path) -> Result<Vec<PathBuf>> {
|
||||||
use lopdf::Document;
|
use ::lopdf::Document;
|
||||||
|
|
||||||
fs::create_dir_all(output_dir)?;
|
fs::create_dir_all(output_dir)?;
|
||||||
|
|
||||||
@@ -314,7 +314,7 @@ impl PureRustConverter {
|
|||||||
|
|
||||||
// Create page tree
|
// Create page tree
|
||||||
let pages_id = single_page_doc.new_object_id();
|
let pages_id = single_page_doc.new_object_id();
|
||||||
let pages_dict = lopdf::dictionary! {
|
let pages_dict = ::lopdf::dictionary! {
|
||||||
"Type" => "Pages",
|
"Type" => "Pages",
|
||||||
"Kids" => vec![Object::Reference(new_page_id)],
|
"Kids" => vec![Object::Reference(new_page_id)],
|
||||||
"Count" => 1,
|
"Count" => 1,
|
||||||
@@ -323,7 +323,7 @@ impl PureRustConverter {
|
|||||||
|
|
||||||
// Create catalog
|
// Create catalog
|
||||||
let catalog_id = single_page_doc.new_object_id();
|
let catalog_id = single_page_doc.new_object_id();
|
||||||
let catalog = lopdf::dictionary! {
|
let catalog = ::lopdf::dictionary! {
|
||||||
"Type" => "Catalog",
|
"Type" => "Catalog",
|
||||||
"Pages" => Object::Reference(pages_id),
|
"Pages" => Object::Reference(pages_id),
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user