package me.ag2s.epublib.epub; import android.util.Log; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import me.ag2s.epublib.Constants; import me.ag2s.epublib.domain.EpubBook; import me.ag2s.epublib.domain.Guide; import me.ag2s.epublib.domain.GuideReference; import me.ag2s.epublib.domain.MediaType; import me.ag2s.epublib.domain.MediaTypes; import me.ag2s.epublib.domain.Resource; import me.ag2s.epublib.domain.Resources; import me.ag2s.epublib.domain.Spine; import me.ag2s.epublib.domain.SpineReference; import me.ag2s.epublib.util.ResourceUtil; import me.ag2s.epublib.util.StringUtil; /** * Reads the opf package document as defined by namespace http://www.idpf.org/2007/opf * * @author paul */ public class PackageDocumentReader extends PackageDocumentBase { private static final String TAG = PackageDocumentReader.class.getName(); private static final String[] POSSIBLE_NCX_ITEM_IDS = new String[]{"toc", "ncx", "ncxtoc", "htmltoc"}; public static void read( Resource packageResource, EpubReader epubReader, EpubBook book, Resources resources) throws SAXException, IOException { Document packageDocument = ResourceUtil.getAsDocument(packageResource); String packageHref = packageResource.getHref(); resources = fixHrefs(packageHref, resources); readGuide(packageDocument, epubReader, book, resources); // Books sometimes use non-identifier ids. We map these here to legal ones Map idMapping = new HashMap<>(); String version = DOMUtil.getAttribute(packageDocument.getDocumentElement(), PREFIX_OPF, PackageDocumentBase.version); resources = readManifest(packageDocument, packageHref, epubReader, resources, idMapping); book.setResources(resources); book.setVersion(version); readCover(packageDocument, book); book.setMetadata( PackageDocumentMetadataReader.readMetadata(packageDocument)); book.setSpine(readSpine(packageDocument, book.getResources(), idMapping)); // if we did not find a cover page then we make the first page of the book the cover page if (book.getCoverPage() == null && book.getSpine().size() > 0) { book.setCoverPage(book.getSpine().getResource(0)); } } /** * * 修复一些非标准epub格式由于 opf 文件内容不全而读取不到图片的问题 * @author qianfanguojin * @return ,修复图片路径后的一个Element列表 */ private static ArrayList ensureImageInfo(Resources resources, Element manifestElement){ ArrayList fixedElements = new ArrayList<>(); //加入当前所有的 item 标签 NodeList originItemElements = manifestElement .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.item); for (int i = 0; i < originItemElements.getLength(); i++) { fixedElements.add((Element) originItemElements.item(i)); } //如果有图片资源未定义在 originItemElements ,则加入该图片信息得到 fixedElements 中 for (Resource resource : resources.getAll()) { MediaType currentMediaType = resource.getMediaType(); if ( currentMediaType == MediaTypes.JPG || currentMediaType == MediaTypes.PNG){ String imageHref = resource.getHref(); //确保该图片信息 resource 在原 originItemElements 列表中没有出现过 boolean flag = false; int i; for (i = 0; i < originItemElements.getLength(); i++){ Element itemElement = (Element) originItemElements.item(i); String href = DOMUtil .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.href); try { href = URLDecoder.decode(href, Constants.CHARACTER_ENCODING); } catch (UnsupportedEncodingException e) { Log.e(TAG, e.getMessage()); } if (href.equals(imageHref)){ break; } } if (i == originItemElements.getLength()){ flag = true; } if (flag){ //由于暂时无法实例化一个Element,则选择克隆一个已存在的节点来修改以达到新增 Element 的效果,作为临时解决方案 Element tempElement = (Element) manifestElement.getElementsByTagNameNS(NAMESPACE_OPF,OPFTags.item).item(0).cloneNode(true); tempElement.setAttribute("id",imageHref.replace("/","")); tempElement.setAttribute("href",imageHref); tempElement.setAttribute("media-type", currentMediaType.getName()); fixedElements.add(tempElement); } } } return fixedElements; } /** * Reads the manifest containing the resource ids, hrefs and mediatypes. * * @param packageDocument e * @param packageHref e * @param epubReader e * @param resources e * @param idMapping e * @return a Map with resources, with their id's as key. */ @SuppressWarnings("unused") private static Resources readManifest(Document packageDocument, String packageHref, EpubReader epubReader, Resources resources, Map idMapping) { Element manifestElement = DOMUtil .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), NAMESPACE_OPF, OPFTags.manifest); Resources result = new Resources(); if (manifestElement == null) { Log.e(TAG, "Package document does not contain element " + OPFTags.manifest); return result; } List ensuredElements = ensureImageInfo(resources,manifestElement); for (Element itemElement : ensuredElements) { // Element itemElement = ; String id = DOMUtil .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.id); String href = DOMUtil .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.href); try { href = URLDecoder.decode(href, Constants.CHARACTER_ENCODING); } catch (UnsupportedEncodingException e) { Log.e(TAG, e.getMessage()); } String mediaTypeName = DOMUtil .getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.media_type); Resource resource = resources.remove(href); if (resource == null) { Log.e(TAG, "resource with href '" + href + "' not found"); continue; } resource.setId(id); //for epub3 String properties = DOMUtil.getAttribute(itemElement, NAMESPACE_OPF, OPFAttributes.properties); resource.setProperties(properties); MediaType mediaType = MediaTypes.getMediaTypeByName(mediaTypeName); if (mediaType != null) { resource.setMediaType(mediaType); } result.add(resource); idMapping.put(id, resource.getId()); } return result; } /** * Reads the book's guide. * Here some more attempts are made at finding the cover page. * * @param packageDocument r * @param epubReader r * @param book r * @param resources g */ @SuppressWarnings("unused") private static void readGuide(Document packageDocument, EpubReader epubReader, EpubBook book, Resources resources) { Element guideElement = DOMUtil .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), NAMESPACE_OPF, OPFTags.guide); if (guideElement == null) { return; } Guide guide = book.getGuide(); NodeList guideReferences = guideElement .getElementsByTagNameNS(NAMESPACE_OPF, OPFTags.reference); for (int i = 0; i < guideReferences.getLength(); i++) { Element referenceElement = (Element) guideReferences.item(i); String resourceHref = DOMUtil .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.href); if (StringUtil.isBlank(resourceHref)) { continue; } Resource resource = resources.getByHref(StringUtil .substringBefore(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR)); if (resource == null) { Log.e(TAG, "Guide is referencing resource with href " + resourceHref + " which could not be found"); continue; } String type = DOMUtil .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.type); if (StringUtil.isBlank(type)) { Log.e(TAG, "Guide is referencing resource with href " + resourceHref + " which is missing the 'type' attribute"); continue; } String title = DOMUtil .getAttribute(referenceElement, NAMESPACE_OPF, OPFAttributes.title); if (GuideReference.COVER.equalsIgnoreCase(type)) { continue; // cover is handled elsewhere } GuideReference reference = new GuideReference(resource, type, title, StringUtil .substringAfter(resourceHref, Constants.FRAGMENT_SEPARATOR_CHAR)); guide.addReference(reference); } } /** * Strips off the package prefixes up to the href of the packageHref. *

* Example: * If the packageHref is "OEBPS/content.opf" then a resource href like "OEBPS/foo/bar.html" will be turned into "foo/bar.html" * * @param packageHref f * @param resourcesByHref g * @return The stripped package href */ static Resources fixHrefs(String packageHref, Resources resourcesByHref) { int lastSlashPos = packageHref.lastIndexOf('/'); if (lastSlashPos < 0) { return resourcesByHref; } Resources result = new Resources(); for (Resource resource : resourcesByHref.getAll()) { if (StringUtil.isNotBlank(resource.getHref()) && resource.getHref().length() > lastSlashPos) { resource.setHref(resource.getHref().substring(lastSlashPos + 1)); } result.add(resource); } return result; } /** * Reads the document's spine, containing all sections in reading order. * * @param packageDocument b * @param resources b * @param idMapping b * @return the document's spine, containing all sections in reading order. */ private static Spine readSpine(Document packageDocument, Resources resources, Map idMapping) { Element spineElement = DOMUtil .getFirstElementByTagNameNS(packageDocument.getDocumentElement(), NAMESPACE_OPF, OPFTags.spine); if (spineElement == null) { Log.e(TAG, "Element " + OPFTags.spine + " not found in package document, generating one automatically"); return generateSpineFromResources(resources); } Spine result = new Spine(); String tocResourceId = DOMUtil.getAttribute(spineElement, NAMESPACE_OPF, OPFAttributes.toc); Log.v(TAG, tocResourceId); result.setTocResource(findTableOfContentsResource(tocResourceId, resources)); NodeList spineNodes = DOMUtil.getElementsByTagNameNS(packageDocument, NAMESPACE_OPF, OPFTags.itemref); if (spineNodes == null) { Log.e(TAG, "spineNodes is null"); return result; } List spineReferences = new ArrayList<>(spineNodes.getLength()); for (int i = 0; i < spineNodes.getLength(); i++) { Element spineItem = (Element) spineNodes.item(i); String itemref = DOMUtil.getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.idref); if (StringUtil.isBlank(itemref)) { Log.e(TAG, "itemref with missing or empty idref"); // XXX continue; } String id = idMapping.get(itemref); if (id == null) { id = itemref; } Resource resource = resources.getByIdOrHref(id); if (resource == null) { Log.e(TAG, "resource with id '" + id + "' not found"); continue; } SpineReference spineReference = new SpineReference(resource); if (OPFValues.no.equalsIgnoreCase(DOMUtil .getAttribute(spineItem, NAMESPACE_OPF, OPFAttributes.linear))) { spineReference.setLinear(false); } spineReferences.add(spineReference); } result.setSpineReferences(spineReferences); return result; } /** * Creates a spine out of all resources in the resources. * The generated spine consists of all XHTML pages in order of their href. * * @param resources f * @return a spine created out of all resources in the resources. */ private static Spine generateSpineFromResources(Resources resources) { Spine result = new Spine(); List resourceHrefs = new ArrayList<>(resources.getAllHrefs()); Collections.sort(resourceHrefs, String.CASE_INSENSITIVE_ORDER); for (String resourceHref : resourceHrefs) { Resource resource = resources.getByHref(resourceHref); if (resource.getMediaType() == MediaTypes.NCX) { result.setTocResource(resource); } else if (resource.getMediaType() == MediaTypes.XHTML) { result.addSpineReference(new SpineReference(resource)); } } return result; } /** * The spine tag should contain a 'toc' attribute with as value the resource id of the table of contents resource. *

* Here we try several ways of finding this table of contents resource. * We try the given attribute value, some often-used ones and finally look through all resources for the first resource with the table of contents mimetype. * * @param tocResourceId g * @param resources g * @return the Resource containing the table of contents */ static Resource findTableOfContentsResource( String tocResourceId, Resources resources ) { Resource tocResource; //一些epub3的文件为了兼容epub2,保留的epub2的目录文件,这里优先选择epub3的xml目录 tocResource = resources.getByProperties("nav"); if (tocResource != null) { return tocResource; } if (StringUtil.isNotBlank(tocResourceId)) { tocResource = resources.getByIdOrHref(tocResourceId); } if (tocResource != null) { return tocResource; } // get the first resource with the NCX mediatype tocResource = resources.findFirstResourceByMediaType(MediaTypes.NCX); if (tocResource == null) { for (String possibleNcxItemId : POSSIBLE_NCX_ITEM_IDS) { tocResource = resources.getByIdOrHref(possibleNcxItemId); if (tocResource != null) { break; } tocResource = resources .getByIdOrHref(possibleNcxItemId.toUpperCase()); if (tocResource != null) { break; } } } if (tocResource == null) { Log.e(TAG, "Could not find table of contents resource. Tried resource with id '" + tocResourceId + "', " + Constants.DEFAULT_TOC_ID + ", " + Constants.DEFAULT_TOC_ID.toUpperCase() + " and any NCX resource."); } return tocResource; } /** * Find all resources that have something to do with the coverpage and the cover image. * Search the meta tags and the guide references * * @param packageDocument s * @return all resources that have something to do with the coverpage and the cover image. */ // package static Set findCoverHrefs(Document packageDocument) { Set result = new HashSet<>(); // try and find a meta tag with name = 'cover' and a non-blank id String coverResourceId = DOMUtil .getFindAttributeValue(packageDocument, NAMESPACE_OPF, OPFTags.meta, OPFAttributes.name, OPFValues.meta_cover, OPFAttributes.content); if (StringUtil.isNotBlank(coverResourceId)) { String coverHref = DOMUtil .getFindAttributeValue(packageDocument, NAMESPACE_OPF, OPFTags.item, OPFAttributes.id, coverResourceId, OPFAttributes.href); if (StringUtil.isNotBlank(coverHref)) { result.add(coverHref); } else { result.add( coverResourceId); // maybe there was a cover href put in the cover id attribute } } // try and find a reference tag with type is 'cover' and reference is not blank String coverHref = DOMUtil .getFindAttributeValue(packageDocument, NAMESPACE_OPF, OPFTags.reference, OPFAttributes.type, OPFValues.reference_cover, OPFAttributes.href); if (StringUtil.isNotBlank(coverHref)) { result.add(coverHref); } return result; } /** * Finds the cover resource in the packageDocument and adds it to the book if found. * Keeps the cover resource in the resources map * * @param packageDocument s * @param book x */ private static void readCover(Document packageDocument, EpubBook book) { Collection coverHrefs = findCoverHrefs(packageDocument); for (String coverHref : coverHrefs) { Resource resource = book.getResources().getByHref(coverHref); if (resource == null) { Log.e(TAG, "Cover resource " + coverHref + " not found"); continue; } if (resource.getMediaType() == MediaTypes.XHTML) { book.setCoverPage(resource); } else if (MediaTypes.isBitmapImage(resource.getMediaType())) { book.setCoverImage(resource); } } } }