本文档依据当前工程中的以下实现编写:
src/util/FileUtil.h文档以 .cpp 的实际行为为准,而不是只复述头文件注释。FileUtil.h 中还声明了独立的 PDFUtil 类,因此本文在完整介绍 FileUtil 后,也提供 PDFUtil 的全部公开接口说明和调用示例。
#include "util/FileUtil.h"
#include <iostream>
#include <string>
#include <vector>
using stt::KV_Str_Str;
using stt::data::TTable;
using stt::util::FileUtil;
using stt::util::PDFUtil;
FileUtil 的方法全部是静态方法,无需创建对象:
bool exists = FileUtil::fileExists("./data/config.txt");
如果不使用 using,也可以写完整名称:
bool exists = stt::util::FileUtil::fileExists("./data/config.txt");
/ 作为路径分隔符,例如 D:/data/a.txt。pathExists 和 createPath。createPath 只创建一层目录,不等价于 mkdir -p。/vendor/udisk_sdxy/,应优先通过 getExportDir 获取。布尔接口通常用 true 表示成功、false 表示失败;整数接口的含义以各节中的返回值表为准。
部分失败会写入全局错误字符串,可这样读取:
#include "util/Util.h"
if (!FileUtil::readTxtFile("./data/config.txt", content)) {
std::cerr << stt::util::DEBUG::getLastErrorString() << std::endl;
}
注意:并非所有接口都会设置错误字符串,而且成功调用不会统一清空旧错误。因此判断结果时必须首先检查当前方法的返回值。
removeFile、removeFiles、removePath 和 clearPath 会直接删除数据,没有回收站或撤销机制。调用前应校验路径,禁止把用户输入未经检查直接传给这些方法。
| 方法 | 作用 | 成功结果 |
|---|---|---|
exportDB2CSV(3 个重载) |
SQLite 表或查询结果导出为 CSV | true |
getExportDir |
获取平台允许的导出目录 | 非空路径 |
exportTableToFile |
TTable 导出为 CSV |
0 |
importTableFromFile |
CSV 导入 TTable |
0 |
fileExists |
判断文件能否作为输入流打开 | true |
pathExists |
判断目录是否存在 | true |
createTxtFile |
覆盖或追加文本文件 | true |
readTxtFile |
读取文本文件 | true |
createPath |
创建单层目录 | true |
isPathOrFile |
判断路径对象类型 | 0 文件,1 目录 |
getPathContent |
递归枚举目录全部内容 | true |
getPathFile |
枚举当前目录内容 | true |
removeFile |
删除单个文件 | true |
removeFiles |
按正则删除当前目录中的匹配项 | 无返回值 |
removePath |
递归删除目录及目录本身 | true |
clearPath |
清空目录并重新创建该目录 | true |
copyFile |
把文件复制到目录,保留原文件名 | true |
copyFile2 |
把文件复制到指定目标文件 | true |
copyPath |
递归复制目录内容 | true |
getPathSpace |
获取所在磁盘的总空间和可用空间 | 0 |
static bool exportDB2CSV(
const string& pathdb,
const string& tbname,
const string& pathcsv,
string& err);
参数:
pathdb:SQLite 数据库文件路径。tbname:表名或视图名。pathcsv:目标 CSV 文件路径,父目录必须已存在。err:失败时接收错误信息。返回值:成功返回 true;数据库文件不存在、CSV 无法打开、数据库无法连接、表/视图不存在、没有数据或写入失败时返回 false。
实际导出行为:
示例:
std::string err;
bool ok = FileUtil::exportDB2CSV(
"./data/oplog.db",
"S_OPLOG",
"./data/oplog.csv",
err);
if (!ok) {
std::cerr << "导出失败: " << err << std::endl;
} else {
std::cout << "已导出 ./data/oplog.csv" << std::endl;
}
static bool exportDB2CSV(
const string& pathdb,
const string& tbname,
const string& sql,
const string& pathcsv,
string& err);
sql 为空时等价于导出整张表;非空时导出该查询的结果。虽然查询可以只选择部分字段,但 tbname 指定的表或视图仍必须存在,因为实现会先单独检查它。
示例:只导出指定时间以后的记录。
std::string err;
const std::string sql =
"SELECT STIME, TAGNAME, VALUE "
"FROM SAMPLE_5_V "
"WHERE STIME >= '2026-01-01' "
"ORDER BY STIME";
bool ok = FileUtil::exportDB2CSV(
"./data/SAMPLE_5.db",
"SAMPLE_5_V",
sql,
"./data/sample_2026.csv",
err);
if (!ok) {
std::cerr << err << std::endl;
}
不要把未经转义的外部输入直接拼接进 sql。该接口不会绑定 SQL 参数,也不会替调用者防止 SQL 注入或引号错误。
static bool exportDB2CSV(
const string& pathdb,
const string& tbname,
const string& sql,
const string& pathcsv,
string& err,
bool isAsync,
const string& strOK = "导出成功!",
const string& strFail = "导出失败");
isAsync == false:在当前线程执行,返回真实导出结果。isAsync == true:启动 AsynWorker 后立即返回 true;完成后通过消息框显示 strOK 或 strFail。true 只表示已发起任务,不代表 CSV 导出成功。同步调用示例:
std::string err;
bool ok = FileUtil::exportDB2CSV(
"./data/SAMPLE_5.db",
"SAMPLE_5_V",
"SELECT * FROM SAMPLE_5_V",
"./data/sample.csv",
err,
false);
异步调用示例(展示现有 API 的调用形式):
std::string err;
FileUtil::exportDB2CSV(
"./data/SAMPLE_5.db",
"SAMPLE_5_V",
"SELECT * FROM SAMPLE_5_V WHERE STIME > '2026-01-01'",
"./data/sample.csv",
err,
true,
"样本导出成功",
"样本导出失败");
重要限制:当前异步实现把内部局部变量 status 按引用捕获到后台任务中,函数返回后该引用失效,存在未定义行为;err 也按引用捕获,调用方必须保证它在任务结束前仍然有效。在修复实现前,生产代码应优先在调用方自己的后台线程中调用同步重载,而不要依赖此异步重载。
static string getExportDir(const string& subdir);
此方法在头文件中标记为 @HIDECA,但它是公开静态方法。
平台行为:
U盘路径/subdir/;没有 U 盘或目录创建失败时返回空字符串。./data/,忽略 subdir,也不会负责创建 ./data。subdir;多层子目录的父级必须事先存在。示例:
std::string exportDir = FileUtil::getExportDir("sample");
if (exportDir.empty()) {
std::cerr << "没有可用的导出目录" << std::endl;
return;
}
std::string csvPath = exportDir + "sample.csv";
std::string err;
FileUtil::exportDB2CSV(
"./data/SAMPLE_5.db",
"SAMPLE_5_V",
csvPath,
err);
exportTableToFile:导出表格static int exportTableToFile(
TTable& table,
const string& path,
bool overwrite = true);
参数:
table:要导出的 TTable。path:目标 CSV 路径,父目录必须已存在。overwrite:目标已存在时是否覆盖。返回值:
| 返回值 | 含义 |
|---|---|
0 |
已成功写出,或者 overwrite == false 且目标文件已存在而跳过 |
1 |
目标文件无法打开 |
CSV 同样带 UTF-8 BOM,并使用标准 CSV 引号转义。当前实现没有在每次写入后检查流状态,因此磁盘中途写满等错误仍可能返回 0。
示例:
TTable table("users");
table.addColumn("ID");
table.addColumn("NAME");
table.addColumn("REMARK");
std::vector<std::string> row1 = {"1", "张三", "一线,二线"};
std::vector<std::string> row2 = {"2", "李四", "他说\"已完成\""};
table.addRow(row1);
table.addRow(row2);
int rc = FileUtil::exportTableToFile(
table,
"./data/users.csv",
true);
if (rc != 0) {
std::cerr << "CSV 文件无法打开" << std::endl;
}
不覆盖已有文件:
int rc = FileUtil::exportTableToFile(
table,
"./data/users.csv",
false);
// rc == 0 也可能表示文件已存在,因此本次被跳过。
importTableFromFile:导入表格static int importTableFromFile(TTable& table, const string& path);
返回值:
| 返回值 | 含义 |
|---|---|
0 |
成功 |
1 |
路径不是普通文件或无法获取状态 |
2 |
文本读取失败 |
3 |
文件内容为空 |
导入成功时会调用 table.set(content),CSV 第一行成为列名,后续行成为数据。原表的列和行会被替换。
示例:
TTable imported("imported_users");
int rc = FileUtil::importTableFromFile(
imported,
"./data/users.csv");
if (rc == 0) {
std::cout << "行数: " << imported.size() << std::endl;
std::cout << "第一行姓名: " << imported.getValue(0, 1) << std::endl;
} else {
std::cerr << "导入失败,错误码: " << rc << std::endl;
}
当前限制:
TTable::set 的 CSV 解析能力有限,不适合字段内部包含真实换行的复杂 CSV。fileExistsstatic bool fileExists(const string& filePath);
该方法通过 ifstream 尝试打开路径。返回 true 表示文件可以作为输入流打开;不存在、没有读取权限或打开失败都返回 false。
if (FileUtil::fileExists("./data/config.ini")) {
std::cout << "配置文件可读" << std::endl;
}
如果需要严格区分文件、目录和其他对象,请使用 isPathOrFile。
pathExistsstatic bool pathExists(const string& path);
只有路径存在且类型是目录时才返回 true。
if (!FileUtil::pathExists("./data/export")) {
if (!FileUtil::createPath("./data/export")) {
std::cerr << "目录创建失败" << std::endl;
}
}
isPathOrFilestatic int isPathOrFile(const string& path);
| 返回值 | 类型 |
|---|---|
-1 |
获取状态失败,通常表示不存在或无权访问 |
0 |
普通文件 |
1 |
目录 |
2 |
其他类型,例如某些设备或特殊文件 |
switch (FileUtil::isPathOrFile("./data/item")) {
case 0:
std::cout << "普通文件" << std::endl;
break;
case 1:
std::cout << "目录" << std::endl;
break;
case 2:
std::cout << "其他文件系统对象" << std::endl;
break;
default:
std::cout << "路径不存在或无法访问" << std::endl;
break;
}
createTxtFilestatic bool createTxtFile(
const string& filePath,
const string& content,
int mode = 0);
mode:
0:覆盖已有文件。1:追加到已有文件末尾。the param mode can be 0 or 1.。如果文件不存在,mode 为 0 或 1 都会创建新文件。方法不会自动加换行,也不会创建父目录。
覆盖写入示例:
bool ok = FileUtil::createTxtFile(
"./data/config.txt",
"host=127.0.0.1\nport=8080\n",
0);
追加写入示例:
bool ok = FileUtil::createTxtFile(
"./data/app.log",
"启动完成\n",
1);
当前实现不会可靠检查所有 open/write 返回值。对配置、升级包等关键文件,建议写入后再调用 fileExists 或 readTxtFile 校验。
readTxtFilestatic bool readTxtFile(const string& filePath, string& res);
行为:
\n 重新连接。res,而是追加到原内容后面。推荐调用方式:
std::string content;
content.clear();
if (FileUtil::readTxtFile("./data/config.txt", content)) {
std::cout << content << std::endl;
} else {
std::cerr << "读取失败" << std::endl;
}
展示追加语义:
std::string content = "prefix:";
FileUtil::readTxtFile("./data/config.txt", content);
// content 以 "prefix:" 开头,随后直接接文件内容。
该方法是文本读取接口,不适合包含 \0 或需要保持原始换行字节的二进制文件。
createPathstatic bool createPath(const string& path);
成功创建目录返回 true。以下情况返回 false:
创建多层目录时要逐层创建:
if (!FileUtil::pathExists("./data")) {
FileUtil::createPath("./data");
}
if (!FileUtil::pathExists("./data/export")) {
if (!FileUtil::createPath("./data/export")) {
std::cerr << "创建 ./data/export 失败" << std::endl;
}
}
getPathContent:递归枚举static bool getPathContent(
const string& path,
vector<KV_Str_Str>& res);
返回的每个 KV_Str_Str:
k:文件名或目录名,不含父路径。v:其父目录路径。方法会把目录本身加入结果,然后继续递归其内容。结果顺序由文件系统枚举顺序决定,不保证排序。方法不会清空 res。
std::vector<KV_Str_Str> entries;
entries.clear();
if (FileUtil::getPathContent("./data", entries)) {
for (const KV_Str_Str& entry : entries) {
std::string fullPath = entry.v + "/" + entry.k;
int type = FileUtil::isPathOrFile(fullPath);
std::cout
<< (type == 1 ? "[DIR] " : "[FILE] ")
<< fullPath << std::endl;
}
}
如果任何一项的 stat 或子目录递归失败,方法会返回 false;此时 res 可能已经包含部分结果。
getPathFile:只枚举当前层static bool getPathFile(
const string& path,
vector<KV_Str_Str>& res);
尽管名字是 getPathFile,它实际返回当前目录中的文件和子目录,只是不进入子目录。k、v、顺序和追加行为与 getPathContent 相同。
std::vector<KV_Str_Str> entries;
if (!FileUtil::getPathFile("./data", entries)) {
std::cerr << "目录无法打开" << std::endl;
return;
}
for (const KV_Str_Str& entry : entries) {
std::string fullPath = entry.v + "/" + entry.k;
std::cout << fullPath << std::endl;
}
如果只想要普通文件,需要再用 isPathOrFile(fullPath) == 0 过滤。
removeFilestatic bool removeFile(const string& filePath);
只适合删除普通文件。文件不存在或删除失败时返回 false。
const std::string file = "./data/old.log";
if (FileUtil::fileExists(file)) {
if (!FileUtil::removeFile(file)) {
std::cerr << "删除失败: " << file << std::endl;
}
}
removeFilesstatic void removeFiles(const string& dir, const string& reg);
该方法只扫描 dir 当前层,并对每个名称执行 POSIX extended 风格的完整正则匹配。匹配后调用 std::remove。它不递归,没有返回值,打开目录或删除失败时调用方无法直接得知。
删除所有 .tmp 文件:
FileUtil::removeFiles("./data/cache", ".*\\.tmp");
删除名称形如 page-000001.png 的图片:
FileUtil::removeFiles(
"./data/pdf",
"page\\-[0-9]{6}\\.png");
正则是对完整文件名匹配,不是子串搜索。需要匹配任意前缀时要显式写 .*。
removePathstatic bool removePath(const string& path);
递归删除目录下的所有文件和子目录,最后删除 path 本身。路径不存在、不是可打开目录或任一步失败时返回 false。
const std::string tempDir = "./data/tmp_export";
if (FileUtil::pathExists(tempDir)) {
if (!FileUtil::removePath(tempDir)) {
std::cerr << "递归删除失败" << std::endl;
}
}
安全要求:不要对根目录、挂载点、工作目录或未经规范化的用户输入调用本方法。实现使用 stat 跟随符号链接,在包含符号链接的目录树上存在越过预期边界的风险。
clearPathstatic bool clearPath(const string& path);
实现步骤是:
removePath(path) 删除目录及全部内容。createPath(path) 重新创建空目录。因此目录必须事先存在;不存在时会返回 false,不会直接创建。
const std::string cacheDir = "./data/cache";
if (FileUtil::pathExists(cacheDir)) {
if (!FileUtil::clearPath(cacheDir)) {
std::cerr << "清空缓存目录失败" << std::endl;
}
}
copyFile:复制到目录static bool copyFile(
const string& srcfile,
const string& dstdir);
当前实际行为是把源文件复制到目标目录,并保留原文件名:
./data/a.txt + ./backup -> ./backup/a.txt
头文件注释中“新文件名为时间戳后 8 位加原文件名”的描述与当前实现不一致,当前实现不会添加时间戳。
if (!FileUtil::pathExists("./backup")) {
FileUtil::createPath("./backup");
}
bool ok = FileUtil::copyFile(
"./data/report.csv",
"./backup");
if (!ok) {
std::cerr << "复制失败" << std::endl;
}
注意事项:
/ 提取,因此即使在 Windows 模拟器上也应使用 /,不要传 D:\\data\\a.txt。copyFile2:复制为指定文件static bool copyFile2(
const string& srcfile,
const string& dstfile);
与 copyFile 不同,第二个参数是完整目标文件路径,可同时改名。
bool ok = FileUtil::copyFile2(
"./data/report.csv",
"./backup/report-previous.csv");
if (!ok) {
std::cerr
<< stt::util::DEBUG::getLastErrorString()
<< std::endl;
}
父目录必须存在;目标文件已存在时会覆盖。该接口不会执行 ARM 可用空间预检查。
copyPath:递归复制目录内容static bool copyPath(
const string& src,
const string& target);
复制的是 src 目录中的内容,不是在 target 下再创建一层 src 目录。目标根目录必须已存在;源目录里的子目录会按需创建。
const std::string src = "./data/project";
const std::string dst = "./backup/project";
if (!FileUtil::pathExists(dst)) {
FileUtil::createPath(dst); // 父目录 ./backup 必须已存在
}
if (!FileUtil::copyPath(src, dst)) {
std::cerr << "目录复制失败" << std::endl;
}
注意事项:
opendir(src) 是否成功;源目录无效时可能发生崩溃。因此调用前必须先确认 pathExists(src)。更稳妥的调用:
bool safeCopyPath(const std::string& src, const std::string& dst) {
if (!FileUtil::pathExists(src)) {
return false;
}
if (!FileUtil::pathExists(dst)) {
return false;
}
return FileUtil::copyPath(src, dst);
}
static int getPathSpace(
const string& path,
float& total,
float& free);
输出参数单位均为 MB:
total:路径所在文件系统/磁盘的总空间。free:普通调用方可用的空间。成功返回 0,失败返回 1。失败时两个输出参数均保持为 0。
float totalMB = 0.0F;
float freeMB = 0.0F;
int rc = FileUtil::getPathSpace(
"./data",
totalMB,
freeMB);
if (rc == 0) {
std::cout << "总空间: " << totalMB << " MB\n";
std::cout << "可用空间: " << freeMB << " MB\n";
} else {
std::cerr << "无法获取磁盘空间" << std::endl;
}
接口使用 float,大容量磁盘的数值精度有限。path 应是已经存在且可访问的目录。
bool writeAndVerifyConfig(const std::string& path) {
const std::string content =
"server=192.168.1.10\n"
"port=9000\n";
if (!FileUtil::createTxtFile(path, content, 0)) {
return false;
}
std::string actual;
if (!FileUtil::readTxtFile(path, actual)) {
return false;
}
// readTxtFile 不保留文件末尾的换行,因此比较时去掉预期末尾换行。
return actual == "server=192.168.1.10\nport=9000";
}
bool exportWithPrecheck(TTable& table, const std::string& path) {
std::size_t slash = path.find_last_of('/');
if (slash == std::string::npos) {
return false;
}
std::string dir = path.substr(0, slash);
if (!FileUtil::pathExists(dir)) {
return false;
}
float totalMB = 0.0F;
float freeMB = 0.0F;
if (FileUtil::getPathSpace(dir, totalMB, freeMB) != 0) {
return false;
}
if (freeMB < 10.0F) {
return false;
}
return FileUtil::exportTableToFile(table, path, true) == 0;
}
void printDirectory(const std::string& dir) {
std::vector<KV_Str_Str> entries;
if (!FileUtil::getPathFile(dir, entries)) {
std::cerr << "无法打开目录: " << dir << std::endl;
return;
}
for (const KV_Str_Str& entry : entries) {
const std::string fullPath = entry.v + "/" + entry.k;
const int type = FileUtil::isPathOrFile(fullPath);
if (type == 0) {
std::cout << "文件: " << fullPath << std::endl;
} else if (type == 1) {
std::cout << "目录: " << fullPath << std::endl;
} else {
std::cout << "其他: " << fullPath << std::endl;
}
}
}
PDFUtil 使用 Xpdf 命令行工具把 PDF 指定页转换成 PNG,再将 PNG 设置为 STTPanel 的背景图。
需要的外部文件至少包括:
pdftopng.exe、pdffonts.exe、pdfinfo.exe。.exe 的可执行文件。xpdfrc 配置文件(updateXpdfCfg 会覆盖生成)。xpdf-t1fonts 和 chinese-simplified 字体相关目录。工具目录、PDF 路径和 PNG 前缀最好使用绝对路径、/ 分隔符且不包含空格。当前实现直接拼接外部命令,没有对路径加引号;带空格路径会执行失败,不可信路径还可能带来命令注入风险。
PDFUtil(
const string& xpdfdir,
const string& filepath,
const string& prefix,
STTPanel* panel);
~PDFUtil();
参数:
xpdfdir:Xpdf 工具目录。filepath:要显示的 PDF 文件。prefix:临时 PNG 的路径及文件名前缀,例如 D:/pdfcache/page。panel:用于显示页面的 STTPanel*,必须非空并且生命周期长于 PDFUtil。构造时会调用 getPageCount 获取总页数,并把当前页初始化为第 1 页。构造函数不会自动显示第一页,通常要紧接着调用 first()。
void showManualPdf(STTPanel* panel) {
static PDFUtil pdf(
"D:/pdftool/xpdf",
"D:/manual/user-guide.pdf",
"D:/pdfcache/page",
panel);
pdf.first();
}
示例使用 static 只是为了确保示例函数返回后对象仍存在。实际工程中更适合把 PDFUtil 保存为页面或控制器成员。析构函数当前不删除临时 PNG,也不拥有/销毁 panel。
gotoPagevoid gotoPage(int ipage);
执行流程:获取该页字体、重写 xpdfrc、删除同前缀旧 PNG、转换指定页、把生成路径设置为面板背景。
PDFUtil pdf(toolDir, pdfPath, pngPrefix, panel);
pdf.gotoPage(12);
当前实现不会检查 ipage 是否位于 [1, 总页数],调用方应自行校验。prefix 必须包含 /,以便实现拆出临时文件所在目录。
first 与 lastvoid first();
void last();
pdf.first(); // 显示第 1 页
pdf.last(); // 显示构造时检测到的最后一页
若 getPageCount 返回 0,last() 会尝试跳转到第 0 页,因此初始化后应先通过静态 getPageCount 验证 PDF。
back 与 nextvoid back();
void next();
pdf.back(); // 当前页大于 1 时显示上一页,否则不操作
pdf.next(); // 当前页小于总页数时显示下一页,否则不操作
rotatevoid rotate();
每次调用顺时针增加 90 度,在 0、90、180、270 之间循环;同时交换面板宽高并重新生成当前页。
pdf.rotate(); // 0 -> 90 度
pdf.rotate(); // 90 -> 180 度
zoomvoid zoom(float scale);
scale 是相对当前面板尺寸的倍率,不是绝对倍率。例如连续调用两次 1.5F,面板边长累计变为原来的 2.25 倍。
pdf.zoom(1.25F); // 放大 25%
pdf.zoom(0.8F); // 在当前尺寸基础上缩小到 80%
放大后按 宽 * 高 * 4 估算的内存达到或超过 4 MiB 时,本次缩放被拒绝。当前实现没有验证 scale > 0,因此调用方必须保证倍率为正数。
class PdfController {
public:
explicit PdfController(STTPanel* panel)
: m_pdf(
"D:/pdftool/xpdf",
"D:/manual/user-guide.pdf",
"D:/pdfcache/manual-page",
panel) {
}
void open() { m_pdf.first(); }
void previous() { m_pdf.back(); }
void following() { m_pdf.next(); }
void lastPage() { m_pdf.last(); }
void rotate90() { m_pdf.rotate(); }
void zoomIn() { m_pdf.zoom(1.25F); }
void zoomOut() { m_pdf.zoom(0.8F); }
private:
PDFUtil m_pdf;
};
这些接口在头文件中标记为 @HideCA,仍然可以从 C++ 代码直接调用。
toPngstatic string toPng(
const string& tooldir,
int ipage,
const string& pdfpath,
const string& prefix,
int rot = 0);
把 PDF 的指定页转换为 PNG。成功时的目标名称格式为:
<prefix>-<6 位页码>.png
示例:
std::string png = PDFUtil::toPng(
"D:/pdftool/xpdf",
12,
"D:/manual/user-guide.pdf",
"D:/pdfcache/page",
90);
// 预期路径:D:/pdfcache/page-000012.png
if (png.empty() || !FileUtil::fileExists(png)) {
std::cerr << "PDF 转 PNG 失败" << std::endl;
}
仅当 PDF 或 pdftopng 不存在时方法明确返回空字符串。执行外部命令后,它不会检查退出码,而是直接返回预期文件名,所以调用方必须再用 fileExists 验证输出。
getFontNameAndTypestatic vector<KV_Str_Str> getFontNameAndType(
const string& tooldir,
int ipage,
const string& pdfpath);
调用 pdffonts 获取某页字体。每项的 k 是字体名称,v 是字体类型。
std::vector<KV_Str_Str> fonts = PDFUtil::getFontNameAndType(
"D:/pdftool/xpdf",
1,
"D:/manual/user-guide.pdf");
for (const KV_Str_Str& font : fonts) {
std::cout << font.k << " -> " << font.v << std::endl;
}
PDF 或工具不存在、命令输出为空或格式无法识别时返回空列表。当前解析依赖 pdffonts 的固定列宽输出格式。
updateXpdfCfgstatic bool updateXpdfCfg(
const string& tooldir,
vector<KV_Str_Str> vfont);
根据字体列表生成并覆盖 <tooldir>/xpdfrc。其中 TrueType 字体会映射到系统指定的文泉驿微米黑字体。
std::vector<KV_Str_Str> fonts = PDFUtil::getFontNameAndType(
toolDir,
1,
pdfPath);
if (!PDFUtil::updateXpdfCfg(toolDir, fonts)) {
std::cerr << "xpdfrc 更新失败" << std::endl;
}
这是覆盖写操作,会丢弃 xpdfrc 原有自定义配置。需要保留自定义内容时,应在调用前备份。
getInfostatic vector<KV_Str_Str> getInfo(
const string& tooldir,
const string& pdfpath);
调用 pdfinfo 并把每项解析为键值对。常见键包括 Title、Author、Pages、Page size、File size 和 PDF version。
std::vector<KV_Str_Str> info = PDFUtil::getInfo(
"D:/pdftool/xpdf",
"D:/manual/user-guide.pdf");
for (const KV_Str_Str& item : info) {
std::cout << item.k << ": " << item.v << std::endl;
}
PDF 或工具不存在时返回空列表。解析依赖 pdfinfo 默认输出格式以及键名区域约 16 个字符的布局。
getPageCountstatic int getPageCount(
const string& tooldir,
const string& pdfpath);
内部调用 getInfo,查找键 Pages 并转换为整数。失败时返回 0。
int pages = PDFUtil::getPageCount(
"D:/pdftool/xpdf",
"D:/manual/user-guide.pdf");
if (pages <= 0) {
std::cerr << "无法获取 PDF 页数" << std::endl;
} else {
std::cout << "总页数: " << pages << std::endl;
}
copyFile 的头文件注释提到时间戳改名,但实现保留原文件名。removeFiles 的头文件注释写有“返回是否成功”,但真实签名为 void。getPathFile 不只返回文件,也返回当前层子目录。createPath 不能递归创建目录,且目录已存在时返回 false。readTxtFile 追加到输出字符串,不会先清空。exportDB2CSV 存在引用生命周期风险,修复前应避免用于生产流程。copyPath 未检查源目录打开结果,必须先调用 pathExists(src)。