feat(ptm): 添加 PDF 转 Markdown 转换工具
- 实现 PDF 文本提取功能,支持将 PDF 文件转换为 Markdown 格式 - 添加命令行参数解析,支持输入文件、输出路径和剪贴板复制选项 - 集成剪贴板操作功能,可直接复制转换结果到系统剪贴板 - 实现基础 Markdown 格式转换逻辑,保留段落结构并清理多余空行 - 创建完整的 Cargo 项目配置,包含必要的依赖库和文档说明
This commit is contained in:
@@ -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"
|
||||||
+168
@@ -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 <PDF文件> [选项]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 命令示例
|
||||||
|
|
||||||
|
#### 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 参数说明
|
||||||
|
|
||||||
|
| 参数 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `<INPUT>` | 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**
|
||||||
+123
@@ -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(())
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user