SlideCombine/程序详细说明.md
yuuko 2dd9d4ecea 添加路径粘贴功能
为每个路径选择框添加粘贴按钮,支持从剪贴板直接粘贴路径:

1. 界面调整:
   - 调整文本框宽度以容纳额外按钮
   - 添加绿色的粘贴按钮
   - 重新排列浏览和粘贴按钮位置

2. 新增功能:
   - btnPasteSource_Click:粘贴PDF路径
   - btnPasteText_Click:粘贴TXT源路径
   - btnPasteOutput_Click:粘贴输出路径

3. 用户体验提升:
   - 支持Ctrl+C/CtrlV快捷操作
   - 自动检测剪贴板内容
   - 友好的错误提示
   - 粘贴操作日志记录

现在用户可以更方便地输入路径,既可以通过浏览选择,也可以直接粘贴!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-24 16:17:11 +08:00

22 KiB
Raw Blame History

PDF书签合并工具 - 详细说明文档

1. 程序概述

PDF书签合并工具是一个专为处理PDF书签数据而设计的Windows桌面应用程序。它能够从PDF文件夹中提取书签信息从TXT文件中读取元数据然后将它们合并成符合特定格式的输出文件。

1.1 核心功能

  • PDF书签提取:从FreePic2Pdf_bkmk.txt文件中提取书签目录信息
  • 元数据读取从TXT文件中读取完整的文档元数据
  • 智能合并:按照文件名前缀自动合并相关文件
  • 格式化输出:生成符合标准格式的合并文件

1.2 应用场景

  • 图书馆数字化处理
  • 学术文献编目
  • PDF文档管理系统
  • 书籍目录整理

2. 系统架构

2.1 技术栈

  • 开发框架.NET Framework 4.8
  • 开发语言C#
  • 界面框架Windows Forms (WinForms)
  • 编码支持UTF-8、GBK、GB2312

2.2 项目结构

SlideCombine/
├── Form1.cs                 # 主窗体界面和用户交互逻辑
├── Form1.Designer.cs        # 主窗体界面设计代码
├── Program.cs               # 程序入口点和环境检查
├── BookmarkExtractor.cs     # 书签数据提取核心类
├── ContentFormatter.cs      # 内容格式化处理类
├── FileMerger.cs           # 文件合并处理核心类
├── MetadataModel.cs        # 元数据模型定义
└── SlideCombine.csproj     # 项目配置文件

2.3 核心类设计

2.3.1 BookmarkExtractor.cs - 书签提取器

public class BookmarkItem
{
    public string Title { get; set; }        // 书签标题
    public string Page { get; set; }         // 页码信息
    public string FormattedContent { get; set; } // 格式化内容
}

public class BookmarkExtractor
{
    // 从bkmk文件提取书签列表
    public static List<BookmarkItem> ExtractBookmarksFromBkmk(string bkmkFilePath)

    // 解析单行书签数据
    private static BookmarkItem ParseBookmarkLine(string line)

    // 验证页码格式
    private static bool IsPageNumber(string text)
}

2.3.2 MetadataModel.cs - 元数据模型

public class DocumentMetadata
{
    // 基本信息
    public string Title { get; set; }
    public string OtherTitles { get; set; }
    public string Volume { get; set; }
    public string ISBN { get; set; }

    // 创建和出版信息
    public string Creator { get; set; }
    public string Contributor { get; set; }
    public string IssuedDate { get; set; }
    public string Publisher { get; set; }
    public string Place { get; set; }

    // 分类和页码信息
    public string ClassificationNumber { get; set; }
    public string Page { get; set; }

    // 书签目录
    public List<BookmarkItem> TableOfContents { get; set; }

    // 扩展信息
    public string Subject { get; set; }
    public string Date { get; set; }
    public string Spatial { get; set; }
    public string OtherISBN { get; set; }
    public string OtherTime { get; set; }
    public string Url { get; set; }
}

2.3.3 FileMerger.cs - 文件合并器

public class ProcessResult
{
    public string BaseFileName { get; set; }              // 基础文件名
    public List<string> SourceFiles { get; set; }          // 源文件列表
    public string OutputContent { get; set; }              // 输出内容
    public bool Success { get; set; }                      // 处理状态
    public string ErrorMessage { get; set; }               // 错误信息
    public List<DocumentMetadata> MetadataDocuments { get; set; } // 元数据文档列表
}

public class FileMerger
{
    // 处理所有文件夹 - 主要入口方法
    public static List<ProcessResult> ProcessAllFolders(string pdfRootPath, string txtSourcePath, string txtOutputPath)

    // 处理单个文件组
    private static ProcessResult ProcessFileGroup(string baseName, List<string> bkmkFiles, string txtSourcePath)

    // 查找对应的TXT文件
    private static string GetCorrespondingTxtFile(string bkmkFile, string txtSourcePath)

    // 从文件创建元数据
    private static DocumentMetadata CreateMetadataFromFiles(string txtFile, string bkmkFile)

    // 读取TXT文件元数据
    private static void ReadMetadataFromTxt(string txtFile, DocumentMetadata metadata)

    // 保存处理结果
    public static void SaveResults(List<ProcessResult> results, string outputPath)
}

3. 详细处理逻辑

3.1 整体处理流程

flowchart TD
    A[用户选择三个路径] --> B{路径验证}
    B -->|失败| C[显示错误信息]
    B -->|成功| D[扫描PDF路径下的bkmk文件]
    D --> E{找到bkmk文件?}
    E -->|否| F[显示未找到文件错误]
    E -->|是| G[按文件名前缀分组]
    G --> H[遍历每个文件组]
    H --> I[查找对应的TXT元数据文件]
    I --> J[读取元数据信息]
    J --> K[提取书签目录]
    K --> L[合并元数据和书签]
    L --> M[格式化输出内容]
    M --> N{还有文件组?}
    N -->|是| H
    N -->|否| O[保存到输出路径]
    O --> P[显示处理结果]

3.2 核心处理算法详解

3.2.1 文件发现和分组算法

public static List<ProcessResult> ProcessAllFolders(string pdfRootPath, string txtSourcePath, string txtOutputPath)
{
    var results = new List<ProcessResult>();

    // 第一步扫描PDF路径下的所有bkmk文件
    var bkmkFiles = new List<string>();
    // 支持无扩展名的bkmk文件
    bkmkFiles.AddRange(Directory.GetFiles(pdfRootPath, "FreePic2Pdf_bkmk", SearchOption.AllDirectories));
    // 支持带.txt扩展名的bkmk文件
    bkmkFiles.AddRange(Directory.GetFiles(pdfRootPath, "FreePic2Pdf_bkmk.txt", SearchOption.AllDirectories));

    // 第二步:验证文件存在性
    if (bkmkFiles.Count == 0)
    {
        throw new Exception($"在路径 {pdfRootPath} 下未找到任何 FreePic2Pdf_bkmk 或 FreePic2Pdf_bkmk.txt 文件");
    }

    // 第三步检查TXT源路径
    if (!Directory.Exists(txtSourcePath))
    {
        throw new Exception($"TXT源文件路径不存在: {txtSourcePath}");
    }

    // 第四步:按文件名前缀分组
    var fileGroups = new Dictionary<string, List<string>>();
    foreach (var bkmkFile in bkmkFiles)
    {
        // 获取文件夹名称(如 "CH-875 1-3"
        var folderName = new DirectoryInfo(Path.GetDirectoryName(bkmkFile)).Name;
        // 提取基础名称(如 "CH-875"
        var baseName = GetBaseFileName(folderName);

        if (!fileGroups.ContainsKey(baseName))
        {
            fileGroups[baseName] = new List<string>();
        }
        fileGroups[baseName].Add(bkmkFile);
    }

    // 第五步:处理每个分组
    foreach (var group in fileGroups)
    {
        var result = ProcessFileGroup(group.Key, group.Value.OrderBy(f => f).ToList(), txtSourcePath);
        results.Add(result);
    }

    return results;
}

3.2.2 文件分组逻辑详解

分组规则

  • 取文件夹名称中的空格前部分作为基础名称
  • 例如:CH-875 1-3CH-875
  • 例如:CH-875 4-6CH-875
  • 相同基础名称的文件会被合并到同一个输出文件

分组示例

输入文件:
├── CH-875 1-3/FreePic2Pdf_bkmk.txt
├── CH-875 4-6/FreePic2Pdf_bkmk.txt
├── CH-876 1-2/FreePic2Pdf_bkmk.txt

分组结果:
- CH-875 组:[CH-875 1-3, CH-875 4-6] → 输出 CH-875.txt
- CH-876 组:[CH-876 1-2] → 输出 CH-876.txt

3.2.3 元数据读取算法

private static void ReadMetadataFromTxt(string txtFile, DocumentMetadata metadata)
{
    string[] lines;
    try
    {
        // 优先使用GB2312编码适合中文Windows系统
        lines = File.ReadAllLines(txtFile, Encoding.GetEncoding("GB2312"));
    }
    catch
    {
        // 如果GB2312失败使用系统默认编码
        lines = File.ReadAllLines(txtFile, Encoding.Default);
    }

    foreach (var line in lines)
    {
        // 按冒号分割,最多分割成两部分(处理包含冒号的值)
        var parts = line.Split(new[] { ':' }, 2);
        if (parts.Length == 2)
        {
            var key = parts[0].Trim();
            var value = parts[1].Trim();

            // 根据字段名设置对应的属性
            switch (key)
            {
                case "title": metadata.Title = value; break;
                case "Other titles": metadata.OtherTitles = value; break;
                case "Volume": metadata.Volume = value; break;
                case "ISBN": metadata.ISBN = value; break;
                case "creator": metadata.Creator = value; break;
                case "contributor": metadata.Contributor = value; break;
                case "issuedDate": metadata.IssuedDate = value; break;
                case "publisher": metadata.Publisher = value; break;
                case "place": metadata.Place = value; break;
                case "Classification number": metadata.ClassificationNumber = value; break;
                case "page": metadata.Page = value; break;
                case "subject": metadata.Subject = value; break;
                case "date": metadata.Date = value; break;
                case "spatial": metadata.Spatial = value; break;
                case "Other ISBN": metadata.OtherISBN = value; break;
                case "Other time": metadata.OtherTime = value; break;
                case "url": metadata.Url = value; break;
            }
        }
    }
}

3.2.4 书签提取算法

public static List<BookmarkItem> ExtractBookmarksFromBkmk(string bkmkFilePath)
{
    var bookmarks = new List<BookmarkItem>();

    if (!File.Exists(bkmkFilePath))
    {
        throw new FileNotFoundException($"FreePic2Pdf_bkmk文件不存在: {bkmkFilePath}");
    }

    try
    {
        string content;
        // 自动检测文件编码
        try
        {
            content = File.ReadAllText(bkmkFilePath, Encoding.UTF8);
        }
        catch
        {
            content = File.ReadAllText(bkmkFilePath, Encoding.GetEncoding("GBK"));
        }

        // 按行分割内容
        var lines = content.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);

        foreach (var line in lines)
        {
            var trimmedLine = line.Trim();
            if (string.IsNullOrEmpty(trimmedLine))
                continue;

            // 解析书签行
            var bookmark = ParseBookmarkLine(trimmedLine);
            if (bookmark != null)
            {
                bookmarks.Add(bookmark);
            }
        }
    }
    catch (Exception ex)
    {
        throw new Exception($"读取书签文件失败: {ex.Message}");
    }

    return bookmarks;
}

private static BookmarkItem ParseBookmarkLine(string line)
{
    // 分割行内容,最后一部分作为页码
    var parts = line.Split(new[] { ' ', '\t', ':' }, StringSplitOptions.RemoveEmptyEntries);

    if (parts.Length < 2)
        return null;

    var bookmark = new BookmarkItem();
    var pagePart = parts[parts.Length - 1];

    // 验证页码格式(支持阿拉伯数字和罗马数字)
    if (IsPageNumber(pagePart))
    {
        bookmark.Page = pagePart;
        bookmark.Title = string.Join(" ", parts, 0, parts.Length - 1);
        return bookmark;
    }

    return null;
}

private static bool IsPageNumber(string text)
{
    // 支持阿拉伯数字
    if (System.Text.RegularExpressions.Regex.IsMatch(text, @"^\d+$"))
        return true;

    // 支持罗马数字
    if (System.Text.RegularExpressions.Regex.IsMatch(text, @"^[IVXLCDMivxlcdm]+$"))
        return true;

    return false;
}

3.2.5 格式化输出算法

public string ToFormattedString()
{
    var result = new System.Text.StringBuilder();

    // 严格按照要求的顺序输出所有字段
    result.AppendLine($"title:{Title}");

    if (!string.IsNullOrEmpty(OtherTitles))
        result.AppendLine($"Other titles:{OtherTitles}");

    result.AppendLine($"Volume:{Volume}");
    result.AppendLine($"ISBN:{ISBN}");              // 即使为空也输出
    result.AppendLine($"creator:{Creator}");          // 即使为空也输出
    result.AppendLine($"contributor:{Contributor}"); // 即使为空也输出
    result.AppendLine($"issuedDate:{IssuedDate}");
    result.AppendLine($"publisher:{Publisher}");
    result.AppendLine($"place:{Place}");
    result.AppendLine($"Classification number:{ClassificationNumber}");
    result.AppendLine($"page:{Page}");

    // 书签目录部分
    result.AppendLine("tableOfContents:");
    foreach (var bookmark in TableOfContents)
    {
        if (!string.IsNullOrEmpty(bookmark.Title))
        {
            result.Append(bookmark.Title.Trim());
            if (!string.IsNullOrEmpty(bookmark.Page))
            {
                // 使用14个短横线连接标题和页码
                result.Append("---------------");
                result.Append(bookmark.Page);
            }
            result.AppendLine("<br/>");
        }
    }

    // 继续输出其他字段
    result.AppendLine($"subject:{Subject}");
    result.AppendLine($"date:{Date}");               // 即使为空也输出
    result.AppendLine($"spatial:{Spatial}");         // 即使为空也输出
    result.AppendLine($"Other ISBN:{OtherISBN}");    // 即使为空也输出
    result.AppendLine($"Other time:{OtherTime}");    // 即使为空也输出
    result.AppendLine($"url:{Url}");                 // 即使为空也输出

    return result.ToString();
}

3.3 输出格式规范

3.3.1 单个文档格式

title:文档标题
Other titles:其他标题
Volume:卷期信息
ISBN:ISBN号码
creator:创作者
contributor:贡献者
issuedDate:发行日期
publisher:出版社
place:出版地
Classification number:分类号
page:页数
tableOfContents:
书签标题1---------------页码1<br/>
书签标题2---------------页码2<br/>
...
subject:主题
date:日期范围
spatial:地理信息
Other ISBN:其他ISBN
Other time:其他时间
url:链接地址

3.3.2 多文档合并格式

多个文档之间用<>分隔:

[文档1完整内容]
<>
[文档2完整内容]
<>
[文档3完整内容]

4. 用户界面设计

4.1 界面布局

┌─────────────────────────────────────────────────────────────────┐
│                    PDF书签合并工具 v1.0                         │
├─────────────────────────────────────────────────────────────────┤
│ 📁 PDF文件夹路径                                                │
│ 选择包含PDF文件夹的路径含FreePic2Pdf_bkmk.txt文件          │
│ [路径文本框                          ][浏览]                    │
├─────────────────────────────────────────────────────────────────┤
│ 📄 TXT源文件路径                                                │
│ 选择包含元数据TXT文件的路径                                    │
│ [路径文本框                          ][浏览]                    │
├─────────────────────────────────────────────────────────────────┤
│ 💾 最终输出路径                                                  │
│ 选择合并后TXT文件的输出路径                                    │
│ [路径文本框                          ][浏览]                    │
├─────────────────────────────────────────────────────────────────┤
│ [🚀 开始合并] [🔄 清空] [❌ 退出]                               │
├─────────────────────────────────────────────────────────────────┤
│ 📊 处理进度                                                      │
│ [进度条]                                                         │
│                                                                 │
│ [彩色日志显示区域]                                              │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.2 交互流程

4.2.1 路径选择交互

  1. 用户点击"浏览"按钮
  2. 弹出文件夹选择对话框
  3. 用户选择文件夹后,路径自动填充
  4. 系统在日志中显示选择结果

4.2.2 处理过程交互

  1. 用户点击"🚀 开始合并"按钮
  2. 系统验证所有路径的有效性
  3. 禁用所有按钮,防止重复操作
  4. 显示处理进度和详细日志
  5. 处理完成后显示结果统计
  6. 重新启用按钮

4.2.3 日志系统

// 普通日志 - 黑色
Log("开始处理PDF书签文件...");

// 成功日志 - 绿色
LogSuccess($"✓ 成功处理: {baseName} (合并了 {count} 个文件)");

// 错误日志 - 红色
LogError($"✗ 处理失败: {errorMessage}");

// 信息日志 - 蓝色
LogInfo($"已选择PDF路径: {path}");

5. 错误处理机制

5.1 输入验证

  • 路径存在性检查:验证所有选择的路径是否存在
  • 权限检查:确保有读写权限
  • 文件格式检查:验证文件格式是否符合要求

5.2 编码处理

  • 自动编码检测优先尝试GB2312失败后使用系统默认编码
  • BOM处理输出文件添加UTF-8 BOM标记
  • 特殊字符处理:正确处理德语、中文等特殊字符

5.3 异常处理

try
{
    // 主要处理逻辑
}
catch (FileNotFoundException ex)
{
    LogError($"文件未找到: {ex.Message}");
}
catch (UnauthorizedAccessException ex)
{
    LogError($"访问被拒绝: {ex.Message}");
}
catch (Exception ex)
{
    LogError($"未知错误: {ex.Message}");
}
finally
{
    // 清理资源,重新启用界面
}

6. 性能优化

6.1 文件I/O优化

  • 批量文件操作使用Directory.GetFiles一次性获取文件列表
  • 流式处理:大文件采用流式读取,减少内存占用
  • 编码缓存:缓存编码检测结果,避免重复检测

6.2 内存管理

  • 及时释放使用using语句确保文件句柄及时释放
  • 字符串优化使用StringBuilder进行大量字符串拼接
  • 对象复用:复用正则表达式等对象

6.3 用户体验优化

  • 进度反馈:实时显示处理进度
  • 异步处理:界面响应不被阻塞
  • 详细日志:提供详细的处理信息

7. 部署和维护

7.1 系统要求

  • 操作系统Windows 7或更高版本
  • 运行环境.NET Framework 4.8
  • 内存要求最低512MB推荐1GB
  • 磁盘空间至少10MB可用空间

7.2 安装说明

  1. 确保系统已安装.NET Framework 4.8
  2. 下载SlideCombine.exe程序文件
  3. 双击运行,无需安装
  4. 确保有读取源文件和写入目标文件的权限

7.3 使用示例

示例目录结构:
PDF文件夹/
├── CH-875 1-3/
│   └── FreePic2Pdf_bkmk.txt
├── CH-875 4-6/
│   └── FreePic2Pdf_bkmk.txt

TXT源文件/
├── CH-875 1-3.txt
├── CH-875 4-6.txt

输出路径/
└── [程序生成的文件]

7.4 常见问题解决

Q1程序提示"未找到FreePic2Pdf_bkmk文件" A1检查PDF路径下是否确实存在该文件包括子文件夹

Q2输出文件出现乱码 A2检查源文件的编码格式程序支持GBK和UTF-8

Q3处理速度很慢 A3检查文件数量和大小大文件需要更长时间处理

Q4程序崩溃或无响应 A4检查磁盘空间和权限确保有足够空间写入文件

8. 扩展性设计

8.1 可扩展的格式支持

当前设计支持通过修改配置文件来支持新的文件格式:

<SupportedFormats>
    <BookmarkFiles>
        <Extension name=".txt" pattern="FreePic2Pdf_bkmk*" />
        <Extension name=".bkmk" pattern="FreePic2Pdf" />
    </BookmarkFiles>
</SupportedFormats>

8.2 插件化架构

程序预留了插件接口,可以扩展:

  • 新的文件格式解析器
  • 自定义输出格式
  • 额外的数据验证规则

8.3 国际化支持

界面文本使用资源文件管理,支持多语言:

Resource1.btnBrowse_Text = "浏览";
Resource1.btnMerge_Text = "开始合并";

9. 总结

PDF书签合并工具是一个功能完善、设计良好的桌面应用程序。它具有以下特点

9.1 技术特点

  • 架构清晰:模块化设计,职责分离
  • 编码健壮:完善的错误处理和编码支持
  • 性能优化:高效的文件处理和内存管理
  • 用户友好:直观的界面和详细的反馈

9.2 业务价值

  • 提高效率:自动化处理,减少人工操作
  • 保证质量:标准化格式,减少错误
  • 增强灵活:多路径选择,适应不同场景
  • 易于维护:详细日志,便于问题排查

9.3 适用场景

  • 数字图书馆建设
  • 学术文献编目
  • PDF文档管理系统
  • 出版物目录整理

该工具通过精心的设计和实现为用户提供了一个可靠、高效、易用的PDF书签处理解决方案。