NCXDocumentV3.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461
  1. package me.ag2s.epublib.epub;
  2. import android.util.Log;
  3. import org.w3c.dom.Document;
  4. import org.w3c.dom.Element;
  5. import org.w3c.dom.Node;
  6. import org.w3c.dom.NodeList;
  7. import org.xmlpull.v1.XmlSerializer;
  8. import java.io.ByteArrayOutputStream;
  9. import java.io.IOException;
  10. import java.io.UnsupportedEncodingException;
  11. import java.net.URLDecoder;
  12. import java.util.ArrayList;
  13. import java.util.List;
  14. import me.ag2s.epublib.Constants;
  15. import me.ag2s.epublib.domain.Author;
  16. import me.ag2s.epublib.domain.EpubBook;
  17. import me.ag2s.epublib.domain.Identifier;
  18. import me.ag2s.epublib.domain.MediaType;
  19. import me.ag2s.epublib.domain.MediaTypes;
  20. import me.ag2s.epublib.domain.Resource;
  21. import me.ag2s.epublib.domain.TOCReference;
  22. import me.ag2s.epublib.domain.TableOfContents;
  23. import me.ag2s.epublib.util.ResourceUtil;
  24. import me.ag2s.epublib.util.StringUtil;
  25. /**
  26. * Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/
  27. *
  28. * @author Ag2S20150909
  29. */
  30. public class NCXDocumentV3 {
  31. public static final String NAMESPACE_XHTML = "http://www.w3.org/1999/xhtml";
  32. public static final String NAMESPACE_EPUB = "http://www.idpf.org/2007/ops";
  33. public static final String LANGUAGE = "en";
  34. @SuppressWarnings("unused")
  35. public static final String PREFIX_XHTML = "html";
  36. public static final String NCX_ITEM_ID = "htmltoc";
  37. public static final String DEFAULT_NCX_HREF = "toc.xhtml";
  38. public static final String V3_NCX_PROPERTIES = "nav";
  39. public static final MediaType V3_NCX_MEDIATYPE = MediaTypes.XHTML;
  40. private static final String TAG = NCXDocumentV3.class.getName();
  41. private interface XHTMLTgs {
  42. String html = "html";
  43. String head = "head";
  44. String title = "title";
  45. String meta = "meta";
  46. String link = "link";
  47. String body = "body";
  48. String h1 = "h1";
  49. String h2 = "h2";
  50. String nav = "nav";
  51. String ol = "ol";
  52. String li = "li";
  53. String a = "a";
  54. String span = "span";
  55. }
  56. private interface XHTMLAttributes {
  57. String xmlns = "xmlns";
  58. String xmlns_epub = "xmlns:epub";
  59. String lang = "lang";
  60. String xml_lang = "xml:lang";
  61. String rel = "rel";
  62. String type = "type";
  63. String epub_type = "epub:type";//nav的必须属性
  64. String id = "id";
  65. String role = "role";
  66. String href = "href";
  67. String http_equiv = "http-equiv";
  68. String content = "content";
  69. }
  70. private interface XHTMLAttributeValues {
  71. String Content_Type = "Content-Type";
  72. String HTML_UTF8 = "text/html; charset=utf-8";
  73. String lang = "en";
  74. String epub_type = "toc";
  75. String role_toc = "doc-toc";
  76. }
  77. /**
  78. * 解析epub的目录文件
  79. *
  80. * @param book Book
  81. * @param epubReader epubreader
  82. * @return Resource
  83. */
  84. @SuppressWarnings("unused")
  85. public static Resource read(EpubBook book, EpubReader epubReader) {
  86. Resource ncxResource = null;
  87. if (book.getSpine().getTocResource() == null) {
  88. Log.e(TAG, "Book does not contain a table of contents file");
  89. return null;
  90. }
  91. try {
  92. ncxResource = book.getSpine().getTocResource();
  93. if (ncxResource == null) {
  94. return null;
  95. }
  96. //一些epub 3 文件没有按照epub3的标准使用删除掉ncx目录文件
  97. if (ncxResource.getHref().endsWith(".ncx")) {
  98. Log.v(TAG, "该epub文件不标准,使用了epub2的目录文件");
  99. return NCXDocumentV2.read(book, epubReader);
  100. }
  101. Log.d(TAG, ncxResource.getHref());
  102. Document ncxDocument = ResourceUtil.getAsDocument(ncxResource);
  103. Log.d(TAG, ncxDocument.getNodeName());
  104. Element navMapElement = (Element) ncxDocument.getElementsByTagName(XHTMLTgs.nav).item(0);
  105. if (navMapElement == null) {
  106. Log.d(TAG, "epub3目录文件未发现nav节点,尝试使用epub2的规则解析");
  107. return NCXDocumentV2.read(book, epubReader);
  108. }
  109. navMapElement = (Element) navMapElement.getElementsByTagName(XHTMLTgs.ol).item(0);
  110. Log.d(TAG, navMapElement.getTagName());
  111. TableOfContents tableOfContents = new TableOfContents(
  112. readTOCReferences(navMapElement.getChildNodes(), book));
  113. Log.d(TAG, tableOfContents.toString());
  114. book.setTableOfContents(tableOfContents);
  115. } catch (Exception e) {
  116. Log.e(TAG, e.getMessage(), e);
  117. }
  118. return ncxResource;
  119. }
  120. private static List<TOCReference> doToc(Node n, EpubBook book) {
  121. if (n == null || n.getNodeType() != Document.ELEMENT_NODE) {
  122. return new ArrayList<>();
  123. }
  124. Element el = (Element) n;
  125. Node node = el.getElementsByTagName(XHTMLTgs.ol).item(0);
  126. if (node == null || node.getNodeType() != Document.ELEMENT_NODE) {
  127. return new ArrayList<>();
  128. }
  129. return readTOCReferences(node.getChildNodes(), book);
  130. }
  131. static List<TOCReference> readTOCReferences(NodeList navpoints,
  132. EpubBook book) {
  133. if (navpoints == null) {
  134. return new ArrayList<>();
  135. }
  136. //Log.d(TAG, "readTOCReferences:navpoints.getLength()" + navpoints.getLength());
  137. List<TOCReference> result = new ArrayList<>(navpoints.getLength());
  138. for (int i = 0; i < navpoints.getLength(); i++) {
  139. Node node = navpoints.item(i);
  140. //如果该node是null,或者不是Element,跳出本次循环
  141. if (node == null || node.getNodeType() != Document.ELEMENT_NODE) {
  142. continue;
  143. }
  144. Element el = (Element) node;
  145. //如果该Element的name为”li“,将其添加到目录结果
  146. if (el.getTagName().equals(XHTMLTgs.li)) {
  147. result.add(readTOCReference(el, book));
  148. }
  149. }
  150. return result;
  151. }
  152. static TOCReference readTOCReference(Element navpointElement, EpubBook book) {
  153. //章节的名称
  154. String label = readNavLabel(navpointElement);
  155. //Log.d(TAG, "label:" + label);
  156. String tocResourceRoot = StringUtil
  157. .substringBeforeLast(book.getSpine().getTocResource().getHref(), '/');
  158. if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref()
  159. .length()) {
  160. tocResourceRoot = "";
  161. } else {
  162. tocResourceRoot = tocResourceRoot + "/";
  163. }
  164. String reference = StringUtil
  165. .collapsePathDots(tocResourceRoot + readNavReference(navpointElement));
  166. String href = StringUtil
  167. .substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
  168. String fragmentId = StringUtil
  169. .substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
  170. Resource resource = book.getResources().getByHref(href);
  171. if (resource == null) {
  172. Log.e(TAG, "Resource with href " + href + " in NCX document not found");
  173. }
  174. Log.v(TAG, "label:" + label);
  175. Log.v(TAG, "href:" + href);
  176. Log.v(TAG, "fragmentId:" + fragmentId);
  177. //父级目录
  178. TOCReference result = new TOCReference(label, resource, fragmentId);
  179. //解析子级目录
  180. List<TOCReference> childTOCReferences = doToc(navpointElement, book);
  181. //readTOCReferences(
  182. //navpointElement.getChildNodes(), book);
  183. result.setChildren(childTOCReferences);
  184. return result;
  185. }
  186. /**
  187. * 获取目录节点的href
  188. *
  189. * @param navpointElement navpointElement
  190. * @return String
  191. */
  192. private static String readNavReference(Element navpointElement) {
  193. //https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav
  194. //父级节点必须是 "li"
  195. //Log.d(TAG, "readNavReference:" + navpointElement.getTagName());
  196. Element contentElement = DOMUtil
  197. .getFirstElementByTagNameNS(navpointElement, "", XHTMLTgs.a);
  198. if (contentElement == null) {
  199. return null;
  200. }
  201. String result = DOMUtil
  202. .getAttribute(contentElement, "", XHTMLAttributes.href);
  203. try {
  204. result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING);
  205. } catch (UnsupportedEncodingException e) {
  206. Log.e(TAG, e.getMessage());
  207. }
  208. return result;
  209. }
  210. /**
  211. * 获取目录节点里面的章节名
  212. *
  213. * @param navpointElement navpointElement
  214. * @return String
  215. */
  216. private static String readNavLabel(Element navpointElement) {
  217. //https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav
  218. //父级节点必须是 "li"
  219. //Log.d(TAG, "readNavLabel:" + navpointElement.getTagName());
  220. String label;
  221. Element labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "a");
  222. assert labelElement != null;
  223. label = labelElement.getTextContent();
  224. if (StringUtil.isNotBlank(label)) {
  225. return label;
  226. } else {
  227. labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "span");
  228. }
  229. assert labelElement != null;
  230. label = labelElement.getTextContent();
  231. //如果通过 a 标签无法获取章节列表,则是无href章节名
  232. return label;
  233. }
  234. public static Resource createNCXResource(EpubBook book)
  235. throws IllegalArgumentException, IllegalStateException, IOException {
  236. return createNCXResource(book.getMetadata().getIdentifiers(),
  237. book.getTitle(), book.getMetadata().getAuthors(),
  238. book.getTableOfContents());
  239. }
  240. public static Resource createNCXResource(List<Identifier> identifiers,
  241. String title, List<Author> authors, TableOfContents tableOfContents)
  242. throws IllegalArgumentException, IllegalStateException, IOException {
  243. ByteArrayOutputStream data = new ByteArrayOutputStream();
  244. XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data);
  245. write(out, identifiers, title, authors, tableOfContents);
  246. Resource resource = new Resource(NCX_ITEM_ID, data.toByteArray(),
  247. DEFAULT_NCX_HREF, V3_NCX_MEDIATYPE);
  248. resource.setProperties(V3_NCX_PROPERTIES);
  249. return resource;
  250. }
  251. /**
  252. * Generates a resource containing an xml document containing the table of contents of the book in ncx format.
  253. *
  254. * @param xmlSerializer the serializer used
  255. * @param book the book to serialize
  256. * @throws IOException IOException
  257. * @throws IllegalStateException IllegalStateException
  258. * @throws IllegalArgumentException IllegalArgumentException
  259. */
  260. public static void write(XmlSerializer xmlSerializer, EpubBook book)
  261. throws IllegalArgumentException, IllegalStateException, IOException {
  262. write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
  263. book.getMetadata().getAuthors(), book.getTableOfContents());
  264. }
  265. /**
  266. * 写入
  267. *
  268. * @param serializer serializer
  269. * @param identifiers identifiers
  270. * @param title title
  271. * @param authors authors
  272. * @param tableOfContents tableOfContents
  273. */
  274. @SuppressWarnings("unused")
  275. public static void write(XmlSerializer serializer,
  276. List<Identifier> identifiers, String title, List<Author> authors,
  277. TableOfContents tableOfContents) throws IllegalArgumentException, IllegalStateException, IOException {
  278. serializer.startDocument(Constants.CHARACTER_ENCODING, false);
  279. serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_XHTML);
  280. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.html);
  281. serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xmlns_epub, NAMESPACE_EPUB);
  282. serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xml_lang, XHTMLAttributeValues.lang);
  283. serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.lang, LANGUAGE);
  284. //写入头部head标签
  285. writeHead(title, serializer);
  286. //body开始
  287. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.body);
  288. //h1开始
  289. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h1);
  290. serializer.text(title);
  291. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h1);
  292. //h1关闭
  293. //nav开始
  294. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.nav);
  295. serializer.attribute("", XHTMLAttributes.epub_type, XHTMLAttributeValues.epub_type);
  296. serializer.attribute("", XHTMLAttributes.id, XHTMLAttributeValues.epub_type);
  297. serializer.attribute("", XHTMLAttributes.role, XHTMLAttributeValues.role_toc);
  298. //h2开始
  299. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h2);
  300. serializer.text("目录");
  301. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h2);
  302. writeNavPoints(tableOfContents.getTocReferences(), 1, serializer);
  303. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.nav);
  304. //body关闭
  305. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.body);
  306. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.html);
  307. serializer.endDocument();
  308. }
  309. private static int writeNavPoints(List<TOCReference> tocReferences,
  310. int playOrder,
  311. XmlSerializer serializer) throws IOException {
  312. writeOlStart(serializer);
  313. for (TOCReference tocReference : tocReferences) {
  314. if (tocReference.getResource() == null) {
  315. playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
  316. serializer);
  317. continue;
  318. }
  319. writeNavPointStart(tocReference, serializer);
  320. playOrder++;
  321. if (!tocReference.getChildren().isEmpty()) {
  322. playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
  323. serializer);
  324. }
  325. writeNavPointEnd(tocReference, serializer);
  326. }
  327. writeOlSEnd(serializer);
  328. return playOrder;
  329. }
  330. private static void writeNavPointStart(TOCReference tocReference, XmlSerializer serializer) throws IOException {
  331. writeLiStart(serializer);
  332. String title = tocReference.getTitle();
  333. String href = tocReference.getCompleteHref();
  334. if (StringUtil.isNotBlank(href)) {
  335. writeLabel(title, href, serializer);
  336. } else {
  337. writeLabel(title, serializer);
  338. }
  339. }
  340. @SuppressWarnings("unused")
  341. private static void writeNavPointEnd(TOCReference tocReference,
  342. XmlSerializer serializer) throws IOException {
  343. writeLiEnd(serializer);
  344. }
  345. protected static void writeLabel(String title, String href, XmlSerializer serializer) throws IOException {
  346. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.a);
  347. serializer.attribute("", XHTMLAttributes.href, href);
  348. //attribute必须在Text之前设置。
  349. serializer.text(title);
  350. //serializer.attribute(NAMESPACE_XHTML, XHTMLAttributes.href, href);
  351. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.a);
  352. }
  353. protected static void writeLabel(String title, XmlSerializer serializer) throws IOException {
  354. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.span);
  355. serializer.text(title);
  356. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.span);
  357. }
  358. private static void writeLiStart(XmlSerializer serializer) throws IOException {
  359. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.li);
  360. Log.d(TAG, "writeLiStart");
  361. }
  362. private static void writeLiEnd(XmlSerializer serializer) throws IOException {
  363. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.li);
  364. Log.d(TAG, "writeLiEND");
  365. }
  366. private static void writeOlStart(XmlSerializer serializer) throws IOException {
  367. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.ol);
  368. Log.d(TAG, "writeOlStart");
  369. }
  370. private static void writeOlSEnd(XmlSerializer serializer) throws IOException {
  371. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.ol);
  372. Log.d(TAG, "writeOlEnd");
  373. }
  374. private static void writeHead(String title, XmlSerializer serializer) throws IOException {
  375. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.head);
  376. //title
  377. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.title);
  378. serializer.text(StringUtil.defaultIfNull(title));
  379. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.title);
  380. //link
  381. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.link);
  382. serializer.attribute("", XHTMLAttributes.rel, "stylesheet");
  383. serializer.attribute("", XHTMLAttributes.type, "text/css");
  384. serializer.attribute("", XHTMLAttributes.href, "css/style.css");
  385. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.link);
  386. //meta
  387. serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.meta);
  388. serializer.attribute("", XHTMLAttributes.http_equiv, XHTMLAttributeValues.Content_Type);
  389. serializer.attribute("", XHTMLAttributes.content, XHTMLAttributeValues.HTML_UTF8);
  390. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.meta);
  391. serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.head);
  392. }
  393. }