10b6042a9c
Closes #799
110 lines
3.7 KiB
C++
110 lines
3.7 KiB
C++
#include "EpubExporter.hpp"
|
|
|
|
#include "EpubAsset.hpp"
|
|
#include "EpubContentProcessor.hpp"
|
|
#include "EpubDocuments.hpp"
|
|
#include "ZipWriter.hpp"
|
|
|
|
#include <filesystem>
|
|
#include <stdexcept>
|
|
#include <unordered_map>
|
|
|
|
namespace margelo::nitro::nitroepub {
|
|
|
|
EpubArchiveResult exportEpubArchive(
|
|
const EpubArchiveMetadata& metadata,
|
|
const std::vector<EpubArchiveChapter>& chapters,
|
|
const std::string& outputPath,
|
|
std::function<void(std::size_t, std::size_t, const std::string&)>
|
|
onProgress) {
|
|
if (metadata.title.empty()) {
|
|
throw std::runtime_error("EPUB title cannot be empty");
|
|
}
|
|
if (outputPath.empty()) {
|
|
throw std::runtime_error("EPUB output path cannot be empty");
|
|
}
|
|
|
|
std::vector<EpubArchiveChapter> availableChapters;
|
|
availableChapters.reserve(chapters.size());
|
|
for (const EpubArchiveChapter& chapter : chapters) {
|
|
if (std::filesystem::is_regular_file(chapter.htmlPath)) {
|
|
availableChapters.push_back(chapter);
|
|
}
|
|
}
|
|
if (availableChapters.empty()) {
|
|
throw std::runtime_error("EPUB requires at least one downloaded chapter");
|
|
}
|
|
|
|
const std::filesystem::path finalPath(outputPath);
|
|
const std::filesystem::path temporaryPath = finalPath.string() + ".tmp";
|
|
if (!finalPath.parent_path().empty()) {
|
|
std::filesystem::create_directories(finalPath.parent_path());
|
|
}
|
|
std::error_code ignored;
|
|
std::filesystem::remove(temporaryPath, ignored);
|
|
|
|
try {
|
|
ZipWriter zip(temporaryPath);
|
|
|
|
// EPUB requires this exact file to be the first entry and uncompressed.
|
|
zip.addStored("mimetype", "application/epub+zip");
|
|
zip.addDeflated("META-INF/container.xml", containerDocument());
|
|
zip.addDeflated("EPUB/styles.css", metadata.stylesheet);
|
|
|
|
const bool hasJavaScript = !metadata.javascript.empty();
|
|
if (hasJavaScript) {
|
|
zip.addDeflated("EPUB/script.js",
|
|
"function fnEpub(){\n" + metadata.javascript + "\n}\n");
|
|
}
|
|
|
|
std::vector<EpubAsset> assets;
|
|
std::unordered_map<std::string, EpubAsset> knownAssets;
|
|
EpubAsset cover;
|
|
EpubAsset* coverPointer = nullptr;
|
|
const std::filesystem::path coverPath =
|
|
localPathFromUri(metadata.coverPath);
|
|
if (!coverPath.empty() && std::filesystem::is_regular_file(coverPath)) {
|
|
const std::string extension = normalizedImageExtension(coverPath);
|
|
cover = EpubAsset{
|
|
"cover-image",
|
|
"EPUB/images/cover" + extension,
|
|
imageMediaType(extension),
|
|
};
|
|
zip.addFile(cover.archivePath, coverPath);
|
|
coverPointer = &cover;
|
|
}
|
|
|
|
for (std::size_t index = 0; index < availableChapters.size(); ++index) {
|
|
const std::string body = prepareChapterBody(
|
|
availableChapters[index], zip, knownAssets, assets);
|
|
zip.addDeflated(
|
|
"EPUB/text/chapter-" + std::to_string(index) + ".xhtml",
|
|
chapterDocument(availableChapters[index], body, hasJavaScript));
|
|
if (onProgress) {
|
|
onProgress(index + 1,
|
|
availableChapters.size(),
|
|
availableChapters[index].title);
|
|
}
|
|
}
|
|
|
|
zip.addDeflated(
|
|
"EPUB/nav.xhtml",
|
|
navigationDocument(metadata, availableChapters));
|
|
zip.addDeflated("EPUB/toc.ncx", ncxDocument(metadata, availableChapters));
|
|
zip.addDeflated(
|
|
"EPUB/content.opf",
|
|
packageDocument(metadata, availableChapters, assets, coverPointer,
|
|
hasJavaScript));
|
|
zip.finish();
|
|
|
|
std::filesystem::remove(finalPath, ignored);
|
|
std::filesystem::rename(temporaryPath, finalPath);
|
|
return EpubArchiveResult{outputPath, availableChapters.size()};
|
|
} catch (...) {
|
|
std::filesystem::remove(temporaryPath, ignored);
|
|
throw;
|
|
}
|
|
}
|
|
|
|
} // namespace margelo::nitro::nitroepub
|