package org.ebookdroid.droids.fb2.codec;
import static org.ebookdroid.droids.fb2.codec.FB2Page.MARGIN_X;
import static org.ebookdroid.droids.fb2.codec.FB2Page.MARGIN_Y;
import static org.ebookdroid.droids.fb2.codec.FB2Page.PAGE_HEIGHT;
import static org.ebookdroid.droids.fb2.codec.FB2Page.PAGE_WIDTH;
import org.ebookdroid.common.settings.AppSettings;
import org.ebookdroid.core.codec.AbstractCodecDocument;
import org.ebookdroid.core.codec.CodecPage;
import org.ebookdroid.core.codec.CodecPageInfo;
import org.ebookdroid.core.codec.OutlineLink;
import org.ebookdroid.droids.fb2.codec.handlers.StandardHandler;
import org.ebookdroid.droids.fb2.codec.tags.FB2TagFactory;
import android.graphics.Bitmap;
import android.graphics.RectF;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.concurrent.atomic.AtomicLong;
import org.emdev.common.archives.zip.ZipArchive;
import org.emdev.common.archives.zip.ZipArchiveEntry;
import org.emdev.common.textmarkup.JustificationMode;
import org.emdev.common.textmarkup.MarkupElement;
import org.emdev.common.textmarkup.MarkupTitle;
import org.emdev.common.textmarkup.TextStyle;
import org.emdev.common.textmarkup.line.HorizontalRule;
import org.emdev.common.textmarkup.line.Line;
import org.emdev.common.textmarkup.line.LineStream;
import org.emdev.common.xml.TextProvider;
import org.emdev.common.xml.parsers.DuckbillParser;
import org.emdev.common.xml.parsers.VTDExParser;
import org.emdev.utils.LengthUtils;
import com.ximpleware.VTDGenEx;
public class FB2Document extends AbstractCodecDocument {
private final ArrayList<FB2Page> pages = new ArrayList<FB2Page>();
private final List<OutlineLink> outline = new ArrayList<OutlineLink>();
private final ParsedContent content = new ParsedContent();
public FB2Document(final FB2Context context, final String fileName) {
super(context, context.getContextHandle());
final long t1 = System.currentTimeMillis();
content.loadFonts();
final long t2 = System.currentTimeMillis();
System.out.println("Fonts preloading: " + (t2 - t1) + " ms");
switch (AppSettings.current().fb2XmlParser) {
case VTDEx:
parseWithVTDEx(fileName);
break;
case Duckbill:
default:
parseWithDuckbill(fileName);
break;
}
final long t3 = System.currentTimeMillis();
final ArrayList<MarkupElement> mainStream = content.getMarkupStream(null);
final LineStream documentLines = content.createLines(mainStream, PAGE_WIDTH - 2 * MARGIN_X,
JustificationMode.Justify, AppSettings.current().fb2HyphenEnabled);
createPages(documentLines);
final long t4 = System.currentTimeMillis();
System.out.println("Markup: " + (t4 - t3) + " ms");
final int removed = removeEmptyPages(true);
content.clear();
final long t5 = System.currentTimeMillis();
System.out.println("Cleanup: " + (t5 - t4) + " ms, removed: " + removed);
}
private void createPages(final LineStream documentLines) {
pages.clear();
if (LengthUtils.isEmpty(documentLines)) {
return;
}
for (final Line line : documentLines) {
FB2Page lastPage = getLastPage();
if (lastPage.contentHeight + 2 * MARGIN_Y + line.getTotalHeight() > PAGE_HEIGHT) {
commitPage();
lastPage = new FB2Page();
pages.add(lastPage);
}
lastPage.appendLine(line);
final MarkupTitle title = line.getTitle();
if (title != null) {
addTitle(title);
}
final LineStream footnotes = line.getFootNotes();
if (footnotes != null) {
final Iterator<Line> iterator = footnotes.iterator();
if (lastPage.noteLines.size() > 0 && iterator.hasNext()) {
// Skip rule for non first note on page
iterator.next();
}
while (iterator.hasNext()) {
final Line l = iterator.next();
lastPage = getLastPage();
if (lastPage.contentHeight + 2 * MARGIN_Y + l.getTotalHeight() > PAGE_HEIGHT) {
commitPage();
lastPage = new FB2Page();
pages.add(lastPage);
final Line ruleLine = new Line(content, PAGE_WIDTH / 4, JustificationMode.Left);
ruleLine.append(new HorizontalRule(PAGE_WIDTH / 4, TextStyle.FOOTNOTE.getFontSize()));
ruleLine.applyJustification(JustificationMode.Left);
lastPage.appendNoteLine(ruleLine);
}
lastPage.appendNoteLine(l);
}
}
}
commitPage();
}
private int removeEmptyPages(final boolean all) {
int count = 0;
final ListIterator<FB2Page> i = pages.listIterator(pages.size());
if (all) {
while (i.hasPrevious()) {
final FB2Page p = i.previous();
if (p.isEmpty()) {
i.remove();
count++;
}
}
} else {
while (i.hasPrevious()) {
final FB2Page p = i.previous();
if (p.isEmpty()) {
i.remove();
count++;
} else {
break;
}
}
}
return count;
}
private void parseWithVTDEx(final String fileName) {
final StandardHandler h = new StandardHandler(content);
final List<Closeable> resources = new ArrayList<Closeable>();
final long t1 = System.currentTimeMillis();
try {
final AtomicLong size = new AtomicLong();
final InputStream inStream = getInputStream(fileName, size, resources);
if (inStream != null) {
final TextProvider text = loadContent(inStream, size, resources);
final long t2 = System.currentTimeMillis();
System.out.println("VTDEx load: " + (t2 - t1) + " ms");
final VTDGenEx gen = new VTDGenEx();
gen.setDoc(text.chars, 0, text.size);
gen.parse(false);
final long t3 = System.currentTimeMillis();
System.out.println("VTDEx parse: " + (t3 - t2) + " ms");
final VTDExParser p = new VTDExParser();
p.parse(gen, FB2TagFactory.instance, h);
final long t4 = System.currentTimeMillis();
System.out.println("VTDEx scan: " + (t4 - t3) + " ms");
}
} catch (final Exception e) {
throw new RuntimeException("FB2 document can not be opened: " + e.getMessage(), e);
} finally {
for (final Closeable r : resources) {
try {
if (r != null) {
r.close();
}
} catch (final IOException e) {
}
}
resources.clear();
}
}
private void parseWithDuckbill(final String fileName) {
final StandardHandler h = new StandardHandler(content);
final List<Closeable> resources = new ArrayList<Closeable>();
final long t1 = System.currentTimeMillis();
try {
final AtomicLong size = new AtomicLong();
final InputStream inStream = getInputStream(fileName, size, resources);
if (inStream != null) {
final TextProvider text = loadContent(inStream, size, resources);
final long t2 = System.currentTimeMillis();
System.out.println("DUCK load: " + (t2 - t1) + " ms");
final DuckbillParser p = new DuckbillParser();
p.parse(text, FB2TagFactory.instance, h);
final long t4 = System.currentTimeMillis();
System.out.println("DUCK parse: " + (t4 - t2) + " ms");
}
} catch (final Exception e) {
throw new RuntimeException("FB2 document can not be opened: " + e.getMessage(), e);
} finally {
for (final Closeable r : resources) {
try {
if (r != null) {
r.close();
}
} catch (final IOException e) {
}
}
resources.clear();
}
}
private TextProvider loadContent(final InputStream inStream, final AtomicLong size, final List<Closeable> resources)
throws IOException, UnsupportedEncodingException {
final String encoding = getEncoding(inStream);
final Reader isr = new InputStreamReader(inStream, encoding);
resources.add(isr);
final char[] chars = new char[(int) size.get()];
int offset = 0;
for (int len = chars.length; offset < len;) {
final int n = isr.read(chars, offset, len);
if (n == -1) {
break;
}
offset += n;
len -= n;
}
size.set(offset);
return new TextProvider(chars, offset);
}
private String getEncoding(final InputStream inStream) throws IOException {
String encoding = "utf-8";
final char[] buffer = new char[256];
boolean found = false;
int len = 0;
while (len < 256) {
final int val = inStream.read();
if (len == 0 && (val == 0xEF || val == 0xBB || val == 0xBF)) {
continue;
}
buffer[len++] = (char) val;
if (val == '>') {
found = true;
break;
}
}
if (found) {
final String xmlheader = new String(buffer, 0, len).trim();
if (xmlheader.startsWith("<?xml") && xmlheader.endsWith("?>")) {
final int index = xmlheader.indexOf("encoding");
if (index > 0) {
final int startIndex = xmlheader.indexOf('"', index);
if (startIndex > 0) {
final int endIndex = xmlheader.indexOf('"', startIndex + 1);
if (endIndex > 0) {
encoding = xmlheader.substring(startIndex + 1, endIndex);
System.out.println("XML encoding:" + encoding);
}
}
}
} else {
throw new RuntimeException("FB2 document can not be opened: " + "Invalid header");
}
} else {
throw new RuntimeException("FB2 document can not be opened: " + "Header not found");
}
return encoding;
}
private InputStream getInputStream(final String fileName, final AtomicLong size, final List<Closeable> resources)
throws IOException, FileNotFoundException {
InputStream inStream = null;
if (fileName.endsWith("zip")) {
final ZipArchive zipArchive = new ZipArchive(new File(fileName));
final Enumeration<ZipArchiveEntry> entries = zipArchive.entries();
while (entries.hasMoreElements()) {
final ZipArchiveEntry entry = entries.nextElement();
if (!entry.isDirectory() && entry.getName().endsWith("fb2")) {
size.set(entry.getSize());
inStream = entry.open();
resources.add(inStream);
break;
}
}
resources.add(zipArchive);
} else {
final File f = new File(fileName);
size.set(f.length());
inStream = new FileInputStream(f);
resources.add(inStream);
}
return inStream;
}
@Override
public List<OutlineLink> getOutline() {
return outline;
}
@Override
public CodecPage getPage(final int pageNuber) {
if (0 <= pageNuber && pageNuber < pages.size()) {
return pages.get(pageNuber);
} else {
return null;
}
}
@Override
public int getPageCount() {
return pages.size();
}
@Override
public CodecPageInfo getUnifiedPageInfo() {
return FB2Page.CPI;
}
@Override
public CodecPageInfo getPageInfo(final int pageNuber) {
return FB2Page.CPI;
}
@Override
protected void freeDocument() {
content.recycle();
for (final FB2Page p : pages) {
p.finalRecycle();
}
pages.clear();
}
void commitPage() {
getLastPage().commit(content);
}
@Override
public Bitmap getEmbeddedThumbnail() {
return content.getCoverImage();
}
public void addTitle(final MarkupTitle title) {
outline.add(new OutlineLink(title.title, "#" + pages.size(), title.level));
}
@Override
public List<? extends RectF> searchText(final int pageNuber, final String pattern) {
if (LengthUtils.isEmpty(pattern)) {
return null;
}
final FB2Page page = (FB2Page) getPage(pageNuber);
return (page == null) ? null : page.searchText(pattern);
}
public FB2Page getLastPage() {
if (pages.size() == 0) {
pages.add(new FB2Page());
}
FB2Page fb2Page = pages.get(pages.size() - 1);
if (fb2Page.committed) {
fb2Page = new FB2Page();
pages.add(fb2Page);
}
return fb2Page;
}
}