代码介绍
1. 核心功能
这是一个专为 macOS 设计的字体文件收集工具,能够:
✅ 智能扫描系统字体目录(如 /Library/Fonts)和用户指定目录
✅ 自动过滤 .ttf/.otf/.ttc 等字体文件
✅ 安全跳过无权限访问的目录(兼容 SIP 保护机制)
✅ 冲突处理自动重命名同名字体文件(追加 _1, _2 等后缀)
✅ 完整日志记录所有跳过的目录及原因
2. 技术亮点
🔧 现代 C++17 标准实现,跨平台兼容
🔧 使用 库实现高效目录遍历
🔧 多线程安全的错误记录机制(std::mutex)
🔧 双重访问策略:
• 优先尝试直接递归访问
• 失败后自动降级为逐个子目录探测
3. 典型使用场景
📁 设计师工作流:快速收集散落在各处的商用字体
💻 开发者工具:为字体管理软件提供基础服务
🔐 系统维护:备份关键字体文件
4. 使用方法示例
FontCopier copier({".ttf", ".otf"}, "~/FontBackup");
copier.add_custom_font_dir("/", true); // 全盘扫描(自动跳过系统保护区)
copier.run();
5. 输出
示例
=== macOS 字体拷贝工具 (C++17) ===
搜索字体类型: .ttf .otf
目标文件夹: /Users/Alice/FontBackup
正在尝试访问目录: /Library/Fonts
跳过受保护目录: /System/Library/Fonts (Operation not permitted)
找到 142 个字体文件:
- 正在拷贝: /Library/Fonts/Arial.ttf -> ~/FontBackup/Arial_1.ttf
...
操作完成。成功拷贝 138/142 个字体文件
跳过的目录 (3个):
-
/System/Library/Fonts (Operation not permitted)
-
/private/var/db (Permission denied)
-
扩展建议
🛠 可扩展为 GUI 工具(Qt/QML 前端)
🛠 添加网络字体下载功能
🛠 集成字体预览能力
注意:运行时可能需要 sudo 权限访问系统目录,但会严格遵守 macOS 权限系统限制
代码主体
cpp
#include <iostream>
#include <vector>
#include <string>
#include <filesystem>
#include <algorithm>
#include <set>
#include <iomanip>
#include <mutex>
namespace fs = std::filesystem;
class FontCopier {
public:
FontCopier(const std::vector<std::string>& extensions, const fs::path& destination)
: target_extensions_(extensions.begin(), extensions.end()),
destination_(destination) {
// 初始化默认 macOS 字体目录
font_dirs_ = {
// "/Library/Fonts",
// "/System/Library/Fonts",
// home_dir() / "Library/Fonts",
// "/Network/Library/Fonts"
};
}
// 添加用户自定义目录(支持递归搜索)
void add_custom_font_dir(const fs::path& dir, bool recursive = true) {
if (fs::exists(dir)) {
custom_dirs_.emplace_back(dir, recursive);
} else {
std::cerr << "警告: 目录不存在 - " << dir << std::endl;
skipped_dirs_.push_back({dir, "Directory does not exist"});
}
}
void run() {
std::cout << "=== macOS 字体拷贝工具 (C++17) ===" << std::endl;
print_config();
auto font_files = find_font_files();
if (font_files.empty()) {
std::cout << "未找到匹配的字体文件\n";
return;
}
copy_files(font_files);
print_summary();
print_skipped_dirs();
}
const std::vector<std::pair<fs::path, std::string>>& get_skipped_dirs() const {
return skipped_dirs_;
}
private:
fs::path home_dir() const {
return fs::path(getenv("HOME"));
}
void print_config() const {
std::cout << "搜索字体类型: ";
for (const auto& ext : target_extensions_) {
std::cout << ext << " ";
}
std::cout << "\n目标文件夹: " << destination_ << "\n\n";
}
std::vector<fs::path> find_font_files() {
std::vector<fs::path> results;
std::mutex mtx;
// 搜索系统字体目录
for (const auto& dir : font_dirs_) {
search_directory(dir, true, results, mtx);
}
// 搜索用户自定义目录
for (const auto& [dir, recursive] : custom_dirs_) {
search_directory(dir, recursive, results, mtx);
}
return results;
}
void search_directory(const fs::path& dir, bool recursive,
std::vector<fs::path>& results, std::mutex& mtx) {
try {
if (!fs::exists(dir)) {
add_skipped_dir(dir, "Directory does not exist");
return;
}
std::cout << "正在尝试访问目录: " << dir << std::endl;
// 先尝试直接访问目录
try {
if (recursive) {
for (const auto& entry : fs::recursive_directory_iterator(
dir, fs::directory_options::skip_permission_denied)) {
process_entry(entry, results, mtx);
}
} else {
for (const auto& entry : fs::directory_iterator(
dir, fs::directory_options::skip_permission_denied)) {
process_entry(entry, results, mtx);
}
}
return; // 如果成功就直接返回
} catch (const fs::filesystem_error& e) {
// 如果直接访问失败,记录并继续尝试子目录
add_skipped_dir(dir, e.what());
std::cerr << "目录访问错误: " << dir << " (" << e.what() << ")\n";
}
// 特殊处理根目录或受限目录:尝试一级子目录
if (dir == "/" || dir == "/System" || dir == "/Library") {
std::cout << "尝试访问 " << dir << " 的子目录..." << std::endl;
try {
for (const auto& entry : fs::directory_iterator(
dir, fs::directory_options::skip_permission_denied)) {
if (fs::is_directory(entry.status())) {
try {
std::cout << "正在搜索子目录: " << entry.path() << std::endl;
if (recursive) {
for (const auto& sub_entry : fs::recursive_directory_iterator(
entry.path(), fs::directory_options::skip_permission_denied)) {
process_entry(sub_entry, results, mtx);
}
} else {
for (const auto& sub_entry : fs::directory_iterator(
entry.path(), fs::directory_options::skip_permission_denied)) {
process_entry(sub_entry, results, mtx);
}
}
} catch (const fs::filesystem_error& e) {
add_skipped_dir(entry.path(), e.what());
std::cerr << "子目录访问错误: " << entry.path() << " (" << e.what() << ")\n";
}
}
}
} catch (const fs::filesystem_error& e) {
add_skipped_dir(dir, e.what());
std::cerr << "无法列出目录内容: " << dir << " (" << e.what() << ")\n";
}
}
} catch (const std::exception& e) {
add_skipped_dir(dir, e.what());
std::cerr << "未知错误: " << e.what() << "\n";
}
}
void process_entry(const fs::directory_entry& entry,
std::vector<fs::path>& results, std::mutex& mtx) {
try {
if (is_target_file(entry.path())) {
std::lock_guard<std::mutex> lock(mtx);
results.push_back(entry.path());
}
} catch (const fs::filesystem_error& e) {
add_skipped_dir(entry.path(), e.what());
}
}
bool is_target_file(const fs::path& path) const {
try {
if (!fs::is_regular_file(path)) return false;
auto ext = path.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
return target_extensions_.count(ext) > 0;
} catch (...) {
return false;
}
}
void copy_files(const std::vector<fs::path>& files) {
fs::create_directories(destination_);
std::cout << "\n找到 " << files.size() << " 个字体文件:\n";
for (size_t i = 0; i < files.size(); ++i) {
const auto& src = files[i];
auto dest = destination_ / src.filename();
// 处理文件名冲突
int counter = 1;
while (fs::exists(dest)) {
auto new_name = src.stem().string() + "_" + std::to_string(counter++) + src.extension().string();
dest = destination_ / new_name;
}
std::cout << i + 1 << ". 正在拷贝: " << src << " -> " << dest << std::endl;
try {
fs::copy(src, dest, fs::copy_options::overwrite_existing);
++success_count_;
} catch (const fs::filesystem_error& e) {
std::cerr << "拷贝失败: " << src << " (" << e.what() << ")\n";
add_skipped_file(src, e.what());
}
}
}
void add_skipped_dir(const fs::path& dir, const std::string& reason) {
std::lock_guard<std::mutex> lock(skip_mutex_);
skipped_dirs_.emplace_back(dir, reason);
}
void add_skipped_file(const fs::path& file, const std::string& reason) {
std::lock_guard<std::mutex> lock(skip_mutex_);
skipped_files_.emplace_back(file, reason);
}
void print_summary() const {
std::cout << "\n操作完成。成功拷贝 " << success_count_
<< "/" << (success_count_ + skipped_files_.size())
<< " 个字体文件到: " << destination_ << std::endl;
}
void print_skipped_dirs() const {
if (!skipped_dirs_.empty()) {
std::cout << "\n跳过的目录 (" << skipped_dirs_.size() << " 个):\n";
for (size_t i = 0; i < skipped_dirs_.size(); ++i) {
std::cout << i + 1 << ". " << skipped_dirs_[i].first
<< " (原因: " << skipped_dirs_[i].second << ")\n";
}
}
if (!skipped_files_.empty()) {
std::cout << "\n跳过的文件 (" << skipped_files_.size() << " 个):\n";
for (size_t i = 0; i < skipped_files_.size(); ++i) {
std::cout << i + 1 << ". " << skipped_files_[i].first
<< " (原因: " << skipped_files_[i].second << ")\n";
}
}
}
private:
std::set<std::string> target_extensions_;
std::vector<fs::path> font_dirs_;
std::vector<std::pair<fs::path, bool>> custom_dirs_; // <目录路径, 是否递归>
fs::path destination_;
size_t success_count_ = 0;
// 跳过的目录和文件记录
std::vector<std::pair<fs::path, std::string>> skipped_dirs_;
std::vector<std::pair<fs::path, std::string>> skipped_files_;
mutable std::mutex skip_mutex_;
};
int main() {
// 配置参数
std::vector<std::string> extensions = {".ttf", ".otf", ".ttc"};
auto destination = fs::path("/Users/admin/Desktop/CopiedFonts");
// 创建拷贝工具实例
FontCopier copier(extensions, destination);
// 添加自定义目录(示例)
copier.add_custom_font_dir("/Library/Fonts");
copier.add_custom_font_dir("/System/Library/Fonts");
copier.add_custom_font_dir("~/Library/Fonts");
copier.add_custom_font_dir("/", true); // 递归搜索根目录
// 运行拷贝工具
copier.run();
return 0;
}
CMakeList.txt
cpp
cmake_minimum_required(VERSION 3.15) # 需要支持 C++17 filesystem
project(FontCopier LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# 设置输出目录
set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
# # 根据平台配置 filesystem 库
# if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
# set(FILESYSTEM_LIB stdc++fs)
# elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
# set(FILESYSTEM_LIB)
# endif()
# 可执行文件配置
add_executable(font_copier
${CMAKE_CURRENT_SOURCE_DIR}/collect_sysfont.cpp
# src/FontCopier.cpp
# include/FontCopier.h
)
target_include_directories(font_copier PRIVATE include)
# 链接系统库
target_link_libraries(font_copier PRIVATE ${FILESYSTEM_LIB})
# macOS 特定设置
if(APPLE)
find_library(CORESERVICES CoreServices)
target_link_libraries(font_copier PRIVATE ${CORESERVICES})
# 设置 macOS 部署目标版本
set_target_properties(font_copier PROPERTIES
MACOSX_RPATH ON
MACOSX_DEPLOYMENT_TARGET "10.15" # 支持 filesystem 的最低版本
)
endif()
# # 安装配置
# install(TARGETS font_copier
# RUNTIME DESTINATION bin
# BUNDLE DESTINATION bin
# )
# # 单元测试配置(可选)
# if(BUILD_TESTING)
# enable_testing()
# add_subdirectory(tests)
# endif()
# # 打包配置
# include(InstallRequiredSystemLibraries)
# set(CPACK_PACKAGE_NAME "FontCopier")
# set(CPACK_PACKAGE_VERSION "1.0.0")
# set(CPACK_PACKAGE_DESCRIPTION "Cross-platform font collector tool")
# include(CPack)