#include #include #include #include #include #include #include std::string join(const std::string &folder_path, const std::string &child_path) { std::string full_path = folder_path; if (!full_path.empty() && full_path.back() != '/') { full_path += '/'; } std::stringstream ss(child_path); std::string segment; while (std::getline(ss, segment, '/')) { if (segment == "..") { // Go up one level if (!full_path.empty()) { size_t lastSlash = full_path.rfind('/', full_path.length() - 2); if (lastSlash != std::string::npos) { full_path.resize(lastSlash + 1); } else { full_path = ""; } } } else if (segment != "." && !segment.empty()) { full_path += segment + '/'; } } if (full_path.length() > 1 && full_path.back() == '/') { full_path.pop_back(); } return full_path; } std::string getParentPath(const std::string &path) { if (path.empty()) return ""; size_t pos = path.find_last_of("/\\"); if (pos == std::string::npos) { return ""; } return path.substr(0, pos); } std::string find_toc_href(const pugi::xml_document &opf_doc) { auto manifest = opf_doc.child("package").child("manifest"); for (pugi::xml_node item : manifest.children("item")) { std::string media_type = item.attribute("media-type").value(); std::string id = item.attribute("id").value(); if (media_type == "application/x-dtbncx+xml" || id == "ncx" || id == "nav") { return item.attribute("href").value(); } } return ""; } void parse_navele_recursive(const pugi::xml_node &parent, std::unordered_map &href_to_label, std::string &nav_folder) { for (pugi::xml_node li : parent.children("li")) { pugi::xml_node a = li.child("a"); if (a) { std::string href = a.attribute("href").as_string(); std::string label = a.text().as_string(); size_t sharp = href.find('#'); if (sharp != std::string::npos) href = href.substr(0, sharp); if (!href.empty() && !label.empty()) href_to_label[join(nav_folder, href)] = label; } if (pugi::xml_node sublist = li.child("ol")) { parse_navele_recursive(sublist, href_to_label, nav_folder); } } } void parse_nav_xhtml(const std::string &nav_path, std::unordered_map &path_to_label) { std::string nav_folder = getParentPath(nav_path); pugi::xml_document nav_doc; if (!nav_doc.load_file(nav_path.c_str())) return; for (pugi::xpath_node nav : nav_doc.select_nodes("//nav")) { pugi::xml_node node = nav.node(); std::string nav_type = node.attribute("epub:type").as_string(); pugi::xml_node ol = node.child("ol"); if (ol) { parse_navele_recursive(ol, path_to_label, nav_folder); } } } void parse_navpoint_recursive(const pugi::xml_node &navPoint, std::unordered_map &result, std::string &ncx_folder) { for (pugi::xml_node point : navPoint.children("navPoint")) { std::string label; pugi::xml_node labelNode = point.child("navLabel").child("text"); if (labelNode) label = labelNode.text().as_string(); std::string src; pugi::xml_node contentNode = point.child("content"); if (contentNode) src = contentNode.attribute("src").as_string(); if (!label.empty() && !src.empty()) { size_t sharp = src.find('#'); if (sharp != std::string::npos) src = src.substr(0, sharp); result[join(ncx_folder, src)] = label; } parse_navpoint_recursive(point, result, ncx_folder); } } void parse_toc_ncx(const std::string &ncx_path, std::unordered_map &href_to_label) { std::string ncx_folder = getParentPath(ncx_path); pugi::xml_document doc; if (!doc.load_file(ncx_path.c_str())) return; pugi::xml_node navMap = doc.child("ncx").child("navMap"); if (!navMap) return; parse_navpoint_recursive(navMap, href_to_label, ncx_folder); } void parse_opf_from_folder(const std::string &base_dir, const std::string &opf_rel_path, EpubMetadata &meta_out) { std::string opf_path = join(base_dir, opf_rel_path); std::string opf_dir = getParentPath(opf_path); pugi::xml_document opf_doc; if (!opf_doc.load_file(opf_path.c_str())) return; std::string version; pugi::xml_node package = opf_doc.child("package"); if (package) { version = package.attribute("version").as_string(); } std::unordered_map path_to_label; std::string toc_href = find_toc_href(opf_doc); if (toc_href.find("ncx") != std::string::npos) { std::string ncx_path = join(opf_dir, toc_href); parse_toc_ncx(ncx_path, path_to_label); } else { std::string nav_path = join(opf_dir, toc_href); parse_nav_xhtml(nav_path, path_to_label); } auto metadata = opf_doc.child("package").child("metadata"); meta_out.name = metadata.child("dc:title").text().as_string(); meta_out.author = metadata.child("dc:creator").text().as_string(); meta_out.artist = metadata.child("dc:contributor").text().as_string(); meta_out.summary = metadata.child("dc:description").text().as_string(); std::unordered_map id_to_href; std::string cover_id; for (pugi::xml_node meta : metadata.children("meta")) { if (std::string(meta.attribute("name").value()) == "cover") { cover_id = meta.attribute("content").value(); break; } } auto manifest = opf_doc.child("package").child("manifest"); for (pugi::xml_node item : manifest.children("item")) { std::string id = item.attribute("id").value(); std::string href = item.attribute("href").value(); std::string media_type = item.attribute("media-type").value(); id_to_href[id] = href; if (media_type == "text/css") { meta_out.cssPaths.push_back(join(opf_dir, href)); } else if (media_type == "image/jpeg" || media_type == "image/png" || media_type == "image/jpg") { meta_out.imagePaths.push_back(join(opf_dir, href)); } } if (!cover_id.empty() && id_to_href.count(cover_id)) { meta_out.cover = join(opf_dir, id_to_href[cover_id]); } auto spine = opf_doc.child("package").child("spine"); std::string prev_name = ""; int part = 2; for (pugi::xml_node itemref : spine.children("itemref")) { std::string idref = itemref.attribute("idref").value(); if (id_to_href.count(idref)) { std::string chapter_href = id_to_href[idref]; Chapter chapter; chapter.path = join(opf_dir, chapter_href); if (path_to_label.count(chapter.path)) { chapter.name = path_to_label[chapter.path]; } if (chapter.name.empty()) { if (prev_name.empty()) { int start_pos = chapter_href.find_last_of("/"); if (start_pos == std::string::npos) { start_pos = 0; } else { start_pos++; } chapter.name = chapter_href.substr(start_pos, chapter_href.find_last_of(".") - start_pos); } else { chapter.name = prev_name + " (" + std::to_string(part) + ")"; part += 1; } } else { prev_name = chapter.name; part = 2; } meta_out.chapters.push_back(chapter); } } } EpubMetadata parseEpub(const std::string epub_path) { std::string container_path = join(epub_path, "META-INF/container.xml"); pugi::xml_document container_doc; if (!container_doc.load_file(container_path.c_str())) throw std::runtime_error("Failed to load container.xml"); std::string opf_path = container_doc.child("container") .child("rootfiles") .child("rootfile") .attribute("full-path") .value(); EpubMetadata metadata; parse_opf_from_folder(epub_path, opf_path, metadata); return metadata; }