feat(ptm): 添加 PDF 转 Markdown 转换工具

- 实现 PDF 文本提取功能,支持将 PDF 文件转换为 Markdown 格式
- 添加命令行参数解析,支持输入文件、输出路径和剪贴板复制选项
- 集成剪贴板操作功能,可直接复制转换结果到系统剪贴板
- 实现基础 Markdown 格式转换逻辑,保留段落结构并清理多余空行
- 创建完整的 Cargo 项目配置,包含必要的依赖库和文档说明
This commit is contained in:
macro
2026-05-11 17:31:10 +08:00
parent cb5d7d0768
commit 9dc72c1ac0
3 changed files with 301 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
use clap::Parser;
use std::path::PathBuf;
use std::fs;
use thiserror::Error;
#[derive(Error, Debug)]
enum PtmError {
#[error("PDF 解析错误: {0}")]
PdfExtract(String),
#[error("文件 IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("剪贴板操作失败: {0}")]
Clipboard(String),
}
type Result<T> = std::result::Result<T, PtmError>;
/// PDF to Markdown converter
#[derive(Parser, Debug)]
#[command(name = "ptm", version, about = "PDF 转 Markdown 工具")]
struct Args {
/// PDF 文件路径
input: PathBuf,
/// 输出文件路径(默认与输入文件同名,扩展名为 .md)
#[arg(short, long)]
output: Option<PathBuf>,
/// 复制生成的 Markdown 内容到剪贴板
#[arg(short, long)]
verbose: bool,
}
fn main() {
let args = Args::parse();
if let Err(e) = run(args) {
eprintln!("❌ 错误: {}", e);
std::process::exit(1);
}
}
fn run(args: Args) -> Result<()> {
// 1. 验证输入文件存在
if !args.input.exists() {
return Err(PtmError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("文件不存在: {}", args.input.display()),
)));
}
println!("📄 正在处理: {}", args.input.display());
// 2. 提取 PDF 文本
let output_doc = pdf_extract::extract_text(&args.input)
.map_err(|e| PtmError::PdfExtract(e.to_string()))?;
// 3. 转换为简单的 Markdown 格式
let markdown = convert_to_markdown(&output_doc);
// 4. 确定输出路径
let output_path = match args.output {
Some(path) => path,
None => {
let mut path = args.input.clone();
path.set_extension("md");
path
}
};
// 5. 写入文件
fs::write(&output_path, &markdown)?;
println!("✅ 已生成: {}", output_path.display());
// 6. 如果需要,复制到剪贴板
if args.verbose {
copy_to_clipboard(&markdown)?;
println!("📋 已复制到剪贴板");
}
Ok(())
}
/// 将 PDF 提取的文本转换为 Markdown 格式
fn convert_to_markdown(text: &str) -> String {
// 简单的转换逻辑:
// 1. 保留段落结构
// 2. 清理多余的空行
// 3. 尝试识别标题(基于字体大小等元数据不可用时,只能做简单处理)
let lines: Vec<&str> = text.lines().collect();
let mut result = String::new();
let mut prev_empty = false;
for line in lines {
let trimmed = line.trim();
if trimmed.is_empty() {
if !prev_empty {
result.push('\n');
prev_empty = true;
}
} else {
result.push_str(trimmed);
result.push('\n');
prev_empty = false;
}
}
result
}
/// 复制文本到剪贴板
fn copy_to_clipboard(text: &str) -> Result<()> {
use clipboard_win::{formats, set_clipboard};
set_clipboard(formats::Unicode, text)
.map_err(|e| PtmError::Clipboard(format!("剪贴板操作失败: {:?}", e)))?;
Ok(())
}