package pl.djvuhtml5.client.ui; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import com.google.gwt.canvas.client.Canvas; import com.google.gwt.canvas.dom.client.Context2d; import com.google.gwt.core.client.JavaScriptObject; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Style; import com.google.gwt.dom.client.Style.Unit; import com.google.gwt.event.dom.client.ScrollEvent; import com.google.gwt.event.dom.client.ScrollHandler; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.Label; import com.lizardtech.djvu.DjVuInfo; import com.lizardtech.djvu.text.DjVuText; import com.lizardtech.djvu.text.DjVuText.Zone; import pl.djvuhtml5.client.DjvuContext; import pl.djvuhtml5.client.Djvu_html5; public class TextLayer extends FlowPanel implements ScrollHandler { private static final String PAGE_STYLE_VISIBLE = "visibleTextPage"; /** * Extra space to the left so that user can scroll over the left edge and go to * the previous page. */ private static final int EXTRA_PAGE_MARGIN = 500; private class TextPage extends FlowPanel { final List<TextLine> textLines = new ArrayList<>(); DjVuText text; int width, height; TextPage() { setStyleName("textPage"); setPixelSize(1, 1); } void setSize(int width, int height) { this.width = width; this.height = height; setPixelSize(width, height); } void resize(double zoom, boolean force) { int w = (int) (width * zoom + 0.5); int h = (int) (height * zoom + 0.5); if (force || w != getOffsetWidth() || h != getOffsetHeight()) { setPixelSize(w, h); for (TextLine textLine : textLines) textLine.resize(zoom); } } String getText(Zone zone) { return text.getString(zone.text_start, zone.text_start + zone.text_length); } } private class TextLine extends FlowPanel { final List<Token> tokens = new ArrayList<>(); TextLine(int minY, int maxY, int prevY, TextPage parent, List<Zone> lineTokens) { Style style = getElement().getStyle(); double marginTop = round((prevY - maxY) * 100.0 / parent.width, PCT_ACCURACY); if (parent.getWidgetCount() == 0) { style.setPaddingTop(marginTop, Unit.PCT); } else { style.setMarginTop(marginTop, Unit.PCT); } style.setHeight(round((maxY - minY) * 100.0 / parent.height, PCT_ACCURACY), Unit.PCT); Token lastToken = null; int prevXmax = 0; for (Zone zone : lineTokens) { Token token = new Token(zone, prevXmax, parent); if (token.text.isEmpty()) { if (lastToken != null) lastToken.setText(lastToken.getText() + token.getText()); continue; } add(token); lastToken = token; if (token.text.length() > 1 || lineTokens.size() == 1) { tokens.add(token); } prevXmax = zone.xmax; } parent.textLines.add(this); parent.add(this); } void resize(double zoom) { double fontSize = 0; for (Token token : tokens) fontSize += token.getFontSize(zoom); fontSize = round(fontSize / tokens.size(), FONT_ACCURACY); getElement().getStyle().setFontSize(fontSize, Unit.PX); for (Token token : tokens) token.adjustSpacing(fontSize, zoom); } } private class Token extends Label { /** Token's text stripped of excessive whitespace, in contrast to {@link #getText()} */ final String text; final double fullWidth; Token(Zone zone, int prevXmax, TextPage page) { super(Document.get().createSpanElement()); String text = page.getText(zone); setText(text); fullWidth = zone.xmax - zone.xmin; this.text = text.trim().replaceAll("\\s+", ""); Style s = getElement().getStyle(); double marginLeft = (zone.xmin - prevXmax) * 100.0 / page.width; s.setMarginLeft(round(marginLeft, PCT_ACCURACY), Unit.PCT); double width = fullWidth * 100.0 / page.width; s.setWidth(round(width, PCT_ACCURACY), Unit.PCT); } double getFontSize(double zoom) { final double width = fullWidth * zoom; double fontSize = previousFontSize; double diff = Double.MAX_VALUE, prevDiff; double ratio = 1; int count = 0; do { fontSize *= ratio; ratio = width / getWidth(fontSize); prevDiff = diff; diff = Math.abs(1 - ratio); } while (count++ < 6 && diff > 0.05 && diff < prevDiff); previousFontSize = fontSize; return fontSize; } void adjustSpacing(double fontSize, double zoom) { int textLength = text.length(); if (textLength < 2) return; double width = getWidth(fontSize); double spacing = (fullWidth * zoom - width) / (textLength - 1); getElement().getStyle().setProperty("letterSpacing", round(spacing, 0.1), Unit.PX); } private double getWidth(double fontSize) { if (fontFamily == null) fontFamily = getComputedFontFamily(getElement()); fontMeasure.setFont(fontSize + "px " + fontFamily); return fontMeasure.measureText(text).getWidth(); } private native String getComputedFontFamily(JavaScriptObject element) /*-{ return document.defaultView.getComputedStyle(element, null)["fontFamily"]; }-*/; } private static final double PCT_ACCURACY = 0.0001; private static final double FONT_ACCURACY = 0.5; private final Djvu_html5 app; private final List<TextPage> pages = new ArrayList<>(); private final Context2d fontMeasure; private String fontFamily; private double previousFontSize = 20; private int currentPage = 0; public TextLayer(Djvu_html5 app) { this.app = app; setStyleName("textLayer"); getElement().setAttribute("tabindex", "-1"); addDomHandler(this, ScrollEvent.getType()); app.getDataStore().addInfoListener(this::pageInfoAvailable); if (DjvuContext.getTextLayerEnabled()) app.getDataStore().addTextListener(this::textAvailable); fontMeasure = Canvas.createIfSupported().getContext2d(); } private void pageInfoAvailable(int pageNum) { DjVuInfo pageInfo = app.getDataStore().getPageInfo(pageNum); getPage(pageNum).setSize(pageInfo.width, pageInfo.height); } private void textAvailable(int pageNum) { DjVuText text = app.getDataStore().getText(pageNum); TextPage page = getPage(pageNum); page.text = text; if (text.length() > 0) { List<Zone> tokens = new ArrayList<>(); text.page_zone.get_smallest(tokens); createTextLines(page, tokens); if (pageNum == currentPage) app.getPageLayout().setPage(pageNum); } } private void createTextLines(TextPage page, List<Zone> tokens) { int prevY = page.height; while (!tokens.isEmpty()) { List<Zone> lineTokens = new ArrayList<>(); Zone lastToken = null; int minY = Integer.MAX_VALUE, maxY = Integer.MIN_VALUE; while (!tokens.isEmpty()) { Zone token = tokens.get(0); boolean hasMultipleChars = page.getText(token).trim().length() > 1; if (lastToken != null) { //make sure it's the same line if (token.xmin < lastToken.xmax) break; int commonPart = Math.min(maxY, token.ymax) - Math.max(minY, token.ymin); int total = Math.max(maxY, token.ymax) - Math.min(minY, token.ymin); if (hasMultipleChars) { if (commonPart <= 0) break; } else { if (commonPart * 2 <= total) break; } } tokens.remove(0); lineTokens.add(token); if (hasMultipleChars) { lastToken = token; minY = Math.min(minY, token.ymin); maxY = Math.max(maxY, token.ymax); } } if (lastToken != null) { new TextLine(minY, maxY, prevY, page, lineTokens); prevY = minY; } else { for (Zone lineToken : lineTokens) { new TextLine(lineToken.ymin, lineToken.ymax, prevY, page, Arrays.asList(lineToken)); prevY = lineToken.ymin; } } } } /** * @param left position of page's left edge on the canvas * @param top position of page's top edge on the canvas */ public void setViewPosition(int pageNum, int left, int top, double zoom) { boolean pageChanged = currentPage != pageNum; TextPage page = getPage(pageNum); if (pageChanged) { getPage(currentPage).removeStyleName(PAGE_STYLE_VISIBLE); page.addStyleName(PAGE_STYLE_VISIBLE); } page.resize(zoom, pageChanged); currentPage = pageNum; Element layerElement = getElement(); Element pageElement = page.getElement(); pageElement.getStyle().setMarginLeft(Math.max(left, 0) + EXTRA_PAGE_MARGIN, Unit.PX); int targetScrollLeft = Math.max(-left, 0) + EXTRA_PAGE_MARGIN; if (layerElement.getScrollLeft() != targetScrollLeft) layerElement.setScrollLeft(targetScrollLeft); int targetScrollTop = pageElement.getOffsetTop() - top; if (layerElement.getScrollTop() != targetScrollTop) layerElement.setScrollTop(targetScrollTop); } @Override public void onScroll(ScrollEvent event) { int scrollTop = getElement().getScrollTop(); int page = currentPage; Element pageElement = pages.get(page).getElement(); while (page > 0 && pageElement.getOffsetTop() > scrollTop) { pageElement = pages.get(--page).getElement(); } while (page + 1 < pages.size() && pageElement.getOffsetTop() + pageElement.getOffsetHeight() < scrollTop) { pageElement = pages.get(++page).getElement(); } int left = pageElement.getOffsetLeft() - getElement().getScrollLeft(); int top = pageElement.getOffsetTop() - scrollTop; app.getPageLayout().externalScroll(page, left, top); } private TextPage getPage(int pageNum) { while (pageNum >= pages.size()) { TextPage page = new TextPage(); if (pages.size() == currentPage) page.addStyleName(PAGE_STYLE_VISIBLE); add(page); pages.add(page); } return pages.get(pageNum); } private static double round(double value, double accuracy) { return Math.round((value + accuracy / 2) / accuracy) * accuracy; } }