如何在Swift中打包目录生成ePub且不压缩mimetype文件?
问题分析与解决方案
你的推测完全正确——NSFileCoordinator的.forUploading选项会对所有文件进行压缩,无法单独排除mimetype文件,这就是Apple Books无法识别你生成的epub的核心原因。ePub规范明确要求:mimetype必须是zip包中的第一个文件条目,且不能被压缩,否则阅读器会判定文件无效。
下面提供两种可行的Swift解决方案,均能满足epub的格式要求:
方案1:使用系统Archive API(macOS 11+/iOS 14+)
直接利用Foundation框架的Archive类手动构建zip包,可精确控制每个文件的压缩策略:
import Foundation func createEPUB(from sourceDirURL: URL, to destinationURL: URL) throws { // 清理已存在的目标文件 if FileManager.default.fileExists(atPath: destinationURL.path) { try FileManager.default.removeItem(at: destinationURL) } // 创建用于写入的Archive实例 guard let archive = Archive(url: destinationURL, accessMode: .create) else { throw NSError(domain: "EPUBBuilder", code: 1, userInfo: [NSLocalizedDescriptionKey: "无法创建zip归档"]) } // 1. 优先添加mimetype,设置为无压缩 let mimetypeURL = sourceDirURL.appendingPathComponent("mimetype") guard let mimetypeData = try? Data(contentsOf: mimetypeURL) else { throw NSError(domain: "EPUBBuilder", code: 2, userInfo: [NSLocalizedDescriptionKey: "未找到mimetype文件"]) } try archive.addEntry(withPath: "mimetype", data: mimetypeData, compressionMethod: .none) // 2. 遍历目录中其余文件,按默认压缩添加 let fileEnumerator = FileManager.default.enumerator(at: sourceDirURL, includingPropertiesForKeys: [.isDirectoryKey])! for case let fileURL as URL in fileEnumerator { let relativePath = fileURL.relativePath(from: sourceDirURL) guard relativePath != "mimetype" else { continue } // 跳过已添加的mimetype if fileURL.hasDirectoryPath { // 添加目录条目 try archive.addEntry(withPath: relativePath + "/", type: .directory) } else { // 添加文件,使用默认压缩级别 let fileData = try Data(contentsOf: fileURL) try archive.addEntry(withPath: relativePath, data: fileData) } } } // 扩展URL获取相对路径 extension URL { func relativePath(from baseURL: URL) -> String { guard let relativeURL = self.relativePath(to: baseURL) else { return self.lastPathComponent } return relativeURL.path } }
方案2:使用第三方库ZipArchive(兼容旧版本系统)
如果需要支持macOS 11以下版本,ZipArchive是成熟的选择,可直接控制单个文件的压缩级别:
- 通过Swift Package Manager或CocoaPods引入ZipArchive
- 使用以下代码构建epub:
import ZipArchive func createEPUBWithZipArchive(from sourceDirURL: URL, to destinationURL: URL) throws { // 清理已存在的目标文件 if FileManager.default.fileExists(atPath: destinationURL.path) { try FileManager.default.removeItem(at: destinationURL) } // 先创建基础zip包 guard !SSZipArchive.createZipFile(atPath: destinationURL.path, withContentsOfDirectory: sourceDirURL.path) else { throw NSError(domain: "EPUBBuilder", code: 3, userInfo: [NSLocalizedDescriptionKey: "初始zip创建失败"]) } // 打开zip包,替换mimetype为无压缩版本 guard SSZipArchive.unzipOpenFile(destinationURL.path) else { throw NSError(domain: "EPUBBuilder", code: 4, userInfo: [NSLocalizedDescriptionKey: "无法打开zip文件"]) } let mimetypeURL = sourceDirURL.appendingPathComponent("mimetype") let mimetypeData = try Data(contentsOf: mimetypeURL) SSZipArchive.unzipAddData(mimetypeData, fileName: "mimetype", compressionLevel: 0) // 0代表无压缩 SSZipArchive.unzipCloseFile() }
两种方案都能确保mimetype文件以无压缩状态作为zip的第一个条目,完全符合ePub规范,生成的文件可正常被Apple Books识别。
内容的提问来源于stack exchange,提问作者KevinF
相关产品推荐
相关产品推荐

