mirror of
https://github.com/geometer/FBReaderJ.git
synced 2025-10-06 03:50:19 +02:00
566 lines
17 KiB
Java
566 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2007-2010 Geometer Plus <contact@geometerplus.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
|
|
* 02110-1301, USA.
|
|
*/
|
|
|
|
package org.geometerplus.fbreader.formats.plucker;
|
|
|
|
import java.io.IOException;
|
|
import java.util.*;
|
|
import java.util.zip.*;
|
|
|
|
import org.geometerplus.zlibrary.core.constants.MimeTypes;
|
|
import org.geometerplus.zlibrary.core.filesystem.ZLFile;
|
|
import org.geometerplus.zlibrary.core.util.ZLInputStreamWithOffset;
|
|
import org.geometerplus.zlibrary.core.image.*;
|
|
import org.geometerplus.zlibrary.text.model.*;
|
|
|
|
import org.geometerplus.fbreader.bookmodel.*;
|
|
import org.geometerplus.fbreader.formats.pdb.*;
|
|
|
|
public class PluckerBookReader extends BookReader {
|
|
private final ZLFile myFile;
|
|
private final int myFileSize;
|
|
private ZLInputStreamWithOffset myStream;
|
|
private int myFont;
|
|
private char[] myCharBuffer;
|
|
private String myConvertedTextBuffer;
|
|
private boolean myParagraphStarted = false;
|
|
private boolean myBufferIsEmpty;
|
|
//private ZLTextForcedControlEntry myForcedEntry;
|
|
private final ArrayList/*<std::pair<FBTextKind,bool> >*/ myDelayedControls = new ArrayList();
|
|
private final ArrayList/*<std::string> */myDelayedHyperlinks = new ArrayList();
|
|
private short myCompressionVersion;
|
|
private char myBytesToSkip;
|
|
|
|
private final ArrayList/*<std::pair<int, int> >*/ myReferencedParagraphs = new ArrayList();
|
|
private final HashMap/*<int, std::vector<int> >*/ myParagraphMap = new HashMap(); /*<int, vector<pair<int, int>>>*/
|
|
private ArrayList/*<Integer, Integer>*/ myParagraphVector = new ArrayList();
|
|
private boolean myParagraphStored;
|
|
|
|
public PluckerBookReader(ZLFile file, BookModel model, String encoding){
|
|
super(model);
|
|
//myConverter = new EncodedTextReader(encoding).getConverter();
|
|
myFile = file;
|
|
myFileSize = (int)file.size();
|
|
//System.out.println(filePath + " " + encoding);
|
|
myFont = FontType.FT_REGULAR;
|
|
myCharBuffer = new char[65535];
|
|
//myForcedEntry = null;
|
|
}
|
|
|
|
public boolean readDocument() {
|
|
try {
|
|
myStream = new ZLInputStreamWithOffset(myFile.getInputStream());
|
|
|
|
PdbHeader header = new PdbHeader(myStream);
|
|
|
|
setMainTextModel();
|
|
myFont = FontType.FT_REGULAR;
|
|
|
|
for (int index = 0; index < header.Offsets.length; ++index) {
|
|
int currentOffset = myStream.offset();
|
|
int pit = header.Offsets[index];
|
|
if (currentOffset > pit) {
|
|
break;
|
|
}
|
|
//myStream.seek(pit - currentOffset, false);
|
|
myStream.skip(pit - currentOffset);
|
|
|
|
if (myStream.offset() != pit) {
|
|
break;
|
|
}
|
|
int recordSize = ((index != header.Offsets.length - 1) ? header.Offsets[index + 1] : myFileSize) - pit;
|
|
readRecord(recordSize);
|
|
}
|
|
myStream.close();
|
|
} catch (IOException e) {
|
|
return false;
|
|
}
|
|
|
|
for (Iterator it = myReferencedParagraphs.iterator(); it.hasNext();) {
|
|
Pair pair = (Pair)it.next();
|
|
int first = (Integer)pair.myFirst;
|
|
int second = (Integer)pair.mySecond;
|
|
ArrayList/*<Integer>*/ list = (ArrayList)myParagraphMap.get(first);
|
|
if (list != null) {
|
|
for(int k = second; k < list.size(); ++k) {
|
|
if (((Integer) ((Pair)list.get(k)).myFirst) != -1) {
|
|
//addHyperlinkLabel(fromNumber(first) + '#' + fromNumber(second), (Integer)list.get(k));
|
|
final Pair p = (Pair)list.get(k);
|
|
//addHyperlinkLabel(fromNumber(first) + '#' + fromNumber(second), (Integer) p.mySecond, (Integer) p.myFirst);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
myReferencedParagraphs.clear();
|
|
myParagraphMap.clear();
|
|
return true;
|
|
}
|
|
|
|
private class FontType {
|
|
public static final int FT_REGULAR = 0;
|
|
public static final int FT_H1 = 1;
|
|
public static final int FT_H2 = 2;
|
|
public static final int FT_H3 = 3;
|
|
public static final int FT_H4 = 4;
|
|
public static final int FT_H5 = 5;
|
|
public static final int FT_H6 = 6;
|
|
public static final int FT_BOLD = 7;
|
|
public static final int FT_TT = 8;
|
|
public static final int FT_SMALL = 9;
|
|
public static final int FT_SUB = 10;
|
|
public static final int FT_SUP = 11;
|
|
};
|
|
|
|
private void readRecord(int recordSize) throws IOException {
|
|
int uid = PdbUtil.readShort(myStream);
|
|
if (uid == 1) {
|
|
myCompressionVersion = (short) PdbUtil.readShort(myStream );
|
|
} else {
|
|
int paragraphs = PdbUtil.readShort(myStream);
|
|
|
|
int size = PdbUtil.readShort(myStream);
|
|
//TODO ??????
|
|
int type = myStream.read();
|
|
|
|
int flags = myStream.read();
|
|
|
|
switch (type) {
|
|
case 0: // text (TODO: found sample file and test this code)
|
|
case 1: // compressed text
|
|
{
|
|
ArrayList/*<Integer>*/ pars = new ArrayList();
|
|
for (int i = 0; i < paragraphs; ++i) {
|
|
int pSize = PdbUtil.readShort(myStream);
|
|
pars.add(pSize);
|
|
myStream.skip(2);
|
|
}
|
|
|
|
boolean doProcess = false;
|
|
if (type == 0) {//?
|
|
byte[] buf = new byte[size];
|
|
doProcess = myStream.read(buf, 0, (int)size) == size;
|
|
if (doProcess) {
|
|
// TODO: use encoding!!!!
|
|
// TODO: don't create any new objects!!!!
|
|
myCharBuffer = new String(buf).toCharArray();
|
|
}
|
|
} else if (myCompressionVersion == 1) {
|
|
byte[] buf = new byte[size];
|
|
doProcess =
|
|
DocDecompressor.decompress(myStream, buf, recordSize - 8 - 4 * paragraphs) == size;
|
|
if (doProcess) {
|
|
myCharBuffer = new String(buf).toCharArray();
|
|
}
|
|
} else if (myCompressionVersion == 2) {
|
|
byte input [] = new byte[(int) (recordSize - 10 - 4 * paragraphs)];
|
|
final int inputSize = myStream.read(input);
|
|
Inflater decompressor = new Inflater();
|
|
decompressor.setInput(input, 0, inputSize);
|
|
byte output [] = new byte[size];
|
|
try {
|
|
doProcess = decompressor.inflate(output) == size;
|
|
decompressor.end();
|
|
myCharBuffer = new String(output, 0, size).toCharArray();
|
|
} catch (DataFormatException e) {
|
|
// TODO Auto-generated catch block
|
|
//e.printStackTrace();
|
|
//System.out.println(e.getMessage());
|
|
}
|
|
//doProcess =
|
|
//ZLZDecompressor(recordSize - 10 - 4 * paragraphs).
|
|
//decompress(myStream, myCharBuffer, size) == size;
|
|
}
|
|
if (doProcess) {
|
|
addHyperlinkLabel(fromNumber(uid));
|
|
myParagraphMap.put(uid, new ArrayList());
|
|
myParagraphVector = (ArrayList)myParagraphMap.get(uid);
|
|
processTextRecord(size, pars);
|
|
if ((flags & 0x1) == 0) {
|
|
// insertEndOfTextParagraph();
|
|
//setNewTextModel();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case 2: // image
|
|
case 3: // compressed image
|
|
{
|
|
ZLImage image = null;
|
|
if (type == 2) {
|
|
//System.out.println("non-compressed image");
|
|
image = new PluckerFileImage(MimeTypes.MIME_IMAGE_PALM, myFile, myStream.offset(), recordSize - 8);
|
|
} else if (myCompressionVersion == 1) {
|
|
//System.out.println("DocCompressedImage");
|
|
image = new DocCompressedFileImage(MimeTypes.MIME_IMAGE_PALM, myFile, myStream.offset(), recordSize - 8);
|
|
} else if (myCompressionVersion == 2) {
|
|
//System.out.println("ZCompressedImage");
|
|
image = new ZCompressedFileImage(MimeTypes.MIME_IMAGE_PALM, myFile, myStream.offset() + 2, recordSize - 10);
|
|
}
|
|
if (image != null) {
|
|
addImage(fromNumber(uid), image);
|
|
}
|
|
break;
|
|
}
|
|
case 9: // category record is ignored
|
|
break;
|
|
case 10:
|
|
short typeCode = (short) PdbUtil.readShort(myStream);
|
|
break;
|
|
case 11: // style sheet record is ignored
|
|
break;
|
|
case 12: // font page record is ignored
|
|
break;
|
|
case 13: // TODO: process tables
|
|
case 14: // TODO: process tables
|
|
break;
|
|
case 15: // multiimage
|
|
{
|
|
short columns = (short) PdbUtil.readShort(myStream);
|
|
short rows = (short) PdbUtil.readShort(myStream);
|
|
//System.out.println("multiimage");
|
|
/*PluckerMultiImage image = new PluckerMultiImage(rows, columns, Model.getImageMap());
|
|
for (int i = 0; i < size / 2 - 2; ++i) {
|
|
short us = (short)myStream.read();
|
|
PdbUtil.readShort(myStream, us);
|
|
image.addId(fromNumber(us));
|
|
}
|
|
addImage(fromNumber(uid), image);
|
|
*/break;
|
|
}
|
|
default:
|
|
//std::cerr << "type = " << (int)type << "\n";
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void processTextRecord(int size, ArrayList/*<Integer>*/ pars) {
|
|
int start = 0;
|
|
int end = 0;
|
|
|
|
for (Iterator it = pars.iterator(); it.hasNext();) {
|
|
start = end;
|
|
end = start + (Integer)it.next();
|
|
if (end > size) {
|
|
return;
|
|
}
|
|
myParagraphStored = false;
|
|
processTextParagraph(myCharBuffer, start, end);
|
|
if (!myParagraphStored) {
|
|
myParagraphVector.add(new Pair(-1, -1));
|
|
}
|
|
}
|
|
}
|
|
|
|
private void processTextParagraph(char[] data, int start, int end) {
|
|
changeFont(FontType.FT_REGULAR);
|
|
while (popKind()) {}
|
|
|
|
myParagraphStarted = false;
|
|
myBytesToSkip = 0;
|
|
|
|
int textStart = start;
|
|
boolean functionFlag = false;
|
|
for (int ptr = start; ptr < end; ++ptr) {
|
|
if (data[ptr] == 0) {
|
|
functionFlag = true;
|
|
if (ptr > textStart) {
|
|
safeBeginParagraph();
|
|
// myConvertedTextBuffer = "";//.erase();
|
|
myConvertedTextBuffer = "";//myConverter.convert(data, textStart, ptr);
|
|
addData(myConvertedTextBuffer.toCharArray());
|
|
myBufferIsEmpty = false;
|
|
}
|
|
} else if (functionFlag) {
|
|
int paramCounter = (data[ptr]) % 8;
|
|
if (end - ptr > paramCounter) {
|
|
processTextFunction(data, ptr);
|
|
ptr += paramCounter;
|
|
} else {
|
|
ptr = end - 1;
|
|
}
|
|
functionFlag = false;
|
|
if (myBytesToSkip > 0) {
|
|
ptr += myBytesToSkip;
|
|
myBytesToSkip = 0;
|
|
}
|
|
textStart = ptr + 1;
|
|
} else {
|
|
if (data[ptr] == 0xA0) {
|
|
data[ptr] = 0x20;
|
|
}
|
|
if (!myParagraphStarted && (textStart == ptr) && (data[ptr] == ' ')) {
|
|
++textStart;
|
|
}
|
|
}
|
|
}
|
|
if (end > textStart) {
|
|
safeBeginParagraph();
|
|
// myConvertedTextBuffer = "";//erase();
|
|
myConvertedTextBuffer = "";//myConverter.convert(data, textStart, end);
|
|
addData(myConvertedTextBuffer.toCharArray());
|
|
myBufferIsEmpty = false;
|
|
}
|
|
safeEndParagraph();
|
|
//if (myForcedEntry != null) {
|
|
// myForcedEntry = null;
|
|
//}
|
|
myDelayedControls.clear();
|
|
}
|
|
|
|
private void processTextFunction(char[] ptr, int cur) {
|
|
switch (ptr[cur]) {
|
|
case 0x08:
|
|
safeAddControl(FBTextKind.INTERNAL_HYPERLINK, false);
|
|
break;
|
|
case 0x0A:
|
|
safeAddHyperlinkControl(fromNumber(twoBytes(ptr, cur+ 1)));
|
|
break;
|
|
case 0x0C:
|
|
{
|
|
int sectionNum = twoBytes(ptr, cur + 1);
|
|
int paragraphNum = twoBytes(ptr, cur + 3);
|
|
safeAddHyperlinkControl(fromNumber(sectionNum) + '#' + fromNumber(paragraphNum));
|
|
myReferencedParagraphs.add(new Pair(sectionNum, paragraphNum));
|
|
break;
|
|
}
|
|
case 0x11:
|
|
changeFont((ptr[cur + 1]));
|
|
break;
|
|
case 0x1A:
|
|
safeBeginParagraph();
|
|
// System.out.println("image ref");
|
|
addImageReference(fromNumber(twoBytes(ptr, cur + 1)), (short) 0);
|
|
break;
|
|
case 0x22:
|
|
if (!myParagraphStarted) {
|
|
//if (myForcedEntry == null) {
|
|
// myForcedEntry = new ZLTextForcedControlEntry();
|
|
//}
|
|
//myForcedEntry.setLeftIndent((short)ptr[cur + 1]);
|
|
//myForcedEntry.setRightIndent((short)ptr[cur + 2]);
|
|
}
|
|
break;
|
|
case 0x29:
|
|
if (!myParagraphStarted) {
|
|
//if (myForcedEntry == null) {
|
|
// myForcedEntry = new ZLTextForcedControlEntry();
|
|
//}
|
|
//switch (ptr[cur + 1]) {
|
|
// case 0: myForcedEntry.setAlignmentType(ZLTextAlignmentType.ALIGN_LEFT); break;
|
|
// case 1: myForcedEntry.setAlignmentType(ZLTextAlignmentType.ALIGN_RIGHT); break;
|
|
// case 2: myForcedEntry.setAlignmentType(ZLTextAlignmentType.ALIGN_CENTER); break;
|
|
// case 3: myForcedEntry.setAlignmentType(ZLTextAlignmentType.ALIGN_JUSTIFY); break;
|
|
//}
|
|
}
|
|
break;
|
|
case 0x33: // just break line instead of horizontal rule (TODO: draw horizontal rule?)
|
|
safeEndParagraph();
|
|
break;
|
|
case 0x38:
|
|
safeEndParagraph();
|
|
break;
|
|
case 0x40:
|
|
safeAddControl(FBTextKind.EMPHASIS, true);
|
|
break;
|
|
case 0x48:
|
|
safeAddControl(FBTextKind.EMPHASIS, false);
|
|
break;
|
|
case 0x53: // color setting is ignored
|
|
break;
|
|
case 0x5C:
|
|
// System.out.println("image ref");
|
|
addImageReference(fromNumber(twoBytes(ptr, cur + 3)), (short) 0);
|
|
break;
|
|
case 0x60: // underlined text is ignored
|
|
break;
|
|
case 0x68: // underlined text is ignored
|
|
break;
|
|
case 0x70: // strike-through text is ignored
|
|
break;
|
|
case 0x78: // strike-through text is ignored
|
|
break;
|
|
case 0x83:
|
|
{
|
|
safeBeginParagraph();
|
|
addData(new char[] { (char)twoBytes(ptr, cur + 2) });
|
|
myBufferIsEmpty = false;
|
|
myBytesToSkip = ptr[cur+1];
|
|
break;
|
|
}
|
|
case 0x85: // TODO: process 4-byte unicode character
|
|
break;
|
|
case 0x8E: // custom font operations are ignored
|
|
case 0x8C:
|
|
case 0x8A:
|
|
case 0x88:
|
|
break;
|
|
case 0x90: // TODO: add table processing
|
|
case 0x92: // TODO: process table
|
|
case 0x97: // TODO: process table
|
|
break;
|
|
default: // this should be impossible
|
|
//std::cerr << "Oops... function #" << (int)(unsigned char)*ptr << "\n";
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void setFont(int font, boolean start) {
|
|
switch (font) {
|
|
case FontType.FT_REGULAR:
|
|
break;
|
|
case FontType.FT_H1:
|
|
case FontType.FT_H2:
|
|
case FontType.FT_H3:
|
|
case FontType.FT_H4:
|
|
case FontType.FT_H5:
|
|
case FontType.FT_H6:
|
|
processHeader(font, start);
|
|
break;
|
|
case FontType.FT_BOLD:
|
|
safeAddControl(FBTextKind.BOLD, start);
|
|
break;
|
|
case FontType.FT_TT:
|
|
safeAddControl(FBTextKind.CODE, start);
|
|
break;
|
|
case FontType.FT_SMALL:
|
|
break;
|
|
case FontType.FT_SUB:
|
|
safeAddControl(FBTextKind.SUB, start);
|
|
break;
|
|
case FontType.FT_SUP:
|
|
safeAddControl(FBTextKind.SUP, start);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void changeFont(int font) {
|
|
if (myFont == font) {
|
|
return;
|
|
}
|
|
setFont(myFont, false);
|
|
myFont = font;
|
|
setFont(myFont, true);
|
|
}
|
|
|
|
private void safeAddControl(byte kind, boolean start) {
|
|
if (myParagraphStarted) {
|
|
addControl((Byte)kind, (Boolean)start);
|
|
} else {
|
|
myDelayedControls.add(new Pair(kind, start));
|
|
}
|
|
}
|
|
private void safeAddHyperlinkControl(String id) {
|
|
if (myParagraphStarted) {
|
|
addHyperlinkControl(FBTextKind.INTERNAL_HYPERLINK, id);
|
|
} else {
|
|
myDelayedHyperlinks.add(id);
|
|
}
|
|
}
|
|
|
|
private void safeBeginParagraph() {
|
|
if (!myParagraphStarted) {
|
|
myParagraphStarted = true;
|
|
myBufferIsEmpty = true;
|
|
beginParagraph(ZLTextParagraph.Kind.TEXT_PARAGRAPH);
|
|
if (!myParagraphStored) {
|
|
//final ArrayList models = Model.getBookTextModels();
|
|
//myParagraphVector.add(new Pair(((ZLTextPlainModel) models.get(models.size()-1)/*BookTextModel*/).getParagraphsNumber() - 1, models.size() - 1));
|
|
myParagraphStored = true;
|
|
}
|
|
for (Iterator it = myDelayedControls.iterator(); it.hasNext(); ) {
|
|
Pair pit = (Pair)it.next();
|
|
addControl((Byte)pit.myFirst, (Boolean)pit.mySecond);
|
|
}
|
|
//if (myForcedEntry != null) {
|
|
// addControl(myForcedEntry);
|
|
//} else {
|
|
addControl(FBTextKind.REGULAR, true);
|
|
//}
|
|
for (Iterator it = myDelayedHyperlinks.iterator(); it.hasNext(); ) {
|
|
addHyperlinkControl(FBTextKind.INTERNAL_HYPERLINK, (String)it.next());
|
|
}
|
|
myDelayedHyperlinks.clear();
|
|
}
|
|
}
|
|
|
|
private void safeEndParagraph() {
|
|
if (myParagraphStarted) {
|
|
if (myBufferIsEmpty) {
|
|
final String SPACE = " ";
|
|
addData(SPACE.toCharArray());
|
|
}
|
|
endParagraph();
|
|
myParagraphStarted = false;
|
|
}
|
|
}
|
|
|
|
private void processHeader(int font, boolean start) {
|
|
if (start) {
|
|
enterTitle();
|
|
int kind;
|
|
switch (font) {
|
|
case FontType.FT_H1:
|
|
kind = FBTextKind.H1;
|
|
break;
|
|
case FontType.FT_H2:
|
|
kind = FBTextKind.H2;
|
|
break;
|
|
case FontType.FT_H3:
|
|
kind = FBTextKind.H3;
|
|
break;
|
|
case FontType.FT_H4:
|
|
kind = FBTextKind.H4;
|
|
break;
|
|
case FontType.FT_H5:
|
|
kind = FBTextKind.H5;
|
|
break;
|
|
case FontType.FT_H6:
|
|
default:
|
|
kind = FBTextKind.H6;
|
|
break;
|
|
}
|
|
pushKind((byte)kind);
|
|
} else {
|
|
popKind();
|
|
exitTitle();
|
|
}
|
|
}
|
|
|
|
static private class Pair {
|
|
public Object myFirst;
|
|
public Object mySecond;
|
|
Pair(Object first, Object second) {
|
|
this.myFirst = first;
|
|
this.mySecond = second;
|
|
}
|
|
}
|
|
|
|
static private int twoBytes(char[] ptr, int offset) {
|
|
return 256 * ptr[offset] + ptr[offset+1];
|
|
}
|
|
|
|
static String fromNumber(int num) {
|
|
String str = "";
|
|
str += num;
|
|
//ZLStringUtil.appendNumber(str, num);
|
|
return str;
|
|
}
|
|
}
|