package org.albite.book.view; import org.albite.book.model.parser.TextParser; import java.util.Vector; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Image; import org.albite.albite.AlbiteMIDlet; import org.albite.albite.ColorScheme; import org.albite.font.AlbiteFont; import org.albite.io.RandomReadingFile; import org.albite.util.archive.Archive; //#if !(TinyMode || TinyModeExport || LightMode || LightModeExport) import org.geometerplus.zlibrary.text.hyphenation.ZLTextHyphenationInfo; import org.geometerplus.zlibrary.text.hyphenation.ZLTextTeXHyphenator; //#endif ///#define DEBUG_PARSER public class TextPage extends Page implements StylingConstants { private int start; /* * start+length, i.e. character is in page if start <= char_pos < end */ private int end; protected Region[] regions; private ImageRegion imageRegion = null; public TextPage(final Booklet booklet, final PageState ip) { this.booklet = booklet; final int width = booklet.width; final int height = booklet.height; // App Settings final AlbiteFont fontPlain = booklet.fontPlain; final AlbiteFont fontItalic = booklet.fontItalic; final int spaceWidth = fontPlain.charWidth(' '); //#debug AlbiteMIDlet.LOGGER.log("Spacewidth: " + spaceWidth); int dashWidth = 0; final int fontHeight = booklet.fontHeight; final int fontHeightX2 = 2 * fontHeight; final int fontIndent = booklet.fontIndent; //#if !(TinyMode || TinyModeExport || LightMode || LightModeExport) final ZLTextTeXHyphenator hyphenator = booklet.hyphenator; //#endif // Chapter settings final String chapterPath = booklet.getChapter().getPath(); final char[] buffer = booklet.getTextBuffer(); final int bufferSize; final Archive bookFile = booklet.bookArchive; final Vector images = ip.images; byte style; boolean center; byte color; AlbiteFont font; HyphenatedTextRegion lastHyphenatedWord; boolean startsNewParagraph; TextParser parser = ip.parser; int wordPixelWidth; //word width in pixels Vector wordsOnThisLine = new Vector(20); //RegionTexts boolean firstWord; int posX = 0; int posY = 0; Vector regionsTemp; if (images.isEmpty()) { //text mode regionsTemp = new Vector(300); parser.position = end = start = ip.position; parser.length = ip.length; bufferSize = buffer.length; style = ip.style; center = ip.center; lastHyphenatedWord = ip.lastHyphenatedWord; startsNewParagraph = ip.startsNewParagraph; } else { //image mode ImageRegion ri = (ImageRegion) images.firstElement(); images.removeElementAt(0); imageRegion = ri; regionsTemp = new Vector(40); posY = 0; bufferSize = ri.altTextBufferPosition + ri.altTextBufferLength; parser.position = end = start = ri.altTextBufferPosition; parser.length = 0; style = ITALIC; center = true; lastHyphenatedWord = null; startsNewParagraph = true; } /* * Setup font & color, based on style value from previous page. */ font = chooseFont(fontPlain, fontItalic, style); color = chooseTextColor(style); boolean lastLine = false; boolean firstLine = true; boolean lineBreak = false; page: while (true) { /* * There is no more space for new lines, * so the page is done. */ if (posY >= height - fontHeight) { break; } /* * Check if it is the last line of the page */ if (posY >= height - (fontHeightX2)) { lastLine = true; } /* * NB: posX & posY are in pixels, pos is in chars. */ posX = 0; firstWord = true; /* * Clear the cache that will hold all the elements on the line */ wordsOnThisLine.removeAllElements(); /* * Indent the line, if it starts a new paragraph. */ if (startsNewParagraph) { posX = fontIndent; } line: while (true) { /* * Parse on */ if (!parser.parseNext(buffer, bufferSize)) { //#ifdef DEBUG_PARSER //# AlbiteMIDlet.LOGGER.log("parser done"); //#endif /* No more chars to read */ if (imageRegion == null) { ip.bufferRead = true; } lineBreak = true; if (wordsOnThisLine.size() > 0) { positionWordsOnLine( wordsOnThisLine, regionsTemp, width, posY, spaceWidth, fontIndent, lineBreak, startsNewParagraph, center); } break page; } //#ifdef DEBUG_PARSER //# AlbiteMIDlet.LOGGER.log( //# "parser: _" //# + new String( //# buffer, parser.position, parser.length) //# + "_, " //# + parser.position + " / " //# + parser.length //# + " state: " + parser.state + "\n"); //#endif /* * Logic for possible parsing states. */ final int state = parser.state; switch (state) { case TextParser.STATE_PASS: continue line; case TextParser.STATE_NEW_SOFT_LINE: if (posX == 0) { /* * Only if it's on the next line */ startsNewParagraph = true; } if (!(posX > (startsNewParagraph ? fontIndent : 0))) { continue line; } case TextParser.STATE_NEW_LINE: //linebreak if (!firstLine || (posX > (startsNewParagraph ? fontIndent : 0) )) { lineBreak = true; break line; } else { /* don't start a page with blank lines */ continue line; } case TextParser.STATE_STYLING: /* enable styling */ if (parser.enableBold) { style |= BOLD; } if (parser.enableItalic) { style |= ITALIC; } if (parser.enableHeading) { style |= HEADING; } if (parser.enableCenterAlign) { center = true; } if (parser.disableCenterAlign) { center = false; } /* disable styling */ if (parser.disableBold) { style &= ~BOLD; } if (parser.disableItalic) { style &= ~ITALIC; } if (parser.disableHeading) { style &= ~HEADING; } /* setup font & color */ font = chooseFont(fontPlain, fontItalic, style); color = chooseTextColor(style); continue line; case TextParser.STATE_IMAGE: if (booklet.renderImages) { ImageRegion ri = new ImageRegion( (bookFile == null ? null : bookFile.getEntry( RandomReadingFile .relativeToAbsoluteURL( chapterPath + new String(buffer, parser.imageURLPosition, parser.imageURLLength)) )), parser.imageTextPosition, parser.imageTextLength); images.addElement(ri); } continue line; case TextParser.STATE_RULER: regionsTemp.addElement( new RulerRegion( (short) 0, (short) posY, (short) width, (short) font.getLineHeight(), parser.position, ColorScheme.COLOR_TEXT)); break line; default: /* * There is nothing to do. It must be * STATE_NORMAL */ } if (parser.length == 0) { continue line; } wordPixelWidth = font.charsWidth(buffer, parser.position, parser.length); if (!firstWord) { /* * If it is not the first word, it will need the * space(s) before it */ posX += font.charWidth(' '); } /* * word FITS on the line without need to split it */ if (wordPixelWidth + posX <= width) { /* * if a hyphenated word chain was being build, * this is the <i>last</i> chunk of it */ if (lastHyphenatedWord != null) { if (parser.length > 0) { HyphenatedTextRegion rt = new HyphenatedTextRegion( (short) 0, (short) 0, (short) wordPixelWidth, (short) fontHeight, lastHyphenatedWord, parser.position, parser.length); lastHyphenatedWord = null; wordsOnThisLine.addElement(rt); } } else { /* * Just add a whole word to the line */ if (parser.length > 0) { wordsOnThisLine.addElement( new TextRegion((short) 0, (short) 0, (short) wordPixelWidth, (short) fontHeight, parser.position, parser.length, style, color)); } } posX += wordPixelWidth; firstWord = false; } else { /* * try to hyphenate word */ dashWidth = font.charWidth('-'); //#if !(TinyMode || TinyModeExport || LightMode || LightModeExport) if (hyphenator != null) { ZLTextHyphenationInfo info = hyphenator.getInfo( buffer, parser.position, parser.length); /* * try to hyphenate word, so that the largest * possible chunk is on this line */ /* * wordInfo.length - 2: starts from one before * the last */ for (int i = parser.length - 2; i > 0; i--) { if (info.isHyphenationPossible(i)) { wordPixelWidth = font.charsWidth(buffer, parser.position, i) + dashWidth; /* * This part of the word fits on the line */ if (wordPixelWidth < width - posX) { /* * If the word chunk already ends with a * dash, include it. */ if (buffer[parser.position + i] == '-') { i++; } if (i > 0) { HyphenatedTextRegion rt; if (lastHyphenatedWord != null){ rt = new HyphenatedTextRegion( (short) 0, (short) 0, (short) wordPixelWidth, (short) fontHeight, lastHyphenatedWord, parser.position, i); } else { rt = new HyphenatedTextRegion( (short) 0, (short) 0, (short) wordPixelWidth, (short) fontHeight, parser.position, parser.length, style, color, parser.position, i); } wordsOnThisLine.addElement(rt); lastHyphenatedWord = rt; } parser.position += i; parser.length = 0; posX += wordPixelWidth; firstWord = false; /* the word was hyphented */ break line; } } } } //#endif /* * The word could not be hyphenated. Could it fit * into a single line at all? */ if (font.charsWidth(buffer, parser.position, parser.length) > width) { /* This word neither hyphenates, nor does it * fit at all on a single line, so one should * force hyphanation on it! */ for (int i = parser.length - 2; i > 0; i--) { wordPixelWidth = font.charsWidth(buffer, parser.position, i) + dashWidth; if (wordPixelWidth < width - posX) { /* * If the word chunk already ends with a * dash, include it. */ if (buffer[parser.position + i] == '-') { i++; } if (i > 0) { HyphenatedTextRegion rt; if (lastHyphenatedWord != null){ rt = new HyphenatedTextRegion( (short) 0, (short) 0, (short) wordPixelWidth, (short) fontHeight, lastHyphenatedWord, parser.position, parser.length); } else { rt = new HyphenatedTextRegion( (short) 0, (short) 0, (short) wordPixelWidth, (short) fontHeight, parser.position, parser.length, style, color, parser.position, parser.length); } wordsOnThisLine.addElement(rt); lastHyphenatedWord = rt; } parser.position += i; parser.length = 0; posX += wordPixelWidth; firstWord = false; break line; } } } /* * The word could fit on a line, so will leave it * for the next line, and won't add anything here. */ parser.length = 0; break; } /* * All the text could fit on one line. This is usually * the case for alt text for images. */ } positionWordsOnLine(wordsOnThisLine, regionsTemp, width, posY, spaceWidth, fontIndent, lineBreak, startsNewParagraph, center); startsNewParagraph = false; if (lineBreak) { startsNewParagraph = true; } lineBreak = false; if (lastLine) { lastLine = false; break; } posY += fontHeight; firstLine = false; } if (imageRegion == null) { /* * save the params for the next page */ ip.position = this.end = parser.position; ip.length = parser.length; ip.style = style; ip.center = center; ip.lastHyphenatedWord = lastHyphenatedWord; ip.startsNewParagraph = startsNewParagraph; } regions = new Region[regionsTemp.size()]; regionsTemp.copyInto(regions); } private void positionWordsOnLine( final Vector words, final Vector regionsTemp, int lineWidth, final int lineY, final int spaceWidth, final int fontIndent, final boolean endsParagraph, final boolean startsNewParagraph, final boolean center) { final int wordsSize = words.size(); final int wordsSize1 = wordsSize - 1; final int wordSpacing = spaceWidth; final byte align = (center ? CENTER : (endsParagraph) ? LEFT : JUSTIFY); if (wordsSize > 0) { int textWidth = 0; int x = 0; if (startsNewParagraph) { lineWidth = lineWidth - fontIndent; x = fontIndent; } for (int i = 0; i < wordsSize; i++) { TextRegion word = (TextRegion) words.elementAt(i); textWidth += word.width; //compute width without spaces } final int ltw = lineWidth - textWidth; int spacing = 0; int additionalSpacing = 0; /* set spacing */ if (align != JUSTIFY) { spacing = wordSpacing; } else { /* calculate spacing so words would be justified */ if (words.size() > 1) { spacing = ltw / wordsSize1; additionalSpacing = ltw % wordsSize1; } } /* calc X so that the block would be centered */ if (align == CENTER) { x = (ltw - (spacing * wordsSize1) ) / 2; } // /* align right */ // if (align == RIGHT) { // x = (lineWidth - (textWidth + (spacing * (wordsSize-1)))); // } for (int i = 0; i < wordsSize; i++) { TextRegion word = (TextRegion)words.elementAt(i); word.x = (short) x; word.y = (short) lineY; x += word.width + spacing; if (i == 0) { x += additionalSpacing; } if (i < wordsSize1) { word.width += (short) spacing; if (i == 0) { word.width += (short) additionalSpacing; } } regionsTemp.addElement(word); } } } public final int getStart() { return start; } public final int getEnd() { return end; } public final boolean contains(final int position) { return start <= position && position < end; } public final Region getRegionAt(final int x, final int y) { Region current = null; int regionsSize = regions.length; for (int i = 0; i < regionsSize; i++) { current = regions[i]; if (current.containsPoint2D(x, y)) { return current; } } return null; } public final int getRegionIndexAt(final int x, final int y) { Region current = null; int regionsSize = regions.length; for (int i = 0; i < regionsSize; i++) { current = regions[i]; if (current.containsPoint2D(x, y)) { return i; } } return -1; } public final boolean isEmpty() { return (regions.length == 0) && (imageRegion == null); } public final void draw( final Graphics g, final ColorScheme cp, final AlbiteFont fontPlain, final AlbiteFont fontItalic, final char[] textBuffer) { final int regionsSize = regions.length; //Draw the image if there's one if (imageRegion != null) { int textTopCorner = 0; int textHeight = 0; if (regionsSize > 0) { /* * There is alt text */ textTopCorner = regions[0].y; final Region r = regions[regions.length - 1]; textHeight = r.y + r.height - textTopCorner; } Image image = imageRegion.getImage( booklet.width, booklet.height, textHeight); final int margin = ImageRegion.MARGIN; final int imageW = image.getWidth() + 4 * margin + 2; final int imageH = image.getHeight() + 4 * margin + 2; final int imageX = (booklet.width - imageW) / 2; int imageY = (booklet.height - imageH) / 2; if (regionsSize > 0) { /* * There is alt text */ final int h = imageH + textHeight; int offset = (booklet.height - h) / 2; imageY = offset; offset += imageH - textTopCorner; for (int i = 0; i < regionsSize; i++) { regions[i].y += offset; } } g.setColor(cp.colors[ColorScheme.COLOR_FRAME]); g.drawRect( imageX + margin, imageY + margin, image.getWidth() + 2 * margin + 1, image.getHeight() + 2 * margin + 1); g.drawImage(image, imageX + 2 * margin + 1, imageY + 2 * margin + 1, Graphics.TOP | Graphics.LEFT); } //Draw the rest of the regions for (int i = 0; i < regionsSize; i++) { regions[i].draw(g, cp, fontPlain, fontItalic, textBuffer); } } public final void drawSelected( final Graphics g, final ColorScheme cp, final int firstElement, final int lastElement) { final AlbiteFont fontPlain = booklet.fontPlain; final AlbiteFont fontItalic = booklet.fontItalic; final char[] textBuffer = booklet.getTextBuffer(); final int regionsSize = regions.length; final int k = Math.min(firstElement, lastElement); final int l = Math.max(firstElement, lastElement); for (int i = 0; i < regionsSize; i++) { if (i >= k && i <= l) { regions[i].drawSelected( g, cp, fontPlain, fontItalic, textBuffer); } else { regions[i].draw(g, cp, fontPlain, fontItalic, textBuffer); } } } public final String getTextForBookmark(final char[] chapterBuffer) { final int size = regions.length; StringBuffer buf = new StringBuffer(48); for (int i = 0; i < size && buf.length() < 24; i++) { regions[i].addTextChunk(chapterBuffer, buf); } if (buf.charAt(buf.length() - 1) == ' ') { buf.deleteCharAt(buf.length() - 1); } return buf.toString(); } public final String getTextForBookmark( final char[] chapterBuffer, final int firstIndex, final int lastIndex) { int first = Math.min(firstIndex, lastIndex); int last = Math.max(firstIndex, lastIndex); if (first < 0) { first = 0; } if (last >= regions.length) { last = regions.length - 1; } final StringBuffer buf = new StringBuffer(100); for (int i = first; i <= last; i++) { regions[i].addTextChunk(chapterBuffer, buf); } if (buf.charAt(buf.length() - 1) == ' ') { buf.deleteCharAt(buf.length() - 1); } return buf.toString(); } public static AlbiteFont chooseFont( final AlbiteFont fontPlain, final AlbiteFont fontItalic, final byte style) { AlbiteFont font = fontPlain; if ((style & ITALIC) == ITALIC) { font = fontItalic; } return font; } public static byte chooseTextColor(final byte style) { byte color = ColorScheme.COLOR_TEXT; if ((style & ITALIC) == ITALIC) { color = ColorScheme.COLOR_TEXT_ITALIC; } if ((style & BOLD) == BOLD) { color = ColorScheme.COLOR_TEXT_BOLD; } if ((style & HEADING) == HEADING) { color = ColorScheme.COLOR_TEXT_HEADING; } return color; } public final boolean hasImage() { return (imageRegion != null); } public Region getRegionForIndex(final int index) { return regions[index]; } }