/*
The MIT License (MIT)
Copyright (c) 2013, V. Giacometti, M. Giuriato, B. Petrantuono
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package it.angrydroids.epub3reader;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import nl.siegmann.epublib.domain.Author;
import nl.siegmann.epublib.domain.Book;
import nl.siegmann.epublib.domain.Metadata;
import nl.siegmann.epublib.domain.Spine;
import nl.siegmann.epublib.domain.SpineReference;
import nl.siegmann.epublib.domain.TOCReference;
import nl.siegmann.epublib.domain.TableOfContents;
import nl.siegmann.epublib.epub.EpubReader;
import android.content.Context;
import android.os.Environment;
import android.util.Log;
public class EpubManipulator {
private Book book;
private int currentSpineElementIndex;
private String currentPage;
private String[] spineElementPaths;
// NOTE: currently, counting the number of XHTML pages
private int pageCount;
private int currentLanguage;
private List<String> availableLanguages;
// tells whether a page has a translation available
private List<Boolean> translations;
private String decompressedFolder;
private String pathOPF;
private static Context context;
private static String location = Environment.getExternalStorageDirectory()
+ "/epubtemp/";
private String fileName;
FileInputStream fs;
// book from fileName
public EpubManipulator(String fileName, String destFolder,
Context theContext) throws Exception {
List<String> spineElements;
List<SpineReference> spineList;
if (context == null) {
context = theContext;
}
this.fs = new FileInputStream(fileName);
this.book = (new EpubReader()).readEpub(fs);
this.fileName = fileName;
this.decompressedFolder = destFolder;
Spine spine = book.getSpine();
spineList = spine.getSpineReferences();
this.currentSpineElementIndex = 0;
this.currentLanguage = 0;
spineElements = new ArrayList<String>();
pages(spineList, spineElements);
this.pageCount = spineElements.size();
this.spineElementPaths = new String[spineElements.size()];
unzip(fileName, location + decompressedFolder);
pathOPF = getPathOPF(location + decompressedFolder);
for (int i = 0; i < spineElements.size(); ++i) {
// TODO: is there a robust path joiner in the java libs?
this.spineElementPaths[i] = "file://" + location
+ decompressedFolder + "/" + pathOPF + "/"
+ spineElements.get(i);
}
if (spineElements.size() > 0) {
goToPage(0);
}
}
// book from already decompressed folder
public EpubManipulator(String fileName, String folder, int spineIndex,
int language, Context theContext) throws Exception {
List<String> spineElements;
List<SpineReference> spineList;
if (context == null) {
context = theContext;
}
this.fs = new FileInputStream(fileName);
this.book = (new EpubReader()).readEpub(fs);
this.fileName = fileName;
this.decompressedFolder = folder;
Spine spine = book.getSpine();
spineList = spine.getSpineReferences();
this.currentSpineElementIndex = spineIndex;
this.currentLanguage = language;
spineElements = new ArrayList<String>();
pages(spineList, spineElements);
this.pageCount = spineElements.size();
this.spineElementPaths = new String[spineElements.size()];
pathOPF = getPathOPF(location + folder);
for (int i = 0; i < spineElements.size(); ++i) {
// TODO: is there a robust path joiner in the java libs?
this.spineElementPaths[i] = "file://" + location + folder + "/"
+ pathOPF + "/" + spineElements.get(i);
}
goToPage(spineIndex);
}
// set language from index
public void setLanguage(int lang) throws Exception {
if ((lang >= 0) && (lang <= this.availableLanguages.size())) {
this.currentLanguage = lang;
}
goToPage(this.currentSpineElementIndex);
}
// set language from an identifier string
public void setLanguage(String lang) throws Exception {
int i = 0;
while ((i < this.availableLanguages.size())
&& (!(this.availableLanguages.get(i).equals(lang)))) {
i++;
}
setLanguage(i);
}
// TODO: lookup table of language names from language codes
public String[] getLanguages() {
String[] lang = new String[availableLanguages.size()];
for (int i = 0; i < availableLanguages.size(); i++) {
lang[i] = availableLanguages.get(i);
}
return lang;
}
// create parallel text mapping
private void pages(List<SpineReference> spineList, List<String> pages) {
int langIndex;
String lang;
String actualPage;
this.translations = new ArrayList<Boolean>();
this.availableLanguages = new ArrayList<String>();
for (int i = 0; i < spineList.size(); ++i) {
actualPage = (spineList.get(i)).getResource().getHref();
lang = getPageLanguage(actualPage);
if (lang != "") {
// parallel text available
langIndex = languageIndexFromID(lang);
if (langIndex == this.availableLanguages.size())
this.availableLanguages.add(lang);
if (langIndex == 0) {
this.translations.add(true);
pages.add(actualPage);
}
} else {
// parallel text NOT available
this.translations.add(false);
pages.add(actualPage);
}
}
}
// language index from language string (id)
private int languageIndexFromID(String id) {
int i = 0;
while ((i < availableLanguages.size())
&& (!(availableLanguages.get(i).equals(id)))) {
i++;
}
return i;
}
// TODO: better parsing
private static String getPathOPF(String unzipDir) throws IOException {
String pathOPF = "";
// get the OPF path, directly from container.xml
BufferedReader br = new BufferedReader(new FileReader(unzipDir
+ "/META-INF/container.xml"));
String line;
while ((line = br.readLine()) != null) {
if (line.indexOf(getS(R.string.full_path)) > -1) {
int start = line.indexOf(getS(R.string.full_path));
int start2 = line.indexOf("\"", start);
int stop2 = line.indexOf("\"", start2 + 1);
if (start2 > -1 && stop2 > start2) {
pathOPF = line.substring(start2 + 1, stop2).trim();
break;
}
}
}
br.close();
// in case the OPF file is in the root directory
if (!pathOPF.contains("/"))
pathOPF = "";
// remove the OPF file name and the preceding '/'
int last = pathOPF.lastIndexOf('/');
if (last > -1) {
pathOPF = pathOPF.substring(0, last);
}
return pathOPF;
}
// TODO: more efficient unzipping
public void unzip(String inputZip, String destinationDirectory)
throws IOException {
int BUFFER = 2048;
List zipFiles = new ArrayList();
File sourceZipFile = new File(inputZip);
File unzipDestinationDirectory = new File(destinationDirectory);
unzipDestinationDirectory.mkdir();
ZipFile zipFile;
zipFile = new ZipFile(sourceZipFile, ZipFile.OPEN_READ);
Enumeration zipFileEntries = zipFile.entries();
// Process each entry
while (zipFileEntries.hasMoreElements()) {
ZipEntry entry = (ZipEntry) zipFileEntries.nextElement();
String currentEntry = entry.getName();
File destFile = new File(unzipDestinationDirectory, currentEntry);
if (currentEntry.endsWith(getS(R.string.zip))) {
zipFiles.add(destFile.getAbsolutePath());
}
File destinationParent = destFile.getParentFile();
destinationParent.mkdirs();
if (!entry.isDirectory()) {
BufferedInputStream is = new BufferedInputStream(
zipFile.getInputStream(entry));
int currentByte;
// buffer for writing file
byte data[] = new byte[BUFFER];
FileOutputStream fos = new FileOutputStream(destFile);
BufferedOutputStream dest = new BufferedOutputStream(fos,
BUFFER);
while ((currentByte = is.read(data, 0, BUFFER)) != -1) {
dest.write(data, 0, currentByte);
}
dest.flush();
dest.close();
is.close();
}
}
zipFile.close();
for (Iterator iter = zipFiles.iterator(); iter.hasNext();) {
String zipName = (String) iter.next();
unzip(zipName,
destinationDirectory
+ File.separatorChar
+ zipName.substring(0,
zipName.lastIndexOf(getS(R.string.zip))));
}
}
public void closeStream() throws IOException {
fs.close();
}
// close the stream and delete the extraction folder
public void destroy() throws IOException {
fs.close();
File c = new File(location + decompressedFolder);
deleteDir(c);
}
// recursively delete a directory
private void deleteDir(File f) {
if (f.isDirectory())
for (File child : f.listFiles())
deleteDir(child);
f.delete();
}
// obtain a page in the current language
public String goToPage(int page) throws Exception {
return goToPage(page, this.currentLanguage);
}
// obtain a page in the given language
public String goToPage(int page, int lang) throws Exception {
String spineElement;
String extension;
if (page < 0) {
page = 0;
}
if (page >= this.pageCount) {
page = this.pageCount - 1;
}
this.currentSpineElementIndex = page;
spineElement = this.spineElementPaths[currentSpineElementIndex];
// TODO: better parsing
if (this.translations.get(page)) {
extension = spineElement.substring(spineElement.lastIndexOf("."));
spineElement = spineElement.substring(0,
spineElement.lastIndexOf(this.availableLanguages.get(0)));
spineElement = spineElement + this.availableLanguages.get(lang)
+ extension;
}
this.currentPage = spineElement;
// addCSS(spineElement, EpubReaderMain.getColor()); work in progress
return spineElement;
}
public String goToNextChapter() throws Exception {
return goToPage(this.currentSpineElementIndex + 1);
}
public String goToPreviousChapter() throws Exception {
return goToPage(this.currentSpineElementIndex - 1);
}
// create an HTML page with book metadata
// TODO: style it and escape metadata values
// TODO: use StringBuilder
public String metadata() {
List<String> tmp;
Metadata metadata = book.getMetadata();
String html = getS(R.string.htmlBodyTableOpen);
// Titles
tmp = metadata.getTitles();
if (tmp.size() > 0) {
html += getS(R.string.titlesMeta);
html += "<td>" + tmp.get(0) + "</td></tr>";
for (int i = 1; i < tmp.size(); i++)
html += "<tr><td></td><td>" + tmp.get(i) + "</td></tr>";
}
// Authors
List<Author> authors = metadata.getAuthors();
if (authors.size() > 0) {
html += getS(R.string.authorsMeta);
html += "<td>" + authors.get(0).getFirstname() + " "
+ authors.get(0).getLastname() + "</td></tr>";
for (int i = 1; i < authors.size(); i++)
html += "<tr><td></td><td>" + authors.get(i).getFirstname()
+ " " + authors.get(i).getLastname() + "</td></tr>";
}
// Contributors
authors = metadata.getContributors();
if (authors.size() > 0) {
html += getS(R.string.contributorsMeta);
html += "<td>" + authors.get(0).getFirstname() + " "
+ authors.get(0).getLastname() + "</td></tr>";
for (int i = 1; i < authors.size(); i++) {
html += "<tr><td></td><td>" + authors.get(i).getFirstname()
+ " " + authors.get(i).getLastname() + "</td></tr>";
}
}
// TODO: extend lib to get multiple languages?
// Language
html += getS(R.string.languageMeta) + metadata.getLanguage()
+ "</td></tr>";
// Publishers
tmp = metadata.getPublishers();
if (tmp.size() > 0) {
html += getS(R.string.publishersMeta);
html += "<td>" + tmp.get(0) + "</td></tr>";
for (int i = 1; i < tmp.size(); i++)
html += "<tr><td></td><td>" + tmp.get(i) + "</td></tr>";
}
// Types
tmp = metadata.getTypes();
if (tmp.size() > 0) {
html += getS(R.string.typesMeta);
html += "<td>" + tmp.get(0) + "</td></tr>";
for (int i = 1; i < tmp.size(); i++)
html += "<tr><td></td><td>" + tmp.get(i) + "</td></tr>";
}
// Descriptions
tmp = metadata.getDescriptions();
if (tmp.size() > 0) {
html += getS(R.string.descriptionsMeta);
html += "<td>" + tmp.get(0) + "</td></tr>";
for (int i = 1; i < tmp.size(); i++)
html += "<tr><td></td><td>" + tmp.get(i) + "</td></tr>";
}
// Rights
tmp = metadata.getRights();
if (tmp.size() > 0) {
html += getS(R.string.rightsMeta);
html += "<td>" + tmp.get(0) + "</td></tr>";
for (int i = 1; i < tmp.size(); i++)
html += "<tr><td></td><td>" + tmp.get(i) + "</td></tr>";
}
html += getS(R.string.tablebodyhtmlClose);
return html;
}
// Create an html file, which contain the TOC, in the EPUB folder
public void createTocFile() {
List<TOCReference> tmp;
TableOfContents toc = book.getTableOfContents();
String html = getS(R.string.htmlBodyTableOpen);
tmp = toc.getTocReferences();
if (tmp.size() > 0) {
html += getS(R.string.tocReference);
for (int i = 0; i < tmp.size(); i++) {
String path = "file://" + location + decompressedFolder + "/"
+ pathOPF + "/" + tmp.get(i).getCompleteHref();
html += "<tr><td></td><td>" + "<a href=\"" + path + "\">"
+ tmp.get(i).getTitle() + "</a>" + "</td></tr>";
// pre-order traversal?
List<TOCReference> children = tmp.get(i).getChildren();
for (int j = 0; j < children.size(); j++) {
String childrenPath = "file://" + location
+ decompressedFolder + "/" + pathOPF + "/"
+ children.get(j).getCompleteHref();
html += "<tr><td></td><td>" + "<a href=\"" + childrenPath
+ "\">" + children.get(j).getTitle() + "</a>"
+ "</td></tr>";
}
}
}
html += getS(R.string.tablebodyhtmlClose);
// write down the html file
String filePath = location + decompressedFolder + "/Toc.html";
try {
File file = new File(filePath);
FileWriter fw = new FileWriter(file);
fw.write(html);
fw.flush();
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
// return the path of the Toc.html file
public String tableOfContents() {
return "File://" + location + decompressedFolder + "/Toc.html";
}
// determine whether a book has the requested page
// if so, return its index; return -1 otherwise
public int getPageIndex(String page) {
int result = -1;
String lang;
lang = getPageLanguage(page);
if ((this.availableLanguages.size() > 0) && (lang != "")) {
page = page.substring(0, page.lastIndexOf(lang))
+ this.availableLanguages.get(0)
+ page.substring(page.lastIndexOf("."));
}
for (int i = 0; i < this.spineElementPaths.length && result == -1; i++) {
if (page.equals(this.spineElementPaths[i])) {
result = i;
}
}
return result;
}
// set the current page and its language
public boolean goToPage(String page) {
int index = getPageIndex(page);
boolean res = false;
if (index >= 0) {
String newLang = getPageLanguage(page);
try {
goToPage(index);
if (newLang != "") {
setLanguage(newLang);
}
res = true;
} catch (Exception e) {
res = false;
Log.e(getS(R.string.error_goToPage), e.getMessage());
}
}
return res;
}
// return the language of the page according to the
// ISO 639-1 naming convention:
// foo.XX.html where X \in [a-z]
// or an empty string if language not found
public String getPageLanguage(String page) {
String[] tmp = page.split("\\.");
// Language XY is present if the string format is "pagename.XY.xhtml",
// where XY are 2 non-numeric characters that identify the language
if (tmp.length > 2) {
String secondFromLastItem = tmp[tmp.length - 2];
if (secondFromLastItem.matches("[a-z][a-z]")) {
return secondFromLastItem;
}
}
return "";
}
// TODO work in progress
public void addCSS(String path, String color) { // metodo per modificare
// CSS
path = path.replace("file:///", "");
String source = readPage(path);
source = source.replace("<style type=\"text/css\">*</style></head>",
"</head>");
String css = "<style type=\"text/css\">\n"; // denota carattere
// speciale \
// Test: ridimensionamento font
css = css + "p{\n\tfont-size: 120%\n}\n";
css = css + "body{color:" + color + ";}";
// Tag di chiusura
css = css + "</style>";
source = source.replace("</head>", css + "</head>");
writePage(path, source);
}
// TODO don't work properly
public boolean deleteCSS(String path) {
path = path.replace("file:///", "");
String source = readPage(path);
source = source.replace("<style type=\"text/css\">.</style></head>",
"</head>");
return writePage(path, source);
}
// TODO work in progress
private String readPage(String path) {
try {
FileInputStream input = new FileInputStream(path);
byte[] fileData = new byte[input.available()];
input.read(fileData);
input.close();
String xhtml = new String(fileData);
return xhtml;
} catch (IOException e) {
return "";
}
}
// TODO work in progress
private boolean writePage(String path, String xhtml) {
try {
File file = new File(path);
FileWriter fw = new FileWriter(file);
fw.write(xhtml);
fw.flush();
fw.close();
return true;
} catch (IOException e) {
return false;
}
}
public int getCurrentSpineElementIndex() {
return currentSpineElementIndex;
}
public String getSpineElementPath(int elementIndex) {
return spineElementPaths[elementIndex];
}
public String getCurrentPageURL() {
return currentPage;
}
public int getCurrentLanguage() {
return currentLanguage;
}
public String getFileName() {
return fileName;
}
public String getDecompressedFolder() {
return decompressedFolder;
}
public static String getS(int id) {
return context.getResources().getString(id);
}
}