lollipop版本分开,有些新特性android5.0和6.0不支持
This commit is contained in:
1 parent
0959e50f53
commit
3cf3231da5
258 files changed
+12817
-13529
No files matched your search
@@ -0,0 +1,20 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import me.ag2s.epublib.domain.EpubBook;
|
||||
|
||||
/**
|
||||
* Post-processes a book.
|
||||
* <p>
|
||||
* Can be used to clean up a book after reading or before writing.
|
||||
*
|
||||
* @author paul
|
||||
*/
|
||||
public interface BookProcessor {
|
||||
|
||||
/**
|
||||
* A BookProcessor that returns the input book unchanged.
|
||||
*/
|
||||
BookProcessor IDENTITY_BOOKPROCESSOR = book -> book;
|
||||
|
||||
EpubBook processBook(EpubBook book);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import me.ag2s.epublib.domain.EpubBook;
|
||||
|
||||
/**
|
||||
* A book processor that combines several other bookprocessors
|
||||
* <p>
|
||||
* Fixes coverpage/coverimage.
|
||||
* Cleans up the XHTML.
|
||||
*
|
||||
* @author paul.siegmann
|
||||
*/
|
||||
@SuppressWarnings("unused declaration")
|
||||
public class BookProcessorPipeline implements BookProcessor {
|
||||
|
||||
private static final String TAG = BookProcessorPipeline.class.getName();
|
||||
private List<BookProcessor> bookProcessors;
|
||||
|
||||
public BookProcessorPipeline() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public BookProcessorPipeline(List<BookProcessor> bookProcessingPipeline) {
|
||||
this.bookProcessors = bookProcessingPipeline;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EpubBook processBook(EpubBook book) {
|
||||
if (bookProcessors == null) {
|
||||
return book;
|
||||
}
|
||||
for (BookProcessor bookProcessor : bookProcessors) {
|
||||
try {
|
||||
book = bookProcessor.processBook(book);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
return book;
|
||||
}
|
||||
|
||||
public void addBookProcessor(BookProcessor bookProcessor) {
|
||||
if (this.bookProcessors == null) {
|
||||
bookProcessors = new ArrayList<>();
|
||||
}
|
||||
this.bookProcessors.add(bookProcessor);
|
||||
}
|
||||
|
||||
public void addBookProcessors(Collection<BookProcessor> bookProcessors) {
|
||||
if (this.bookProcessors == null) {
|
||||
this.bookProcessors = new ArrayList<>();
|
||||
}
|
||||
this.bookProcessors.addAll(bookProcessors);
|
||||
}
|
||||
|
||||
|
||||
public List<BookProcessor> getBookProcessors() {
|
||||
return bookProcessors;
|
||||
}
|
||||
|
||||
|
||||
public void setBookProcessingPipeline(
|
||||
List<BookProcessor> bookProcessingPipeline) {
|
||||
this.bookProcessors = bookProcessingPipeline;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.w3c.dom.Text;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.ag2s.epublib.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Utility methods for working with the DOM.
|
||||
*
|
||||
* @author paul
|
||||
*/
|
||||
// package
|
||||
class DOMUtil {
|
||||
|
||||
/**
|
||||
* First tries to get the attribute value by doing an getAttributeNS on the element, if that gets an empty element it does a getAttribute without namespace.
|
||||
*
|
||||
* @param element element
|
||||
* @param namespace namespace
|
||||
* @param attribute attribute
|
||||
* @return String Attribute
|
||||
*/
|
||||
public static String getAttribute(Element element, String namespace,
|
||||
String attribute) {
|
||||
String result = element.getAttributeNS(namespace, attribute);
|
||||
if (StringUtil.isEmpty(result)) {
|
||||
result = element.getAttribute(attribute);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all descendant elements of the given parentElement with the given namespace and tagname and returns their text child as a list of String.
|
||||
*
|
||||
* @param parentElement parentElement
|
||||
* @param namespace namespace
|
||||
* @param tagName tagName
|
||||
* @return List<String>
|
||||
*/
|
||||
public static List<String> getElementsTextChild(Element parentElement,
|
||||
String namespace, String tagName) {
|
||||
NodeList elements = parentElement
|
||||
.getElementsByTagNameNS(namespace, tagName);
|
||||
//ArrayList 初始化时指定长度提高性能
|
||||
List<String> result = new ArrayList<>(elements.getLength());
|
||||
for (int i = 0; i < elements.getLength(); i++) {
|
||||
result.add(getTextChildrenContent((Element) elements.item(i)));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds in the current document the first element with the given namespace and elementName and with the given findAttributeName and findAttributeValue.
|
||||
* It then returns the value of the given resultAttributeName.
|
||||
*
|
||||
* @param document document
|
||||
* @param namespace namespace
|
||||
* @param elementName elementName
|
||||
* @param findAttributeName findAttributeName
|
||||
* @param findAttributeValue findAttributeValue
|
||||
* @param resultAttributeName resultAttributeName
|
||||
* @return String value
|
||||
*/
|
||||
public static String getFindAttributeValue(Document document,
|
||||
String namespace, String elementName, String findAttributeName,
|
||||
String findAttributeValue, String resultAttributeName) {
|
||||
NodeList metaTags = document.getElementsByTagNameNS(namespace, elementName);
|
||||
for (int i = 0; i < metaTags.getLength(); i++) {
|
||||
Element metaElement = (Element) metaTags.item(i);
|
||||
if (findAttributeValue
|
||||
.equalsIgnoreCase(metaElement.getAttribute(findAttributeName))
|
||||
&& StringUtil
|
||||
.isNotBlank(metaElement.getAttribute(resultAttributeName))) {
|
||||
return metaElement.getAttribute(resultAttributeName);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first element that is a child of the parentElement and has the given namespace and tagName
|
||||
*
|
||||
* @param parentElement parentElement
|
||||
* @param namespace namespace
|
||||
* @param tagName tagName
|
||||
* @return Element
|
||||
*/
|
||||
public static NodeList getElementsByTagNameNS(Element parentElement,
|
||||
String namespace, String tagName) {
|
||||
NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName);
|
||||
if (nodes.getLength() != 0) {
|
||||
return nodes;
|
||||
}
|
||||
nodes = parentElement.getElementsByTagName(tagName);
|
||||
if (nodes.getLength() == 0) {
|
||||
return null;
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first element that is a child of the parentElement and has the given namespace and tagName
|
||||
*
|
||||
* @param parentElement parentElement
|
||||
* @param namespace namespace
|
||||
* @param tagName tagName
|
||||
* @return Element
|
||||
*/
|
||||
public static NodeList getElementsByTagNameNS(Document parentElement,
|
||||
String namespace, String tagName) {
|
||||
NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName);
|
||||
if (nodes.getLength() != 0) {
|
||||
return nodes;
|
||||
}
|
||||
nodes = parentElement.getElementsByTagName(tagName);
|
||||
if (nodes.getLength() == 0) {
|
||||
return null;
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first element that is a child of the parentElement and has the given namespace and tagName
|
||||
*
|
||||
* @param parentElement parentElement
|
||||
* @param namespace namespace
|
||||
* @param tagName tagName
|
||||
* @return Element
|
||||
*/
|
||||
public static Element getFirstElementByTagNameNS(Element parentElement,
|
||||
String namespace, String tagName) {
|
||||
NodeList nodes = parentElement.getElementsByTagNameNS(namespace, tagName);
|
||||
if (nodes.getLength() != 0) {
|
||||
return (Element) nodes.item(0);
|
||||
}
|
||||
nodes = parentElement.getElementsByTagName(tagName);
|
||||
if (nodes.getLength() == 0) {
|
||||
return null;
|
||||
}
|
||||
return (Element) nodes.item(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* The contents of all Text nodes that are children of the given parentElement.
|
||||
* The result is trim()-ed.
|
||||
* <p>
|
||||
* The reason for this more complicated procedure instead of just returning the data of the firstChild is that
|
||||
* when the text is Chinese characters then on Android each Characater is represented in the DOM as
|
||||
* an individual Text node.
|
||||
*
|
||||
* @param parentElement parentElement
|
||||
* @return String value
|
||||
*/
|
||||
public static String getTextChildrenContent(Element parentElement) {
|
||||
if (parentElement == null) {
|
||||
return null;
|
||||
}
|
||||
StringBuilder result = new StringBuilder();
|
||||
NodeList childNodes = parentElement.getChildNodes();
|
||||
for (int i = 0; i < childNodes.getLength(); i++) {
|
||||
Node node = childNodes.item(i);
|
||||
if ((node == null) ||
|
||||
(node.getNodeType() != Node.TEXT_NODE)) {
|
||||
continue;
|
||||
}
|
||||
result.append(((Text) node).getData());
|
||||
}
|
||||
return result.toString().trim();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.xml.sax.EntityResolver;
|
||||
import org.xml.sax.InputSource;
|
||||
import org.xmlpull.v1.XmlPullParserFactory;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.io.Writer;
|
||||
import java.net.URL;
|
||||
import java.util.Objects;
|
||||
|
||||
import javax.xml.parsers.DocumentBuilder;
|
||||
import javax.xml.parsers.DocumentBuilderFactory;
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
import me.ag2s.epublib.Constants;
|
||||
|
||||
/**
|
||||
* Various low-level support methods for reading/writing epubs.
|
||||
*
|
||||
* @author paul.siegmann
|
||||
*/
|
||||
public class EpubProcessorSupport {
|
||||
|
||||
private static final String TAG = EpubProcessorSupport.class.getName();
|
||||
|
||||
protected static DocumentBuilderFactory documentBuilderFactory;
|
||||
|
||||
static {
|
||||
init();
|
||||
}
|
||||
|
||||
static class EntityResolverImpl implements EntityResolver {
|
||||
|
||||
private String previousLocation;
|
||||
|
||||
@Override
|
||||
public InputSource resolveEntity(String publicId, String systemId)
|
||||
throws IOException {
|
||||
String resourcePath;
|
||||
if (systemId.startsWith("http:")) {
|
||||
URL url = new URL(systemId);
|
||||
resourcePath = "dtd/" + url.getHost() + url.getPath();
|
||||
previousLocation = resourcePath
|
||||
.substring(0, resourcePath.lastIndexOf('/'));
|
||||
} else {
|
||||
resourcePath =
|
||||
previousLocation + systemId.substring(systemId.lastIndexOf('/'));
|
||||
}
|
||||
|
||||
if (Objects.requireNonNull(this.getClass().getClassLoader()).getResource(resourcePath) == null) {
|
||||
throw new RuntimeException(
|
||||
"remote resource is not cached : [" + systemId
|
||||
+ "] cannot continue");
|
||||
}
|
||||
|
||||
InputStream in = Objects.requireNonNull(EpubProcessorSupport.class.getClassLoader())
|
||||
.getResourceAsStream(resourcePath);
|
||||
return new InputSource(in);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void init() {
|
||||
EpubProcessorSupport.documentBuilderFactory = DocumentBuilderFactory
|
||||
.newInstance();
|
||||
documentBuilderFactory.setNamespaceAware(true);
|
||||
documentBuilderFactory.setValidating(false);
|
||||
}
|
||||
|
||||
public static XmlSerializer createXmlSerializer(OutputStream out)
|
||||
throws UnsupportedEncodingException {
|
||||
return createXmlSerializer(
|
||||
new OutputStreamWriter(out, Constants.CHARACTER_ENCODING));
|
||||
}
|
||||
|
||||
public static XmlSerializer createXmlSerializer(Writer out) {
|
||||
XmlSerializer result = null;
|
||||
try {
|
||||
/*
|
||||
* Disable XmlPullParserFactory here before it doesn't work when
|
||||
* building native image using GraalVM
|
||||
*/
|
||||
XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
|
||||
factory.setValidating(true);
|
||||
result = factory.newSerializer();
|
||||
|
||||
//result = new KXmlSerializer();
|
||||
result.setFeature(
|
||||
"http://xmlpull.org/v1/doc/features.html#indent-output", true);
|
||||
result.setOutput(out);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG,
|
||||
"When creating XmlSerializer: " + e.getClass().getName() + ": " + e
|
||||
.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an EntityResolver that loads dtd's and such from the epub4j classpath.
|
||||
* In order to enable the loading of relative urls the given EntityResolver contains the previousLocation.
|
||||
* Because of a new EntityResolver is created every time this method is called.
|
||||
* Fortunately the EntityResolver created uses up very little memory per instance.
|
||||
*
|
||||
* @return an EntityResolver that loads dtd's and such from the epub4j classpath.
|
||||
*/
|
||||
public static EntityResolver getEntityResolver() {
|
||||
return new EntityResolverImpl();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public DocumentBuilderFactory getDocumentBuilderFactory() {
|
||||
return documentBuilderFactory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath.
|
||||
*
|
||||
* @return a DocumentBuilder that looks up dtd's and schema's from epub4j's classpath.
|
||||
*/
|
||||
public static DocumentBuilder createDocumentBuilder() {
|
||||
DocumentBuilder result = null;
|
||||
try {
|
||||
result = documentBuilderFactory.newDocumentBuilder();
|
||||
result.setEntityResolver(getEntityResolver());
|
||||
} catch (ParserConfigurationException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import me.ag2s.epublib.Constants;
|
||||
import me.ag2s.epublib.domain.EpubBook;
|
||||
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.util.ResourceUtil;
|
||||
import me.ag2s.epublib.util.StringUtil;
|
||||
import me.ag2s.epublib.util.zip.AndroidZipFile;
|
||||
import me.ag2s.epublib.util.zip.ZipFileWrapper;
|
||||
|
||||
/**
|
||||
* Reads an epub file.
|
||||
*
|
||||
* @author paul
|
||||
*/
|
||||
@SuppressWarnings("ALL")
|
||||
public class EpubReader {
|
||||
|
||||
private static final String TAG = EpubReader.class.getName();
|
||||
private final BookProcessor bookProcessor = BookProcessor.IDENTITY_BOOKPROCESSOR;
|
||||
|
||||
public EpubBook readEpub(InputStream in) throws IOException {
|
||||
return readEpub(in, Constants.CHARACTER_ENCODING);
|
||||
}
|
||||
|
||||
public EpubBook readEpub(ZipInputStream in) throws IOException {
|
||||
return readEpub(in, Constants.CHARACTER_ENCODING);
|
||||
}
|
||||
|
||||
public EpubBook readEpub(ZipFile zipfile) throws IOException {
|
||||
return readEpub(zipfile, Constants.CHARACTER_ENCODING);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read epub from inputstream
|
||||
*
|
||||
* @param in the inputstream from which to read the epub
|
||||
* @param encoding the encoding to use for the html files within the epub
|
||||
* @return the Book as read from the inputstream
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
public EpubBook readEpub(InputStream in, String encoding) throws IOException {
|
||||
return readEpub(new ZipInputStream(in), encoding);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Reads this EPUB without loading any resources into memory.
|
||||
*
|
||||
* @param zipFile the file to load
|
||||
* @param encoding the encoding for XHTML files
|
||||
* @return this Book without loading all resources into memory.
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
public EpubBook readEpubLazy(@NonNull ZipFile zipFile, @NonNull String encoding)
|
||||
throws IOException {
|
||||
return readEpubLazy(zipFile, encoding, Arrays.asList(MediaTypes.mediaTypes));
|
||||
}
|
||||
|
||||
public EpubBook readEpubLazy(@NonNull AndroidZipFile zipFile, @NonNull String encoding)
|
||||
throws IOException {
|
||||
return readEpubLazy(zipFile, encoding, Arrays.asList(MediaTypes.mediaTypes));
|
||||
}
|
||||
|
||||
public EpubBook readEpub(@NonNull ZipInputStream in, @NonNull String encoding) throws IOException {
|
||||
return readEpub(ResourcesLoader.loadResources(in, encoding));
|
||||
}
|
||||
|
||||
public EpubBook readEpub(ZipFile in, String encoding) throws IOException {
|
||||
return readEpub(ResourcesLoader.loadResources(new ZipFileWrapper(in), encoding));
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads this EPUB without loading all resources into memory.
|
||||
*
|
||||
* @param zipFile the file to load
|
||||
* @param encoding the encoding for XHTML files
|
||||
* @param lazyLoadedTypes a list of the MediaType to load lazily
|
||||
* @return this Book without loading all resources into memory.
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
public EpubBook readEpubLazy(@NonNull ZipFile zipFile, @NonNull String encoding,
|
||||
@NonNull List<MediaType> lazyLoadedTypes) throws IOException {
|
||||
Resources resources = ResourcesLoader
|
||||
.loadResources(new ZipFileWrapper(zipFile), encoding, lazyLoadedTypes);
|
||||
return readEpub(resources);
|
||||
}
|
||||
|
||||
public EpubBook readEpubLazy(@NonNull AndroidZipFile zipFile, @NonNull String encoding,
|
||||
@NonNull List<MediaType> lazyLoadedTypes) throws IOException {
|
||||
Resources resources = ResourcesLoader
|
||||
.loadResources(new ZipFileWrapper(zipFile), encoding, lazyLoadedTypes);
|
||||
return readEpub(resources);
|
||||
}
|
||||
|
||||
public EpubBook readEpub(Resources resources) {
|
||||
return readEpub(resources, new EpubBook());
|
||||
}
|
||||
|
||||
public EpubBook readEpub(Resources resources, EpubBook result) {
|
||||
if (result == null) {
|
||||
result = new EpubBook();
|
||||
}
|
||||
handleMimeType(result, resources);
|
||||
String packageResourceHref = getPackageResourceHref(resources);
|
||||
Resource packageResource = processPackageResource(packageResourceHref, result, resources);
|
||||
result.setOpfResource(packageResource);
|
||||
Resource ncxResource = processNcxResource(packageResource, result);
|
||||
result.setNcxResource(ncxResource);
|
||||
result = postProcessBook(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private EpubBook postProcessBook(EpubBook book) {
|
||||
if (bookProcessor != null) {
|
||||
book = bookProcessor.processBook(book);
|
||||
}
|
||||
return book;
|
||||
}
|
||||
|
||||
private Resource processNcxResource(Resource packageResource, EpubBook book) {
|
||||
Log.d(TAG, "OPF:getHref()" + packageResource.getHref());
|
||||
if (book.isEpub3()) {
|
||||
return NCXDocumentV3.read(book, this);
|
||||
} else {
|
||||
return NCXDocumentV2.read(book, this);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private Resource processPackageResource(String packageResourceHref, EpubBook book,
|
||||
Resources resources) {
|
||||
Resource packageResource = resources.remove(packageResourceHref);
|
||||
try {
|
||||
PackageDocumentReader.read(packageResource, this, book, resources);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage(), e);
|
||||
}
|
||||
return packageResource;
|
||||
}
|
||||
|
||||
private String getPackageResourceHref(Resources resources) {
|
||||
String defaultResult = "OEBPS/content.opf";
|
||||
String result = defaultResult;
|
||||
|
||||
Resource containerResource = resources.remove("META-INF/container.xml");
|
||||
if (containerResource == null) {
|
||||
return result;
|
||||
}
|
||||
try {
|
||||
Document document = ResourceUtil.getAsDocument(containerResource);
|
||||
Element rootFileElement = (Element) ((Element) document
|
||||
.getDocumentElement().getElementsByTagName("rootfiles").item(0))
|
||||
.getElementsByTagName("rootfile").item(0);
|
||||
result = rootFileElement.getAttribute("full-path");
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage(), e);
|
||||
}
|
||||
if (StringUtil.isBlank(result)) {
|
||||
result = defaultResult;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void handleMimeType(EpubBook result, Resources resources) {
|
||||
resources.remove("mimetype");
|
||||
//result.setResources(resources);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.Writer;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import me.ag2s.epublib.domain.EpubBook;
|
||||
import me.ag2s.epublib.domain.MediaTypes;
|
||||
import me.ag2s.epublib.domain.Resource;
|
||||
import me.ag2s.epublib.util.IOUtil;
|
||||
|
||||
/**
|
||||
* Generates an epub file. Not thread-safe, single use object.
|
||||
*
|
||||
* @author paul
|
||||
*/
|
||||
public class EpubWriter {
|
||||
|
||||
private static final String TAG = EpubWriter.class.getName();
|
||||
|
||||
// package
|
||||
static final String EMPTY_NAMESPACE_PREFIX = "";
|
||||
|
||||
private BookProcessor bookProcessor;
|
||||
|
||||
public EpubWriter() {
|
||||
this(BookProcessor.IDENTITY_BOOKPROCESSOR);
|
||||
}
|
||||
|
||||
|
||||
public EpubWriter(BookProcessor bookProcessor) {
|
||||
this.bookProcessor = bookProcessor;
|
||||
}
|
||||
|
||||
|
||||
public void write(EpubBook book, OutputStream out) throws IOException {
|
||||
book = processBook(book);
|
||||
ZipOutputStream resultStream = new ZipOutputStream(out);
|
||||
writeMimeType(resultStream);
|
||||
writeContainer(resultStream);
|
||||
initTOCResource(book);
|
||||
writeResources(book, resultStream);
|
||||
writePackageDocument(book, resultStream);
|
||||
resultStream.close();
|
||||
}
|
||||
|
||||
private EpubBook processBook(EpubBook book) {
|
||||
if (bookProcessor != null) {
|
||||
book = bookProcessor.processBook(book);
|
||||
}
|
||||
return book;
|
||||
}
|
||||
|
||||
private void initTOCResource(EpubBook book) {
|
||||
Resource tocResource;
|
||||
try {
|
||||
if (book.isEpub3()) {
|
||||
tocResource = NCXDocumentV3.createNCXResource(book);
|
||||
} else {
|
||||
tocResource = NCXDocumentV2.createNCXResource(book);
|
||||
}
|
||||
|
||||
Resource currentTocResource = book.getSpine().getTocResource();
|
||||
if (currentTocResource != null) {
|
||||
book.getResources().remove(currentTocResource.getHref());
|
||||
}
|
||||
book.getSpine().setTocResource(tocResource);
|
||||
book.getResources().add(tocResource);
|
||||
} catch (Exception ex) {
|
||||
Log.e(TAG,
|
||||
"Error writing table of contents: "
|
||||
+ ex.getClass().getName() + ": " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void writeResources(EpubBook book, ZipOutputStream resultStream) {
|
||||
for (Resource resource : book.getResources().getAll()) {
|
||||
writeResource(resource, resultStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the resource to the resultStream.
|
||||
*
|
||||
* @param resource resource
|
||||
* @param resultStream resultStream
|
||||
*/
|
||||
private void writeResource(Resource resource, ZipOutputStream resultStream) {
|
||||
if (resource == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resultStream.putNextEntry(new ZipEntry("OEBPS/" + resource.getHref()));
|
||||
InputStream inputStream = resource.getInputStream();
|
||||
|
||||
IOUtil.copy(inputStream, resultStream);
|
||||
inputStream.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void writePackageDocument(EpubBook book, ZipOutputStream resultStream)
|
||||
throws IOException {
|
||||
resultStream.putNextEntry(new ZipEntry("OEBPS/content.opf"));
|
||||
XmlSerializer xmlSerializer = EpubProcessorSupport
|
||||
.createXmlSerializer(resultStream);
|
||||
PackageDocumentWriter.write(this, xmlSerializer, book);
|
||||
xmlSerializer.flush();
|
||||
// String resultAsString = result.toString();
|
||||
// resultStream.write(resultAsString.getBytes(Constants.ENCODING));
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the META-INF/container.xml file.
|
||||
*
|
||||
* @param resultStream resultStream
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
private void writeContainer(ZipOutputStream resultStream) throws IOException {
|
||||
resultStream.putNextEntry(new ZipEntry("META-INF/container.xml"));
|
||||
Writer out = new OutputStreamWriter(resultStream);
|
||||
out.write("<?xml version=\"1.0\"?>\n");
|
||||
out.write(
|
||||
"<container version=\"1.0\" xmlns=\"urn:oasis:names:tc:opendocument:xmlns:container\">\n");
|
||||
out.write("\t<rootfiles>\n");
|
||||
out.write(
|
||||
"\t\t<rootfile full-path=\"OEBPS/content.opf\" media-type=\"application/oebps-package+xml\"/>\n");
|
||||
out.write("\t</rootfiles>\n");
|
||||
out.write("</container>");
|
||||
out.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the mimetype as an uncompressed file in the ZipOutputStream.
|
||||
*
|
||||
* @param resultStream resultStream
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
private void writeMimeType(ZipOutputStream resultStream) throws IOException {
|
||||
ZipEntry mimetypeZipEntry = new ZipEntry("mimetype");
|
||||
mimetypeZipEntry.setMethod(ZipEntry.STORED);
|
||||
byte[] mimetypeBytes = MediaTypes.EPUB.getName().getBytes();
|
||||
mimetypeZipEntry.setSize(mimetypeBytes.length);
|
||||
mimetypeZipEntry.setCrc(calculateCrc(mimetypeBytes));
|
||||
resultStream.putNextEntry(mimetypeZipEntry);
|
||||
resultStream.write(mimetypeBytes);
|
||||
}
|
||||
|
||||
private long calculateCrc(byte[] data) {
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(data);
|
||||
return crc.getValue();
|
||||
}
|
||||
|
||||
String getNcxId() {
|
||||
return "ncx";
|
||||
}
|
||||
|
||||
String getNcxHref() {
|
||||
return "toc.ncx";
|
||||
}
|
||||
|
||||
String getNcxMediaType() {
|
||||
return MediaTypes.NCX.getName();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public BookProcessor getBookProcessor() {
|
||||
return bookProcessor;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public void setBookProcessor(BookProcessor bookProcessor) {
|
||||
this.bookProcessor = bookProcessor;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import java.io.OutputStream;
|
||||
|
||||
import me.ag2s.epublib.domain.Resource;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface HtmlProcessor {
|
||||
|
||||
void processHtmlResource(Resource resource, OutputStream out);
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
import me.ag2s.epublib.Constants;
|
||||
import me.ag2s.epublib.domain.Author;
|
||||
import me.ag2s.epublib.domain.EpubBook;
|
||||
import me.ag2s.epublib.domain.Identifier;
|
||||
import me.ag2s.epublib.domain.MediaTypes;
|
||||
import me.ag2s.epublib.domain.Resource;
|
||||
import me.ag2s.epublib.domain.TOCReference;
|
||||
import me.ag2s.epublib.domain.TableOfContents;
|
||||
import me.ag2s.epublib.util.ResourceUtil;
|
||||
import me.ag2s.epublib.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/
|
||||
*
|
||||
* @author paul
|
||||
*/
|
||||
public class NCXDocumentV2 {
|
||||
|
||||
public static final String NAMESPACE_NCX = "http://www.daisy.org/z3986/2005/ncx/";
|
||||
@SuppressWarnings("unused")
|
||||
public static final String PREFIX_NCX = "ncx";
|
||||
public static final String NCX_ITEM_ID = "ncx";
|
||||
public static final String DEFAULT_NCX_HREF = "toc.ncx";
|
||||
public static final String PREFIX_DTB = "dtb";
|
||||
|
||||
private static final String TAG = NCXDocumentV2.class.getName();
|
||||
|
||||
private interface NCXTags {
|
||||
|
||||
String ncx = "ncx";
|
||||
String meta = "meta";
|
||||
String navPoint = "navPoint";
|
||||
String navMap = "navMap";
|
||||
String navLabel = "navLabel";
|
||||
String content = "content";
|
||||
String text = "text";
|
||||
String docTitle = "docTitle";
|
||||
String docAuthor = "docAuthor";
|
||||
String head = "head";
|
||||
}
|
||||
|
||||
private interface NCXAttributes {
|
||||
|
||||
String src = "src";
|
||||
String name = "name";
|
||||
String content = "content";
|
||||
String id = "id";
|
||||
String playOrder = "playOrder";
|
||||
String clazz = "class";
|
||||
String version = "version";
|
||||
}
|
||||
|
||||
private interface NCXAttributeValues {
|
||||
|
||||
String chapter = "chapter";
|
||||
String version = "2005-1";
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static Resource read(EpubBook book, EpubReader epubReader) {
|
||||
Resource ncxResource = null;
|
||||
if (book.getSpine().getTocResource() == null) {
|
||||
Log.e(TAG, "Book does not contain a table of contents file");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
ncxResource = book.getSpine().getTocResource();
|
||||
if (ncxResource == null) {
|
||||
return null;
|
||||
}
|
||||
Log.d(TAG, ncxResource.getHref());
|
||||
Document ncxDocument = ResourceUtil.getAsDocument(ncxResource);
|
||||
Element navMapElement = DOMUtil
|
||||
.getFirstElementByTagNameNS(ncxDocument.getDocumentElement(),
|
||||
NAMESPACE_NCX, NCXTags.navMap);
|
||||
if (navMapElement == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TableOfContents tableOfContents = new TableOfContents(
|
||||
readTOCReferences(navMapElement.getChildNodes(), book));
|
||||
book.setTableOfContents(tableOfContents);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage(), e);
|
||||
}
|
||||
return ncxResource;
|
||||
}
|
||||
|
||||
static List<TOCReference> readTOCReferences(NodeList navpoints,
|
||||
EpubBook book) {
|
||||
if (navpoints == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<TOCReference> result = new ArrayList<>(
|
||||
navpoints.getLength());
|
||||
for (int i = 0; i < navpoints.getLength(); i++) {
|
||||
Node node = navpoints.item(i);
|
||||
if (node.getNodeType() != Document.ELEMENT_NODE) {
|
||||
continue;
|
||||
}
|
||||
if (!(node.getLocalName().equals(NCXTags.navPoint))) {
|
||||
continue;
|
||||
}
|
||||
TOCReference tocReference = readTOCReference((Element) node, book);
|
||||
result.add(tocReference);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static TOCReference readTOCReference(Element navpointElement, EpubBook book) {
|
||||
String label = readNavLabel(navpointElement);
|
||||
//Log.d(TAG,"label:"+label);
|
||||
String tocResourceRoot = StringUtil
|
||||
.substringBeforeLast(book.getSpine().getTocResource().getHref(), '/');
|
||||
if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref()
|
||||
.length()) {
|
||||
tocResourceRoot = "";
|
||||
} else {
|
||||
tocResourceRoot = tocResourceRoot + "/";
|
||||
}
|
||||
String reference = StringUtil
|
||||
.collapsePathDots(tocResourceRoot + readNavReference(navpointElement));
|
||||
String href = StringUtil
|
||||
.substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
|
||||
String fragmentId = StringUtil
|
||||
.substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
|
||||
Resource resource = book.getResources().getByHref(href);
|
||||
if (resource == null) {
|
||||
Log.e(TAG, "Resource with href " + href + " in NCX document not found");
|
||||
}
|
||||
Log.v(TAG, "label:" + label);
|
||||
Log.v(TAG, "href:" + href);
|
||||
Log.v(TAG, "fragmentId:" + fragmentId);
|
||||
TOCReference result = new TOCReference(label, resource, fragmentId);
|
||||
List<TOCReference> childTOCReferences = readTOCReferences(
|
||||
navpointElement.getChildNodes(), book);
|
||||
result.setChildren(childTOCReferences);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String readNavReference(Element navpointElement) {
|
||||
Element contentElement = DOMUtil
|
||||
.getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX,
|
||||
NCXTags.content);
|
||||
if (contentElement == null) {
|
||||
return null;
|
||||
}
|
||||
String result = DOMUtil
|
||||
.getAttribute(contentElement, NAMESPACE_NCX, NCXAttributes.src);
|
||||
try {
|
||||
result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String readNavLabel(Element navpointElement) {
|
||||
//Log.d(TAG,navpointElement.getTagName());
|
||||
Element navLabel = DOMUtil
|
||||
.getFirstElementByTagNameNS(navpointElement, NAMESPACE_NCX,
|
||||
NCXTags.navLabel);
|
||||
assert navLabel != null;
|
||||
return DOMUtil.getTextChildrenContent(DOMUtil
|
||||
.getFirstElementByTagNameNS(navLabel, NAMESPACE_NCX, NCXTags.text));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static void write(EpubWriter epubWriter, EpubBook book,
|
||||
ZipOutputStream resultStream) throws IOException {
|
||||
resultStream
|
||||
.putNextEntry(new ZipEntry(book.getSpine().getTocResource().getHref()));
|
||||
XmlSerializer out = EpubProcessorSupport.createXmlSerializer(resultStream);
|
||||
write(out, book);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates a resource containing an xml document containing the table of contents of the book in ncx format.
|
||||
*
|
||||
* @param xmlSerializer the serializer used
|
||||
* @param book the book to serialize
|
||||
* @throws IOException IOException
|
||||
* @throws IllegalStateException IllegalStateException
|
||||
* @throws IllegalArgumentException IllegalArgumentException
|
||||
*/
|
||||
public static void write(XmlSerializer xmlSerializer, EpubBook book)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
|
||||
book.getMetadata().getAuthors(), book.getTableOfContents());
|
||||
}
|
||||
|
||||
public static Resource createNCXResource(EpubBook book)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
return createNCXResource(book.getMetadata().getIdentifiers(),
|
||||
book.getTitle(), book.getMetadata().getAuthors(),
|
||||
book.getTableOfContents());
|
||||
}
|
||||
|
||||
public static Resource createNCXResource(List<Identifier> identifiers,
|
||||
String title, List<Author> authors, TableOfContents tableOfContents)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data);
|
||||
write(out, identifiers, title, authors, tableOfContents);
|
||||
return new Resource(NCX_ITEM_ID, data.toByteArray(),
|
||||
DEFAULT_NCX_HREF, MediaTypes.NCX);
|
||||
}
|
||||
|
||||
public static void write(XmlSerializer serializer,
|
||||
List<Identifier> identifiers, String title, List<Author> authors,
|
||||
TableOfContents tableOfContents)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
serializer.startDocument(Constants.CHARACTER_ENCODING, false);
|
||||
serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_NCX);
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.ncx);
|
||||
// serializer.writeNamespace("ncx", NAMESPACE_NCX);
|
||||
// serializer.attribute("xmlns", NAMESPACE_NCX);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.version,
|
||||
NCXAttributeValues.version);
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.head);
|
||||
|
||||
for (Identifier identifier : identifiers) {
|
||||
writeMetaElement(identifier.getScheme(), identifier.getValue(),
|
||||
serializer);
|
||||
}
|
||||
|
||||
writeMetaElement("generator", Constants.EPUB_GENERATOR_NAME, serializer);
|
||||
writeMetaElement("depth", String.valueOf(tableOfContents.calculateDepth()),
|
||||
serializer);
|
||||
writeMetaElement("totalPageCount", "0", serializer);
|
||||
writeMetaElement("maxPageNumber", "0", serializer);
|
||||
|
||||
serializer.endTag(NAMESPACE_NCX, "head");
|
||||
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.docTitle);
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.text);
|
||||
// write the first title
|
||||
serializer.text(StringUtil.defaultIfNull(title));
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.text);
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.docTitle);
|
||||
|
||||
for (Author author : authors) {
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.docAuthor);
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.text);
|
||||
serializer.text(author.getLastname() + ", " + author.getFirstname());
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.text);
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.docAuthor);
|
||||
}
|
||||
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.navMap);
|
||||
writeNavPoints(tableOfContents.getTocReferences(), 1, serializer);
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.navMap);
|
||||
|
||||
serializer.endTag(NAMESPACE_NCX, "ncx");
|
||||
serializer.endDocument();
|
||||
}
|
||||
|
||||
|
||||
private static void writeMetaElement(String dtbName, String content,
|
||||
XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.meta);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.name,
|
||||
PREFIX_DTB + ":" + dtbName);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.content,
|
||||
content);
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.meta);
|
||||
}
|
||||
|
||||
private static int writeNavPoints(List<TOCReference> tocReferences,
|
||||
int playOrder,
|
||||
XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
for (TOCReference tocReference : tocReferences) {
|
||||
if (tocReference.getResource() == null) {
|
||||
playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
|
||||
serializer);
|
||||
continue;
|
||||
}
|
||||
writeNavPointStart(tocReference, playOrder, serializer);
|
||||
playOrder++;
|
||||
if (!tocReference.getChildren().isEmpty()) {
|
||||
playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
|
||||
serializer);
|
||||
}
|
||||
writeNavPointEnd(tocReference, serializer);
|
||||
}
|
||||
return playOrder;
|
||||
}
|
||||
|
||||
|
||||
private static void writeNavPointStart(TOCReference tocReference,
|
||||
int playOrder, XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.navPoint);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.id,
|
||||
"navPoint-" + playOrder);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.playOrder,
|
||||
String.valueOf(playOrder));
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.clazz,
|
||||
NCXAttributeValues.chapter);
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.navLabel);
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.text);
|
||||
serializer.text(tocReference.getTitle());
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.text);
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.navLabel);
|
||||
serializer.startTag(NAMESPACE_NCX, NCXTags.content);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, NCXAttributes.src,
|
||||
tocReference.getCompleteHref());
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.content);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void writeNavPointEnd(TOCReference tocReference,
|
||||
XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
serializer.endTag(NAMESPACE_NCX, NCXTags.navPoint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,461 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import me.ag2s.epublib.Constants;
|
||||
import me.ag2s.epublib.domain.Author;
|
||||
import me.ag2s.epublib.domain.EpubBook;
|
||||
import me.ag2s.epublib.domain.Identifier;
|
||||
import me.ag2s.epublib.domain.MediaType;
|
||||
import me.ag2s.epublib.domain.MediaTypes;
|
||||
import me.ag2s.epublib.domain.Resource;
|
||||
import me.ag2s.epublib.domain.TOCReference;
|
||||
import me.ag2s.epublib.domain.TableOfContents;
|
||||
import me.ag2s.epublib.util.ResourceUtil;
|
||||
import me.ag2s.epublib.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Writes the ncx document as defined by namespace http://www.daisy.org/z3986/2005/ncx/
|
||||
*
|
||||
* @author Ag2S20150909
|
||||
*/
|
||||
|
||||
public class NCXDocumentV3 {
|
||||
public static final String NAMESPACE_XHTML = "http://www.w3.org/1999/xhtml";
|
||||
public static final String NAMESPACE_EPUB = "http://www.idpf.org/2007/ops";
|
||||
public static final String LANGUAGE = "en";
|
||||
@SuppressWarnings("unused")
|
||||
public static final String PREFIX_XHTML = "html";
|
||||
public static final String NCX_ITEM_ID = "htmltoc";
|
||||
public static final String DEFAULT_NCX_HREF = "toc.xhtml";
|
||||
public static final String V3_NCX_PROPERTIES = "nav";
|
||||
public static final MediaType V3_NCX_MEDIATYPE = MediaTypes.XHTML;
|
||||
|
||||
private static final String TAG = NCXDocumentV3.class.getName();
|
||||
|
||||
private interface XHTMLTgs {
|
||||
String html = "html";
|
||||
String head = "head";
|
||||
String title = "title";
|
||||
String meta = "meta";
|
||||
String link = "link";
|
||||
String body = "body";
|
||||
String h1 = "h1";
|
||||
String h2 = "h2";
|
||||
String nav = "nav";
|
||||
String ol = "ol";
|
||||
String li = "li";
|
||||
String a = "a";
|
||||
String span = "span";
|
||||
}
|
||||
|
||||
private interface XHTMLAttributes {
|
||||
String xmlns = "xmlns";
|
||||
String xmlns_epub = "xmlns:epub";
|
||||
String lang = "lang";
|
||||
String xml_lang = "xml:lang";
|
||||
String rel = "rel";
|
||||
String type = "type";
|
||||
String epub_type = "epub:type";//nav的必须属性
|
||||
String id = "id";
|
||||
String role = "role";
|
||||
String href = "href";
|
||||
String http_equiv = "http-equiv";
|
||||
String content = "content";
|
||||
|
||||
}
|
||||
|
||||
private interface XHTMLAttributeValues {
|
||||
String Content_Type = "Content-Type";
|
||||
String HTML_UTF8 = "text/html; charset=utf-8";
|
||||
String lang = "en";
|
||||
String epub_type = "toc";
|
||||
String role_toc = "doc-toc";
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 解析epub的目录文件
|
||||
*
|
||||
* @param book Book
|
||||
* @param epubReader epubreader
|
||||
* @return Resource
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static Resource read(EpubBook book, EpubReader epubReader) {
|
||||
Resource ncxResource = null;
|
||||
if (book.getSpine().getTocResource() == null) {
|
||||
Log.e(TAG, "Book does not contain a table of contents file");
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
ncxResource = book.getSpine().getTocResource();
|
||||
if (ncxResource == null) {
|
||||
return null;
|
||||
}
|
||||
//一些epub 3 文件没有按照epub3的标准使用删除掉ncx目录文件
|
||||
if (ncxResource.getHref().endsWith(".ncx")) {
|
||||
Log.v(TAG, "该epub文件不标准,使用了epub2的目录文件");
|
||||
return NCXDocumentV2.read(book, epubReader);
|
||||
}
|
||||
Log.d(TAG, ncxResource.getHref());
|
||||
|
||||
Document ncxDocument = ResourceUtil.getAsDocument(ncxResource);
|
||||
Log.d(TAG, ncxDocument.getNodeName());
|
||||
|
||||
Element navMapElement = (Element) ncxDocument.getElementsByTagName(XHTMLTgs.nav).item(0);
|
||||
if (navMapElement == null) {
|
||||
Log.d(TAG, "epub3目录文件未发现nav节点,尝试使用epub2的规则解析");
|
||||
return NCXDocumentV2.read(book, epubReader);
|
||||
}
|
||||
navMapElement = (Element) navMapElement.getElementsByTagName(XHTMLTgs.ol).item(0);
|
||||
Log.d(TAG, navMapElement.getTagName());
|
||||
|
||||
TableOfContents tableOfContents = new TableOfContents(
|
||||
readTOCReferences(navMapElement.getChildNodes(), book));
|
||||
Log.d(TAG, tableOfContents.toString());
|
||||
book.setTableOfContents(tableOfContents);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, e.getMessage(), e);
|
||||
}
|
||||
return ncxResource;
|
||||
}
|
||||
|
||||
private static List<TOCReference> doToc(Node n, EpubBook book) {
|
||||
List<TOCReference> result = new ArrayList<>();
|
||||
|
||||
if (n == null || n.getNodeType() != Document.ELEMENT_NODE) {
|
||||
return result;
|
||||
} else {
|
||||
Element el = (Element) n;
|
||||
NodeList nodeList = el.getElementsByTagName(XHTMLTgs.li);
|
||||
for (int i = 0; i < nodeList.getLength(); i++) {
|
||||
result.add(readTOCReference((Element) nodeList.item(i), book));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
static List<TOCReference> readTOCReferences(NodeList navpoints,
|
||||
EpubBook book) {
|
||||
if (navpoints == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
//Log.d(TAG, "readTOCReferences:navpoints.getLength()" + navpoints.getLength());
|
||||
List<TOCReference> result = new ArrayList<>(navpoints.getLength());
|
||||
for (int i = 0; i < navpoints.getLength(); i++) {
|
||||
Node node = navpoints.item(i);
|
||||
//如果该node是null,或者不是Element,跳出本次循环
|
||||
if (node == null || node.getNodeType() != Document.ELEMENT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Element el = (Element) node;
|
||||
//如果该Element的name为”li“,将其添加到目录结果
|
||||
if (el.getTagName().equals(XHTMLTgs.li)) {
|
||||
result.add(readTOCReference(el, book));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
static TOCReference readTOCReference(Element navpointElement, EpubBook book) {
|
||||
//章节的名称
|
||||
String label = readNavLabel(navpointElement);
|
||||
//Log.d(TAG, "label:" + label);
|
||||
String tocResourceRoot = StringUtil
|
||||
.substringBeforeLast(book.getSpine().getTocResource().getHref(), '/');
|
||||
if (tocResourceRoot.length() == book.getSpine().getTocResource().getHref()
|
||||
.length()) {
|
||||
tocResourceRoot = "";
|
||||
} else {
|
||||
tocResourceRoot = tocResourceRoot + "/";
|
||||
}
|
||||
|
||||
String reference = StringUtil
|
||||
.collapsePathDots(tocResourceRoot + readNavReference(navpointElement));
|
||||
String href = StringUtil
|
||||
.substringBefore(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
|
||||
String fragmentId = StringUtil
|
||||
.substringAfter(reference, Constants.FRAGMENT_SEPARATOR_CHAR);
|
||||
Resource resource = book.getResources().getByHref(href);
|
||||
if (resource == null) {
|
||||
Log.e(TAG, "Resource with href " + href + " in NCX document not found");
|
||||
}
|
||||
Log.v(TAG, "label:" + label);
|
||||
Log.v(TAG, "href:" + href);
|
||||
Log.v(TAG, "fragmentId:" + fragmentId);
|
||||
|
||||
//父级目录
|
||||
TOCReference result = new TOCReference(label, resource, fragmentId);
|
||||
//解析子级目录
|
||||
List<TOCReference> childTOCReferences = doToc(navpointElement, book);
|
||||
//readTOCReferences(
|
||||
//navpointElement.getChildNodes(), book);
|
||||
result.setChildren(childTOCReferences);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目录节点的href
|
||||
*
|
||||
* @param navpointElement navpointElement
|
||||
* @return String
|
||||
*/
|
||||
private static String readNavReference(Element navpointElement) {
|
||||
//https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav
|
||||
//父级节点必须是 "li"
|
||||
//Log.d(TAG, "readNavReference:" + navpointElement.getTagName());
|
||||
|
||||
Element contentElement = DOMUtil
|
||||
.getFirstElementByTagNameNS(navpointElement, "", XHTMLTgs.a);
|
||||
if (contentElement == null) {
|
||||
return null;
|
||||
}
|
||||
String result = DOMUtil
|
||||
.getAttribute(contentElement, "", XHTMLAttributes.href);
|
||||
try {
|
||||
result = URLDecoder.decode(result, Constants.CHARACTER_ENCODING);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目录节点里面的章节名
|
||||
*
|
||||
* @param navpointElement navpointElement
|
||||
* @return String
|
||||
*/
|
||||
private static String readNavLabel(Element navpointElement) {
|
||||
//https://www.w3.org/publishing/epub/epub-packages.html#sec-package-nav
|
||||
//父级节点必须是 "li"
|
||||
//Log.d(TAG, "readNavLabel:" + navpointElement.getTagName());
|
||||
String label;
|
||||
Element labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "a");
|
||||
assert labelElement != null;
|
||||
label = labelElement.getTextContent();
|
||||
if (StringUtil.isNotBlank(label)) {
|
||||
return label;
|
||||
} else {
|
||||
labelElement = DOMUtil.getFirstElementByTagNameNS(navpointElement, "", "span");
|
||||
}
|
||||
assert labelElement != null;
|
||||
label = labelElement.getTextContent();
|
||||
//如果通过 a 标签无法获取章节列表,则是无href章节名
|
||||
return label;
|
||||
|
||||
}
|
||||
|
||||
public static Resource createNCXResource(EpubBook book)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
return createNCXResource(book.getMetadata().getIdentifiers(),
|
||||
book.getTitle(), book.getMetadata().getAuthors(),
|
||||
book.getTableOfContents());
|
||||
}
|
||||
|
||||
public static Resource createNCXResource(List<Identifier> identifiers,
|
||||
String title, List<Author> authors, TableOfContents tableOfContents)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
ByteArrayOutputStream data = new ByteArrayOutputStream();
|
||||
XmlSerializer out = EpubProcessorSupport.createXmlSerializer(data);
|
||||
write(out, identifiers, title, authors, tableOfContents);
|
||||
|
||||
Resource resource = new Resource(NCX_ITEM_ID, data.toByteArray(),
|
||||
DEFAULT_NCX_HREF, V3_NCX_MEDIATYPE);
|
||||
resource.setProperties(V3_NCX_PROPERTIES);
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a resource containing an xml document containing the table of contents of the book in ncx format.
|
||||
*
|
||||
* @param xmlSerializer the serializer used
|
||||
* @param book the book to serialize
|
||||
* @throws IOException IOException
|
||||
* @throws IllegalStateException IllegalStateException
|
||||
* @throws IllegalArgumentException IllegalArgumentException
|
||||
*/
|
||||
public static void write(XmlSerializer xmlSerializer, EpubBook book)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
write(xmlSerializer, book.getMetadata().getIdentifiers(), book.getTitle(),
|
||||
book.getMetadata().getAuthors(), book.getTableOfContents());
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入
|
||||
*
|
||||
* @param serializer serializer
|
||||
* @param identifiers identifiers
|
||||
* @param title title
|
||||
* @param authors authors
|
||||
* @param tableOfContents tableOfContents
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static void write(XmlSerializer serializer,
|
||||
List<Identifier> identifiers, String title, List<Author> authors,
|
||||
TableOfContents tableOfContents) throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
serializer.startDocument(Constants.CHARACTER_ENCODING, false);
|
||||
serializer.setPrefix(EpubWriter.EMPTY_NAMESPACE_PREFIX, NAMESPACE_XHTML);
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.html);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xmlns_epub, NAMESPACE_EPUB);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.xml_lang, XHTMLAttributeValues.lang);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, XHTMLAttributes.lang, LANGUAGE);
|
||||
//写入头部head标签
|
||||
writeHead(title, serializer);
|
||||
//body开始
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.body);
|
||||
//h1开始
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h1);
|
||||
serializer.text(title);
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h1);
|
||||
//h1关闭
|
||||
//nav开始
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.nav);
|
||||
serializer.attribute("", XHTMLAttributes.epub_type, XHTMLAttributeValues.epub_type);
|
||||
serializer.attribute("", XHTMLAttributes.id, XHTMLAttributeValues.epub_type);
|
||||
serializer.attribute("", XHTMLAttributes.role, XHTMLAttributeValues.role_toc);
|
||||
//h2开始
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.h2);
|
||||
serializer.text("目录");
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.h2);
|
||||
|
||||
|
||||
writeNavPoints(tableOfContents.getTocReferences(), 1, serializer);
|
||||
|
||||
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.nav);
|
||||
|
||||
//body关闭
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.body);
|
||||
|
||||
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.html);
|
||||
serializer.endDocument();
|
||||
|
||||
}
|
||||
|
||||
private static int writeNavPoints(List<TOCReference> tocReferences,
|
||||
int playOrder,
|
||||
XmlSerializer serializer) throws IOException {
|
||||
writeOlStart(serializer);
|
||||
for (TOCReference tocReference : tocReferences) {
|
||||
if (tocReference.getResource() == null) {
|
||||
playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
|
||||
serializer);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
writeNavPointStart(tocReference, serializer);
|
||||
|
||||
playOrder++;
|
||||
if (!tocReference.getChildren().isEmpty()) {
|
||||
playOrder = writeNavPoints(tocReference.getChildren(), playOrder,
|
||||
serializer);
|
||||
}
|
||||
|
||||
writeNavPointEnd(tocReference, serializer);
|
||||
}
|
||||
writeOlSEnd(serializer);
|
||||
return playOrder;
|
||||
}
|
||||
|
||||
private static void writeNavPointStart(TOCReference tocReference, XmlSerializer serializer) throws IOException {
|
||||
writeLiStart(serializer);
|
||||
String title = tocReference.getTitle();
|
||||
String href = tocReference.getCompleteHref();
|
||||
if (StringUtil.isNotBlank(href)) {
|
||||
writeLabel(title, href, serializer);
|
||||
} else {
|
||||
writeLabel(title, serializer);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void writeNavPointEnd(TOCReference tocReference,
|
||||
XmlSerializer serializer) throws IOException {
|
||||
writeLiEnd(serializer);
|
||||
}
|
||||
|
||||
protected static void writeLabel(String title, String href, XmlSerializer serializer) throws IOException {
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.a);
|
||||
serializer.attribute("", XHTMLAttributes.href, href);
|
||||
//attribute必须在Text之前设置。
|
||||
serializer.text(title);
|
||||
//serializer.attribute(NAMESPACE_XHTML, XHTMLAttributes.href, href);
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.a);
|
||||
}
|
||||
|
||||
protected static void writeLabel(String title, XmlSerializer serializer) throws IOException {
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.span);
|
||||
serializer.text(title);
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.span);
|
||||
}
|
||||
|
||||
private static void writeLiStart(XmlSerializer serializer) throws IOException {
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.li);
|
||||
Log.d(TAG, "writeLiStart");
|
||||
}
|
||||
|
||||
private static void writeLiEnd(XmlSerializer serializer) throws IOException {
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.li);
|
||||
Log.d(TAG, "writeLiEND");
|
||||
}
|
||||
|
||||
private static void writeOlStart(XmlSerializer serializer) throws IOException {
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.ol);
|
||||
Log.d(TAG, "writeOlStart");
|
||||
}
|
||||
|
||||
private static void writeOlSEnd(XmlSerializer serializer) throws IOException {
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.ol);
|
||||
Log.d(TAG, "writeOlEnd");
|
||||
}
|
||||
|
||||
private static void writeHead(String title, XmlSerializer serializer) throws IOException {
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.head);
|
||||
//title
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.title);
|
||||
serializer.text(StringUtil.defaultIfNull(title));
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.title);
|
||||
//link
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.link);
|
||||
serializer.attribute("", XHTMLAttributes.rel, "stylesheet");
|
||||
serializer.attribute("", XHTMLAttributes.type, "text/css");
|
||||
serializer.attribute("", XHTMLAttributes.href, "css/style.css");
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.link);
|
||||
|
||||
//meta
|
||||
serializer.startTag(NAMESPACE_XHTML, XHTMLTgs.meta);
|
||||
serializer.attribute("", XHTMLAttributes.http_equiv, XHTMLAttributeValues.Content_Type);
|
||||
serializer.attribute("", XHTMLAttributes.content, XHTMLAttributeValues.HTML_UTF8);
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.meta);
|
||||
|
||||
serializer.endTag(NAMESPACE_XHTML, XHTMLTgs.head);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
|
||||
/**
|
||||
* Functionality shared by the PackageDocumentReader and the PackageDocumentWriter
|
||||
*
|
||||
* @author paul
|
||||
*/
|
||||
public class PackageDocumentBase {
|
||||
|
||||
public static final String BOOK_ID_ID = "duokan-book-id";
|
||||
public static final String NAMESPACE_OPF = "http://www.idpf.org/2007/opf";
|
||||
public static final String NAMESPACE_DUBLIN_CORE = "http://purl.org/dc/elements/1.1/";
|
||||
public static final String PREFIX_DUBLIN_CORE = "dc";
|
||||
//public static final String PREFIX_OPF = "opf";
|
||||
//在EPUB3标准中,packge前面没有opf头,一些epub阅读器也不支持opf头。
|
||||
//Some Epub Reader not reconize op:packge,So just let it empty;
|
||||
public static final String PREFIX_OPF = "";
|
||||
//添加 version 变量来区分Epub文件的版本
|
||||
//Add the version field to distinguish the version of EPUB file
|
||||
public static final String version = "version";
|
||||
public static final String dateFormat = "yyyy-MM-dd";
|
||||
|
||||
protected interface DCTags {
|
||||
|
||||
String title = "title";
|
||||
String creator = "creator";
|
||||
String subject = "subject";
|
||||
String description = "description";
|
||||
String publisher = "publisher";
|
||||
String contributor = "contributor";
|
||||
String date = "date";
|
||||
String type = "type";
|
||||
String format = "format";
|
||||
String identifier = "identifier";
|
||||
String source = "source";
|
||||
String language = "language";
|
||||
String relation = "relation";
|
||||
String coverage = "coverage";
|
||||
String rights = "rights";
|
||||
}
|
||||
|
||||
protected interface DCAttributes {
|
||||
|
||||
String scheme = "scheme";
|
||||
String id = "id";
|
||||
}
|
||||
|
||||
protected interface OPFTags {
|
||||
|
||||
String metadata = "metadata";
|
||||
String meta = "meta";
|
||||
String manifest = "manifest";
|
||||
String packageTag = "package";
|
||||
String itemref = "itemref";
|
||||
String spine = "spine";
|
||||
String reference = "reference";
|
||||
String guide = "guide";
|
||||
String item = "item";
|
||||
}
|
||||
|
||||
protected interface OPFAttributes {
|
||||
|
||||
String uniqueIdentifier = "unique-identifier";
|
||||
String idref = "idref";
|
||||
String name = "name";
|
||||
String content = "content";
|
||||
String type = "type";
|
||||
String href = "href";
|
||||
String linear = "linear";
|
||||
String event = "event";
|
||||
String role = "role";
|
||||
String file_as = "file-as";
|
||||
String id = "id";
|
||||
String media_type = "media-type";
|
||||
String title = "title";
|
||||
String toc = "toc";
|
||||
String version = "version";
|
||||
String scheme = "scheme";
|
||||
String property = "property";
|
||||
//add for epub3
|
||||
/**
|
||||
* add for epub3
|
||||
*/
|
||||
String properties = "properties";
|
||||
}
|
||||
|
||||
protected interface OPFValues {
|
||||
|
||||
String meta_cover = "cover";
|
||||
String reference_cover = "cover";
|
||||
String no = "no";
|
||||
String generator = "generator";
|
||||
String duokan = "duokan-body-font";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.w3c.dom.Document;
|
||||
import org.w3c.dom.Element;
|
||||
import org.w3c.dom.Node;
|
||||
import org.w3c.dom.NodeList;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
import me.ag2s.epublib.domain.Author;
|
||||
import me.ag2s.epublib.domain.Date;
|
||||
import me.ag2s.epublib.domain.Identifier;
|
||||
import me.ag2s.epublib.domain.Metadata;
|
||||
import me.ag2s.epublib.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Reads the package document metadata.
|
||||
* <p>
|
||||
* In its own separate class because the PackageDocumentReader became a bit large and unwieldy.
|
||||
*
|
||||
* @author paul
|
||||
*/
|
||||
// package
|
||||
class PackageDocumentMetadataReader extends PackageDocumentBase {
|
||||
|
||||
private static final String TAG = PackageDocumentMetadataReader.class.getName();
|
||||
|
||||
public static Metadata readMetadata(Document packageDocument) {
|
||||
Metadata result = new Metadata();
|
||||
Element metadataElement = DOMUtil
|
||||
.getFirstElementByTagNameNS(packageDocument.getDocumentElement(),
|
||||
NAMESPACE_OPF, OPFTags.metadata);
|
||||
if (metadataElement == null) {
|
||||
Log.e(TAG, "Package does not contain element " + OPFTags.metadata);
|
||||
return result;
|
||||
}
|
||||
result.setTitles(DOMUtil
|
||||
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
|
||||
DCTags.title));
|
||||
result.setPublishers(DOMUtil
|
||||
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
|
||||
DCTags.publisher));
|
||||
result.setDescriptions(DOMUtil
|
||||
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
|
||||
DCTags.description));
|
||||
result.setRights(DOMUtil
|
||||
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
|
||||
DCTags.rights));
|
||||
result.setTypes(DOMUtil
|
||||
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
|
||||
DCTags.type));
|
||||
result.setSubjects(DOMUtil
|
||||
.getElementsTextChild(metadataElement, NAMESPACE_DUBLIN_CORE,
|
||||
DCTags.subject));
|
||||
result.setIdentifiers(readIdentifiers(metadataElement));
|
||||
result.setAuthors(readCreators(metadataElement));
|
||||
result.setContributors(readContributors(metadataElement));
|
||||
result.setDates(readDates(metadataElement));
|
||||
result.setOtherProperties(readOtherProperties(metadataElement));
|
||||
result.setMetaAttributes(readMetaProperties(metadataElement));
|
||||
Element languageTag = DOMUtil
|
||||
.getFirstElementByTagNameNS(metadataElement, NAMESPACE_DUBLIN_CORE,
|
||||
DCTags.language);
|
||||
if (languageTag != null) {
|
||||
result.setLanguage(DOMUtil.getTextChildrenContent(languageTag));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* consumes meta tags that have a property attribute as defined in the standard. For example:
|
||||
* <meta property="rendition:layout">pre-paginated</meta>
|
||||
*
|
||||
* @param metadataElement metadataElement
|
||||
* @return Map<QName, String>
|
||||
*/
|
||||
private static Map<QName, String> readOtherProperties(
|
||||
Element metadataElement) {
|
||||
Map<QName, String> result = new HashMap<>();
|
||||
|
||||
NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta);
|
||||
for (int i = 0; i < metaTags.getLength(); i++) {
|
||||
Node metaNode = metaTags.item(i);
|
||||
Node property = metaNode.getAttributes()
|
||||
.getNamedItem(OPFAttributes.property);
|
||||
if (property != null) {
|
||||
String name = property.getNodeValue();
|
||||
String value = metaNode.getTextContent();
|
||||
result.put(new QName(name), value);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* consumes meta tags that have a property attribute as defined in the standard. For example:
|
||||
* <meta property="rendition:layout">pre-paginated</meta>
|
||||
*
|
||||
* @param metadataElement metadataElement
|
||||
* @return Map<String, String>
|
||||
*/
|
||||
private static Map<String, String> readMetaProperties(
|
||||
Element metadataElement) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
|
||||
NodeList metaTags = metadataElement.getElementsByTagName(OPFTags.meta);
|
||||
for (int i = 0; i < metaTags.getLength(); i++) {
|
||||
Element metaElement = (Element) metaTags.item(i);
|
||||
String name = metaElement.getAttribute(OPFAttributes.name);
|
||||
String value = metaElement.getAttribute(OPFAttributes.content);
|
||||
result.put(name, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static String getBookIdId(Document document) {
|
||||
Element packageElement = DOMUtil
|
||||
.getFirstElementByTagNameNS(document.getDocumentElement(),
|
||||
NAMESPACE_OPF, OPFTags.packageTag);
|
||||
if (packageElement == null) {
|
||||
return null;
|
||||
}
|
||||
return DOMUtil.getAttribute(packageElement, NAMESPACE_OPF, OPFAttributes.uniqueIdentifier);
|
||||
|
||||
}
|
||||
|
||||
private static List<Author> readCreators(Element metadataElement) {
|
||||
return readAuthors(DCTags.creator, metadataElement);
|
||||
}
|
||||
|
||||
private static List<Author> readContributors(Element metadataElement) {
|
||||
return readAuthors(DCTags.contributor, metadataElement);
|
||||
}
|
||||
|
||||
private static List<Author> readAuthors(String authorTag,
|
||||
Element metadataElement) {
|
||||
NodeList elements = metadataElement
|
||||
.getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, authorTag);
|
||||
List<Author> result = new ArrayList<>(elements.getLength());
|
||||
for (int i = 0; i < elements.getLength(); i++) {
|
||||
Element authorElement = (Element) elements.item(i);
|
||||
Author author = createAuthor(authorElement);
|
||||
if (author != null) {
|
||||
result.add(author);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
private static List<Date> readDates(Element metadataElement) {
|
||||
NodeList elements = metadataElement
|
||||
.getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.date);
|
||||
List<Date> result = new ArrayList<>(elements.getLength());
|
||||
for (int i = 0; i < elements.getLength(); i++) {
|
||||
Element dateElement = (Element) elements.item(i);
|
||||
Date date;
|
||||
try {
|
||||
date = new Date(DOMUtil.getTextChildrenContent(dateElement),
|
||||
DOMUtil.getAttribute(dateElement, NAMESPACE_OPF, OPFAttributes.event));
|
||||
result.add(date);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, e.getMessage());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
private static Author createAuthor(Element authorElement) {
|
||||
String authorString = DOMUtil.getTextChildrenContent(authorElement);
|
||||
if (StringUtil.isBlank(authorString)) {
|
||||
return null;
|
||||
}
|
||||
int spacePos = authorString.lastIndexOf(' ');
|
||||
Author result;
|
||||
if (spacePos < 0) {
|
||||
result = new Author(authorString);
|
||||
} else {
|
||||
result = new Author(authorString.substring(0, spacePos),
|
||||
authorString.substring(spacePos + 1));
|
||||
}
|
||||
result.setRole(
|
||||
DOMUtil.getAttribute(authorElement, NAMESPACE_OPF, OPFAttributes.role));
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static List<Identifier> readIdentifiers(Element metadataElement) {
|
||||
NodeList identifierElements = metadataElement
|
||||
.getElementsByTagNameNS(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
|
||||
if (identifierElements.getLength() == 0) {
|
||||
Log.e(TAG, "Package does not contain element " + DCTags.identifier);
|
||||
return new ArrayList<>();
|
||||
}
|
||||
String bookIdId = getBookIdId(metadataElement.getOwnerDocument());
|
||||
List<Identifier> result = new ArrayList<>(
|
||||
identifierElements.getLength());
|
||||
for (int i = 0; i < identifierElements.getLength(); i++) {
|
||||
Element identifierElement = (Element) identifierElements.item(i);
|
||||
String schemeName = DOMUtil.getAttribute(identifierElement, NAMESPACE_OPF, DCAttributes.scheme);
|
||||
String identifierValue = DOMUtil
|
||||
.getTextChildrenContent(identifierElement);
|
||||
if (StringUtil.isBlank(identifierValue)) {
|
||||
continue;
|
||||
}
|
||||
Identifier identifier = new Identifier(schemeName, identifierValue);
|
||||
if (identifierElement.getAttribute("id").equals(bookIdId)) {
|
||||
identifier.setBookId(true);
|
||||
}
|
||||
result.add(identifier);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.xml.namespace.QName;
|
||||
|
||||
import me.ag2s.epublib.Constants;
|
||||
import me.ag2s.epublib.domain.Author;
|
||||
import me.ag2s.epublib.domain.Date;
|
||||
import me.ag2s.epublib.domain.EpubBook;
|
||||
import me.ag2s.epublib.domain.Identifier;
|
||||
import me.ag2s.epublib.util.StringUtil;
|
||||
|
||||
public class PackageDocumentMetadataWriter extends PackageDocumentBase {
|
||||
|
||||
/**
|
||||
* Writes the book's metadata.
|
||||
*
|
||||
* @param book book
|
||||
* @param serializer serializer
|
||||
* @throws IOException IOException
|
||||
* @throws IllegalStateException IllegalStateException
|
||||
* @throws IllegalArgumentException IllegalArgumentException
|
||||
*/
|
||||
public static void writeMetaData(EpubBook book, XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.metadata);
|
||||
serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE);
|
||||
serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);
|
||||
|
||||
writeIdentifiers(book.getMetadata().getIdentifiers(), serializer);
|
||||
writeSimpleMetdataElements(DCTags.title, book.getMetadata().getTitles(),
|
||||
serializer);
|
||||
writeSimpleMetdataElements(DCTags.subject, book.getMetadata().getSubjects(),
|
||||
serializer);
|
||||
writeSimpleMetdataElements(DCTags.description,
|
||||
book.getMetadata().getDescriptions(), serializer);
|
||||
writeSimpleMetdataElements(DCTags.publisher,
|
||||
book.getMetadata().getPublishers(), serializer);
|
||||
writeSimpleMetdataElements(DCTags.type, book.getMetadata().getTypes(),
|
||||
serializer);
|
||||
writeSimpleMetdataElements(DCTags.rights, book.getMetadata().getRights(),
|
||||
serializer);
|
||||
|
||||
// write authors
|
||||
for (Author author : book.getMetadata().getAuthors()) {
|
||||
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.creator);
|
||||
serializer.attribute(NAMESPACE_OPF, OPFAttributes.role,
|
||||
author.getRelator().getCode());
|
||||
serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as,
|
||||
author.getLastname() + ", " + author.getFirstname());
|
||||
serializer.text(author.getFirstname() + " " + author.getLastname());
|
||||
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.creator);
|
||||
}
|
||||
|
||||
// write contributors
|
||||
for (Author author : book.getMetadata().getContributors()) {
|
||||
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor);
|
||||
serializer.attribute(NAMESPACE_OPF, OPFAttributes.role,
|
||||
author.getRelator().getCode());
|
||||
serializer.attribute(NAMESPACE_OPF, OPFAttributes.file_as,
|
||||
author.getLastname() + ", " + author.getFirstname());
|
||||
serializer.text(author.getFirstname() + " " + author.getLastname());
|
||||
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.contributor);
|
||||
}
|
||||
|
||||
// write dates
|
||||
for (Date date : book.getMetadata().getDates()) {
|
||||
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.date);
|
||||
if (date.getEvent() != null) {
|
||||
serializer.attribute(NAMESPACE_OPF, OPFAttributes.event,
|
||||
date.getEvent().toString());
|
||||
}
|
||||
serializer.text(date.getValue());
|
||||
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.date);
|
||||
}
|
||||
|
||||
// write language
|
||||
if (StringUtil.isNotBlank(book.getMetadata().getLanguage())) {
|
||||
serializer.startTag(NAMESPACE_DUBLIN_CORE, "language");
|
||||
serializer.text(book.getMetadata().getLanguage());
|
||||
serializer.endTag(NAMESPACE_DUBLIN_CORE, "language");
|
||||
}
|
||||
|
||||
// write other properties
|
||||
if (book.getMetadata().getOtherProperties() != null) {
|
||||
for (Map.Entry<QName, String> mapEntry : book.getMetadata()
|
||||
.getOtherProperties().entrySet()) {
|
||||
serializer.startTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX,
|
||||
OPFAttributes.property, mapEntry.getKey().getLocalPart());
|
||||
serializer.text(mapEntry.getValue());
|
||||
serializer.endTag(mapEntry.getKey().getNamespaceURI(), OPFTags.meta);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// write coverimage
|
||||
if (book.getCoverImage() != null) { // write the cover image
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.meta);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name,
|
||||
OPFValues.meta_cover);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content,
|
||||
book.getCoverImage().getId());
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.meta);
|
||||
}
|
||||
|
||||
// write generator
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.meta);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name,
|
||||
OPFValues.generator);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content,
|
||||
Constants.EPUB_GENERATOR_NAME);
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.meta);
|
||||
|
||||
// write duokan
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.meta);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.name,
|
||||
OPFValues.duokan);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.content,
|
||||
Constants.EPUB_DUOKAN_NAME);
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.meta);
|
||||
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.metadata);
|
||||
}
|
||||
|
||||
private static void writeSimpleMetdataElements(String tagName,
|
||||
List<String> values, XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
for (String value : values) {
|
||||
if (StringUtil.isBlank(value)) {
|
||||
continue;
|
||||
}
|
||||
serializer.startTag(NAMESPACE_DUBLIN_CORE, tagName);
|
||||
serializer.text(value);
|
||||
serializer.endTag(NAMESPACE_DUBLIN_CORE, tagName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Writes out the complete list of Identifiers to the package document.
|
||||
* The first identifier for which the bookId is true is made the bookId identifier.
|
||||
* If no identifier has bookId == true then the first bookId identifier is written as the primary.
|
||||
*
|
||||
* @param identifiers identifiers
|
||||
* @param serializer serializer
|
||||
* @throws IllegalStateException e
|
||||
* @throws IllegalArgumentException e
|
||||
* @
|
||||
*/
|
||||
private static void writeIdentifiers(List<Identifier> identifiers,
|
||||
XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
Identifier bookIdIdentifier = Identifier.getBookIdIdentifier(identifiers);
|
||||
if (bookIdIdentifier == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, DCAttributes.id,
|
||||
BOOK_ID_ID);
|
||||
serializer.attribute(NAMESPACE_OPF, OPFAttributes.scheme,
|
||||
bookIdIdentifier.getScheme());
|
||||
serializer.text(bookIdIdentifier.getValue());
|
||||
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
|
||||
|
||||
for (Identifier identifier : identifiers.subList(1, identifiers.size())) {
|
||||
if (identifier == bookIdIdentifier) {
|
||||
continue;
|
||||
}
|
||||
serializer.startTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
|
||||
serializer.attribute(NAMESPACE_OPF, "scheme", identifier.getScheme());
|
||||
serializer.text(identifier.getValue());
|
||||
serializer.endTag(NAMESPACE_DUBLIN_CORE, DCTags.identifier);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,477 @@
|
||||
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<String, String> 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 文件内容不全而读取不到图片的问题
|
||||
*
|
||||
* @return ,修复图片路径后的一个Element列表
|
||||
* @author qianfanguojin
|
||||
*/
|
||||
private static ArrayList<Element> ensureImageInfo(Resources resources,
|
||||
Element manifestElement) {
|
||||
ArrayList<Element> 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<String, String> 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<Element> 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.
|
||||
* <p>
|
||||
* 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<String, String> 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<SpineReference> 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<String> 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.
|
||||
* <p>
|
||||
* 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<String> findCoverHrefs(Document packageDocument) {
|
||||
|
||||
Set<String> 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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import org.xmlpull.v1.XmlSerializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
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.MediaTypes;
|
||||
import me.ag2s.epublib.domain.Resource;
|
||||
import me.ag2s.epublib.domain.Spine;
|
||||
import me.ag2s.epublib.domain.SpineReference;
|
||||
import me.ag2s.epublib.util.StringUtil;
|
||||
|
||||
/**
|
||||
* Writes the opf package document as defined by namespace http://www.idpf.org/2007/opf
|
||||
*
|
||||
* @author paul
|
||||
*/
|
||||
public class PackageDocumentWriter extends PackageDocumentBase {
|
||||
|
||||
private static final String TAG = PackageDocumentWriter.class.getName();
|
||||
|
||||
public static void write(EpubWriter epubWriter, XmlSerializer serializer,
|
||||
EpubBook book) {
|
||||
try {
|
||||
serializer.startDocument(Constants.CHARACTER_ENCODING, false);
|
||||
serializer.setPrefix(PREFIX_OPF, NAMESPACE_OPF);
|
||||
serializer.setPrefix(PREFIX_DUBLIN_CORE, NAMESPACE_DUBLIN_CORE);
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.packageTag);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.version,
|
||||
book.getVersion());
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX,
|
||||
OPFAttributes.uniqueIdentifier, BOOK_ID_ID);
|
||||
|
||||
PackageDocumentMetadataWriter.writeMetaData(book, serializer);
|
||||
|
||||
writeManifest(book, epubWriter, serializer);
|
||||
writeSpine(book, epubWriter, serializer);
|
||||
writeGuide(book, epubWriter, serializer);
|
||||
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.packageTag);
|
||||
serializer.endDocument();
|
||||
serializer.flush();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the package's spine.
|
||||
*
|
||||
* @param book e
|
||||
* @param epubWriter g
|
||||
* @param serializer g
|
||||
* @throws IOException g
|
||||
* @throws IllegalStateException g
|
||||
* @throws IllegalArgumentException 1@throws XMLStreamException
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
private static void writeSpine(EpubBook book, EpubWriter epubWriter,
|
||||
XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.spine);
|
||||
Resource tocResource = book.getSpine().getTocResource();
|
||||
String tocResourceId = tocResource.getId();
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.toc,
|
||||
tocResourceId);
|
||||
|
||||
if (book.getCoverPage() != null // there is a cover page
|
||||
&& book.getSpine().findFirstResourceById(book.getCoverPage().getId())
|
||||
< 0) { // cover page is not already in the spine
|
||||
// write the cover html file
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.itemref);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref,
|
||||
book.getCoverPage().getId());
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear,
|
||||
"no");
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.itemref);
|
||||
}
|
||||
writeSpineItems(book.getSpine(), serializer);
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.spine);
|
||||
}
|
||||
|
||||
|
||||
private static void writeManifest(EpubBook book, EpubWriter epubWriter,
|
||||
XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.manifest);
|
||||
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.item);
|
||||
|
||||
//For EPUB3
|
||||
if (book.isEpub3()) {
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.properties, NCXDocumentV3.V3_NCX_PROPERTIES);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id, NCXDocumentV3.NCX_ITEM_ID);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, NCXDocumentV3.DEFAULT_NCX_HREF);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, NCXDocumentV3.V3_NCX_MEDIATYPE.getName());
|
||||
} else {
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id,
|
||||
epubWriter.getNcxId());
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href, epubWriter.getNcxHref());
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type, epubWriter.getNcxMediaType());
|
||||
}
|
||||
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.item);
|
||||
|
||||
// writeCoverResources(book, serializer);
|
||||
|
||||
for (Resource resource : getAllResourcesSortById(book)) {
|
||||
writeItem(book, resource, serializer);
|
||||
}
|
||||
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.manifest);
|
||||
}
|
||||
|
||||
private static List<Resource> getAllResourcesSortById(EpubBook book) {
|
||||
List<Resource> allResources = new ArrayList<>(
|
||||
book.getResources().getAll());
|
||||
Collections.sort(allResources, (resource1, resource2) -> resource1.getId().compareToIgnoreCase(resource2.getId()));
|
||||
return allResources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a resources as an item element
|
||||
*
|
||||
* @param resource g
|
||||
* @param serializer g
|
||||
* @throws IOException g
|
||||
* @throws IllegalStateException g
|
||||
* @throws IllegalArgumentException 1@throws XMLStreamException
|
||||
*/
|
||||
private static void writeItem(EpubBook book, Resource resource,
|
||||
XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
if (resource == null ||
|
||||
(resource.getMediaType() == MediaTypes.NCX
|
||||
&& book.getSpine().getTocResource() != null)) {
|
||||
return;
|
||||
}
|
||||
if (StringUtil.isBlank(resource.getId())) {
|
||||
Log.e(TAG, "resource id must not be empty (href: " + resource.getHref()
|
||||
+ ", mediatype:" + resource.getMediaType() + ")");
|
||||
return;
|
||||
}
|
||||
if (StringUtil.isBlank(resource.getHref())) {
|
||||
Log.e(TAG, "resource href must not be empty (id: " + resource.getId()
|
||||
+ ", mediatype:" + resource.getMediaType() + ")");
|
||||
return;
|
||||
}
|
||||
if (resource.getMediaType() == null) {
|
||||
Log.e(TAG, "resource mediatype must not be empty (id: " + resource.getId()
|
||||
+ ", href:" + resource.getHref() + ")");
|
||||
return;
|
||||
}
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.item);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.id,
|
||||
resource.getId());
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href,
|
||||
resource.getHref());
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.media_type,
|
||||
resource.getMediaType().getName());
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.item);
|
||||
}
|
||||
|
||||
/**
|
||||
* List all spine references
|
||||
*
|
||||
* @throws IOException f
|
||||
* @throws IllegalStateException f
|
||||
* @throws IllegalArgumentException f
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
private static void writeSpineItems(Spine spine, XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
for (SpineReference spineReference : spine.getSpineReferences()) {
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.itemref);
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.idref,
|
||||
spineReference.getResourceId());
|
||||
if (!spineReference.isLinear()) {
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.linear,
|
||||
OPFValues.no);
|
||||
}
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.itemref);
|
||||
}
|
||||
}
|
||||
|
||||
private static void writeGuide(EpubBook book, EpubWriter epubWriter,
|
||||
XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.guide);
|
||||
ensureCoverPageGuideReferenceWritten(book.getGuide(), epubWriter,
|
||||
serializer);
|
||||
for (GuideReference reference : book.getGuide().getReferences()) {
|
||||
writeGuideReference(reference, serializer);
|
||||
}
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.guide);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static void ensureCoverPageGuideReferenceWritten(Guide guide,
|
||||
EpubWriter epubWriter, XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
if (!(guide.getGuideReferencesByType(GuideReference.COVER).isEmpty())) {
|
||||
return;
|
||||
}
|
||||
Resource coverPage = guide.getCoverPage();
|
||||
if (coverPage != null) {
|
||||
writeGuideReference(
|
||||
new GuideReference(guide.getCoverPage(), GuideReference.COVER,
|
||||
GuideReference.COVER), serializer);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void writeGuideReference(GuideReference reference,
|
||||
XmlSerializer serializer)
|
||||
throws IllegalArgumentException, IllegalStateException, IOException {
|
||||
if (reference == null) {
|
||||
return;
|
||||
}
|
||||
serializer.startTag(NAMESPACE_OPF, OPFTags.reference);
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.type,
|
||||
reference.getType());
|
||||
serializer.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.href,
|
||||
reference.getCompleteHref());
|
||||
if (StringUtil.isNotBlank(reference.getTitle())) {
|
||||
serializer
|
||||
.attribute(EpubWriter.EMPTY_NAMESPACE_PREFIX, OPFAttributes.title,
|
||||
reference.getTitle());
|
||||
}
|
||||
serializer.endTag(NAMESPACE_OPF, OPFTags.reference);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
package me.ag2s.epublib.epub;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipException;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
import me.ag2s.epublib.domain.EpubResourceProvider;
|
||||
import me.ag2s.epublib.domain.LazyResource;
|
||||
import me.ag2s.epublib.domain.LazyResourceProvider;
|
||||
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.util.CollectionUtil;
|
||||
import me.ag2s.epublib.util.ResourceUtil;
|
||||
import me.ag2s.epublib.util.zip.ZipEntryWrapper;
|
||||
import me.ag2s.epublib.util.zip.ZipFileWrapper;
|
||||
|
||||
|
||||
/**
|
||||
* Loads Resources from inputStreams, ZipFiles, etc
|
||||
*
|
||||
* @author paul
|
||||
*/
|
||||
public class ResourcesLoader {
|
||||
|
||||
private static final String TAG = ResourcesLoader.class.getName();
|
||||
|
||||
|
||||
/**
|
||||
* Loads the entries of the zipFileWrapper as resources.
|
||||
* <p>
|
||||
* The MediaTypes that are in the lazyLoadedTypes will not get their
|
||||
* contents loaded, but are stored as references to entries into the
|
||||
* AndroidZipFile and are loaded on demand by the Resource system.
|
||||
*
|
||||
* @param zipFileWrapper import epub zipfile
|
||||
* @param defaultHtmlEncoding epub xhtml default encoding
|
||||
* @param lazyLoadedTypes lazyLoadedTypes
|
||||
* @return Resources
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
public static Resources loadResources(
|
||||
ZipFileWrapper zipFileWrapper,
|
||||
String defaultHtmlEncoding,
|
||||
List<MediaType> lazyLoadedTypes
|
||||
) throws IOException {
|
||||
|
||||
LazyResourceProvider resourceProvider =
|
||||
new EpubResourceProvider(zipFileWrapper);
|
||||
|
||||
Resources result = new Resources();
|
||||
Enumeration entries = zipFileWrapper.entries();
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntryWrapper zipEntry = new ZipEntryWrapper(entries.nextElement());
|
||||
|
||||
if (zipEntry == null || zipEntry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String href = zipEntry.getName();
|
||||
|
||||
Resource resource;
|
||||
|
||||
if (shouldLoadLazy(href, lazyLoadedTypes)) {
|
||||
resource = new LazyResource(resourceProvider, zipEntry.getSize(), href);
|
||||
} else {
|
||||
resource = ResourceUtil
|
||||
.createResource(zipEntry.getName(), zipFileWrapper.getInputStream(zipEntry));
|
||||
/*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/
|
||||
if (href.endsWith("opf")) {
|
||||
String string = new String(resource.getData())
|
||||
.replace(" smlns=\"", " xmlns=\"")
|
||||
.replace(" mlns=\"", " xmlns=\"");
|
||||
resource.setData(string.getBytes());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (resource.getMediaType() == MediaTypes.XHTML) {
|
||||
resource.setInputEncoding(defaultHtmlEncoding);
|
||||
}
|
||||
result.add(resource);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the given href will load a mediaType that is in the
|
||||
* collection of lazilyLoadedMediaTypes.
|
||||
*
|
||||
* @param href href
|
||||
* @param lazilyLoadedMediaTypes lazilyLoadedMediaTypes
|
||||
* @return Whether the given href will load a mediaType that is
|
||||
* in the collection of lazilyLoadedMediaTypes.
|
||||
*/
|
||||
private static boolean shouldLoadLazy(String href,
|
||||
Collection<MediaType> lazilyLoadedMediaTypes) {
|
||||
if (CollectionUtil.isEmpty(lazilyLoadedMediaTypes)) {
|
||||
return false;
|
||||
}
|
||||
MediaType mediaType = MediaTypes.determineMediaType(href);
|
||||
return lazilyLoadedMediaTypes.contains(mediaType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all entries from the ZipInputStream as Resources.
|
||||
* <p>
|
||||
* Loads the contents of all ZipEntries into memory.
|
||||
* Is fast, but may lead to memory problems when reading large books
|
||||
* on devices with small amounts of memory.
|
||||
*
|
||||
* @param zipInputStream zipInputStream
|
||||
* @param defaultHtmlEncoding defaultHtmlEncoding
|
||||
* @return Resources
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
public static Resources loadResources(ZipInputStream zipInputStream,
|
||||
String defaultHtmlEncoding) throws IOException {
|
||||
Resources result = new Resources();
|
||||
ZipEntry zipEntry;
|
||||
do {
|
||||
// get next valid zipEntry
|
||||
zipEntry = getNextZipEntry(zipInputStream);
|
||||
if ((zipEntry == null) || zipEntry.isDirectory()) {
|
||||
continue;
|
||||
}
|
||||
String href = zipEntry.getName();
|
||||
|
||||
// store resource
|
||||
Resource resource = ResourceUtil.createResource(zipEntry.getName(), zipInputStream);
|
||||
///*掌上书苑有很多自制书OPF的nameSpace格式不标准,强制修复成正确的格式*/
|
||||
if (href.endsWith("opf")) {
|
||||
String string = new String(resource.getData())
|
||||
.replace(" smlns=\"", " xmlns=\"")
|
||||
.replace(" mlns=\"", " xmlns=\"");
|
||||
resource.setData(string.getBytes());
|
||||
}
|
||||
if (resource.getMediaType() == MediaTypes.XHTML) {
|
||||
resource.setInputEncoding(defaultHtmlEncoding);
|
||||
}
|
||||
result.add(resource);
|
||||
} while (zipEntry != null);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
private static ZipEntry getNextZipEntry(ZipInputStream zipInputStream)
|
||||
throws IOException {
|
||||
try {
|
||||
return zipInputStream.getNextEntry();
|
||||
} catch (ZipException e) {
|
||||
//see <a href="https://github.com/psiegman/epublib/issues/122">Issue #122 Infinite loop</a>.
|
||||
//when reading a file that is not a real zip archive or a zero length file, zipInputStream.getNextEntry()
|
||||
//throws an exception and does not advance, so loadResources enters an infinite loop
|
||||
//log.error("Invalid or damaged zip file.", e);
|
||||
Log.e(TAG, e.getLocalizedMessage());
|
||||
try {
|
||||
zipInputStream.closeEntry();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all entries from the ZipInputStream as Resources.
|
||||
* <p>
|
||||
* Loads the contents of all ZipEntries into memory.
|
||||
* Is fast, but may lead to memory problems when reading large books
|
||||
* on devices with small amounts of memory.
|
||||
*
|
||||
* @param zipFile zipFile
|
||||
* @param defaultHtmlEncoding defaultHtmlEncoding
|
||||
* @return Resources
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
public static Resources loadResources(ZipFileWrapper zipFile, String defaultHtmlEncoding) throws IOException {
|
||||
List<MediaType> ls = new ArrayList<>();
|
||||
return loadResources(zipFile, defaultHtmlEncoding, ls);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user