diff --git a/ptm/Cargo.toml b/ptm/Cargo.toml new file mode 100644 index 0000000..2b06b90 --- /dev/null +++ b/ptm/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ptm" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.6", features = ["derive"] } +pdf-extract = "0.7" +clipboard-win = "5.4" +thiserror = "2.0" diff --git a/ptm/README.md b/ptm/README.md new file mode 100644 index 0000000..90bbeb3 --- /dev/null +++ b/ptm/README.md @@ -0,0 +1,168 @@ +# ptm - PDF to Markdown Converter + +一个简单易用的命令行 PDF 转 Markdown 工具。 + +## ✨ 功能特性 + +- 📄 **PDF 文本提取**: 从 PDF 文件中提取纯文本内容 +- 📝 **Markdown 转换**: 将提取的文本转换为 Markdown 格式 +- 💾 **灵活输出**: 支持原地生成或指定输出路径 +- 📋 **剪贴板支持**: 可直接复制生成的 Markdown 到剪贴板 + +## 📦 安装 + +### 从源码编译 + +```bash +cd ptm +cargo build --release +``` + +### 全局安装 + +```bash +cargo install --path . --force +``` + +安装后可直接使用 `ptm` 命令。 + +## 🚀 使用方法 + +**基本格式:** +```bash +ptm [选项] +``` + +### 命令示例 + +#### 1. 基本用法(原地生成) + +```bash +# 将 test.pdf 转换为 test.md(同一目录) +ptm test.pdf +``` + +#### 2. 指定输出路径 + +```bash +# 输出到指定位置 +ptm test.pdf -o output/result.md + +# 使用完整路径 +ptm document.pdf -o C:\Users\macro\Documents\notes.md +``` + +#### 3. 复制到剪贴板 + +```bash +# 转换并复制到剪贴板 +ptm test.pdf -v + +# 指定输出并复制 +ptm test.pdf -o result.md -v +``` + +### 参数说明 + +| 参数 | 说明 | 示例 | +|------|------|------| +| `` | PDF 文件路径(必需) | `test.pdf` | +| `-o, --output` | 输出文件路径(可选,默认与输入同名) | `-o result.md` | +| `-v, --verbose` | 复制内容到剪贴板 | `-v` | +| `-h, --help` | 显示帮助信息 | `-h` | +| `-V, --version` | 显示版本信息 | `-V` | + +## 💡 使用场景 + +### 场景 1: 快速转换 PDF + +```bash +ptm report.pdf +# 生成 report.md +``` + +### 场景 2: 批量处理 + +```powershell +# PowerShell 批量转换 +Get-ChildItem *.pdf | ForEach-Object { + ptm $_.FullName +} +``` + +### 场景 3: 转换后直接粘贴 + +```bash +ptm article.pdf -v +# 现在可以直接在编辑器中 Ctrl+V 粘贴 +``` + +### 场景 4: 自定义输出位置 + +```bash +ptm book.pdf -o notes/book-chapter1.md +``` + +## 📊 工作原理 + +1. **读取 PDF**: 使用 `pdf-extract` 库解析 PDF 文件 +2. **提取文本**: 提取 PDF 中的所有文本内容 +3. **格式转换**: + - 清理多余空行 + - 保留段落结构 + - 转换为纯文本 Markdown +4. **写入文件**: 保存为 `.md` 文件 +5. **可选复制**: 如指定 `-v`,复制到剪贴板 + +## ⚠️ 注意事项 + +1. **文本型 PDF**: 仅支持包含文本层的 PDF,扫描版 PDF 需要 OCR 处理 +2. **格式限制**: 当前版本主要提取纯文本,复杂格式(表格、图片)可能需要手动调整 +3. **编码支持**: 支持 UTF-8 编码,包括中文等多语言内容 +4. **文件大小**: 大文件可能需要较长时间处理 + +## 🔧 技术细节 + +### 依赖库 + +- **pdf-extract**: PDF 文本提取库 +- **clap**: 命令行参数解析 +- **clipboard-win**: Windows 剪贴板操作 +- **thiserror**: 错误处理 + +### 转换逻辑 + +当前的 Markdown 转换采用简化策略: +- 保留原始段落结构 +- 合并连续空行为单个空行 +- 去除每行首尾空白 +- 不尝试识别标题层级(需要更复杂的字体分析) + +## 🎯 未来改进方向 + +- [ ] 智能识别标题层级(基于字体大小) +- [ ] 支持列表和表格转换 +- [ ] 支持图片提取 +- [ ] 更好的段落检测 +- [ ] 支持 OCR 处理扫描版 PDF +- [ ] 批量处理模式 +- [ ] 支持更多输出格式(HTML, TXT 等) + +## 🤝 贡献 + +欢迎提交 Issue 和 Pull Request! + +## 📄 许可证 + +本项目采用 MIT 许可证。 + +## 🙏 致谢 + +感谢以下开源项目: +- [pdf-extract](https://github.com/pdf-extract/pdf-extract-rs) - PDF 文本提取库 +- [clap](https://github.com/clap-rs/clap) - 命令行参数解析库 +- [clipboard-win](https://github.com/DoumanAsh/clipboard-win) - Windows 剪贴板库 + +--- + +**Made with ❤️ using Rust** diff --git a/ptm/src/main.rs b/ptm/src/main.rs new file mode 100644 index 0000000..b0500ca --- /dev/null +++ b/ptm/src/main.rs @@ -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 = std::result::Result; + +/// 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, + + /// 复制生成的 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(()) +}