1 package cz.cvut.fel.dce.qrscanner.mupdf;
3 import java.util.ArrayList;
4 import java.util.Iterator;
6 import android.content.Context;
7 import android.graphics.Bitmap;
8 import android.graphics.Bitmap.Config;
9 import android.graphics.Canvas;
10 import android.graphics.Color;
11 import android.graphics.Matrix;
12 import android.graphics.Paint;
13 import android.graphics.Path;
14 import android.graphics.Point;
15 import android.graphics.PointF;
16 import android.graphics.Rect;
17 import android.graphics.RectF;
18 import android.os.Handler;
19 import android.view.View;
20 import android.view.ViewGroup;
21 import android.widget.ImageView;
22 import android.widget.ProgressBar;
24 import cz.cvut.fel.dce.qrscanner.R;
26 // Make our ImageViews opaque to optimize redraw
27 class OpaqueImageView extends ImageView {
29 public OpaqueImageView(Context context) {
34 public boolean isOpaque() {
39 interface TextProcessor {
41 void onWord(TextWord word);
46 final private TextWord[][] mText;
47 final private RectF mSelectBox;
49 public TextSelector(TextWord[][] text, RectF selectBox) {
51 mSelectBox = selectBox;
54 public void select(TextProcessor tp) {
55 if (mText == null || mSelectBox == null)
58 ArrayList<TextWord[]> lines = new ArrayList<TextWord[]>();
59 for (TextWord[] line : mText)
60 if (line[0].bottom > mSelectBox.top && line[0].top < mSelectBox.bottom)
63 Iterator<TextWord[]> it = lines.iterator();
64 while (it.hasNext()) {
65 TextWord[] line = it.next();
66 boolean firstLine = line[0].top < mSelectBox.top;
67 boolean lastLine = line[0].bottom > mSelectBox.bottom;
68 float start = Float.NEGATIVE_INFINITY;
69 float end = Float.POSITIVE_INFINITY;
71 if (firstLine && lastLine) {
72 start = Math.min(mSelectBox.left, mSelectBox.right);
73 end = Math.max(mSelectBox.left, mSelectBox.right);
74 } else if (firstLine) {
75 start = mSelectBox.left;
76 } else if (lastLine) {
77 end = mSelectBox.right;
82 for (TextWord word : line)
83 if (word.right > start && word.left < end)
91 public abstract class PageView extends ViewGroup {
92 private static final int HIGHLIGHT_COLOR = 0x802572AC;
93 private static final int LINK_COLOR = 0x80AC7225;
94 private static final int BOX_COLOR = 0xFF4444FF;
95 private static final int INK_COLOR = 0xFFFF0000;
96 private static final float INK_THICKNESS = 10.0f;
97 private static final int BACKGROUND_COLOR = 0xFFFFFFFF;
98 private static final int PROGRESS_DIALOG_DELAY = 200;
99 protected final Context mContext;
100 protected int mPageNumber;
101 private Point mParentSize;
102 protected Point mSize; // Size of page at minimum zoom
103 protected float mSourceScale;
105 private ImageView mEntire; // Image rendered at minimum zoom
106 private Bitmap mEntireBm;
107 private Matrix mEntireMat;
108 private AsyncTask<Void,Void,TextWord[][]> mGetText;
109 private AsyncTask<Void,Void,LinkInfo[]> mGetLinkInfo;
110 private CancellableAsyncTask<Void, Void> mDrawEntire;
112 private Point mPatchViewSize; // View size on the basis of which the patch was created
113 private Rect mPatchArea;
114 private ImageView mPatch;
115 private Bitmap mPatchBm;
116 private CancellableAsyncTask<Void,Void> mDrawPatch;
117 private RectF mSearchBoxes[];
118 protected LinkInfo mLinks[];
119 private RectF mSelectBox;
120 private TextWord mText[][];
121 private RectF mItemSelectBox;
122 protected ArrayList<ArrayList<PointF>> mDrawing;
123 private View mSearchView;
124 private boolean mIsBlank;
125 private boolean mHighlightLinks;
127 private ProgressBar mBusyIndicator;
128 private final Handler mHandler = new Handler();
130 public PageView(Context c, Point parentSize, Bitmap sharedHqBm) {
133 mParentSize = parentSize;
134 setBackgroundColor(BACKGROUND_COLOR);
135 mEntireBm = Bitmap.createBitmap(parentSize.x, parentSize.y, Config.ARGB_8888);
136 mPatchBm = sharedHqBm;
137 mEntireMat = new Matrix();
140 protected abstract CancellableTaskDefinition<Void, Void> getDrawPageTask(Bitmap bm, int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight);
141 protected abstract CancellableTaskDefinition<Void, Void> getUpdatePageTask(Bitmap bm, int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight);
142 protected abstract LinkInfo[] getLinkInfo();
143 protected abstract TextWord[][] getText();
144 protected abstract void addMarkup(PointF[] quadPoints, Annotation.Type type);
146 private void reinit() {
147 // Cancel pending render task
148 if (mDrawEntire != null) {
149 mDrawEntire.cancelAndWait();
153 if (mDrawPatch != null) {
154 mDrawPatch.cancelAndWait();
158 if (mGetLinkInfo != null) {
159 mGetLinkInfo.cancel(true);
163 if (mGetText != null) {
164 mGetText.cancel(true);
174 if (mEntire != null) {
175 mEntire.setImageBitmap(null);
176 mEntire.invalidate();
179 if (mPatch != null) {
180 mPatch.setImageBitmap(null);
184 mPatchViewSize = null;
191 mItemSelectBox = null;
194 public void releaseResources() {
197 if (mBusyIndicator != null) {
198 removeView(mBusyIndicator);
199 mBusyIndicator = null;
203 public void releaseBitmaps() {
209 public void blank(int page) {
213 if (mBusyIndicator == null) {
214 mBusyIndicator = new ProgressBar(mContext);
215 mBusyIndicator.setIndeterminate(true);
216 mBusyIndicator.setBackgroundResource(R.drawable.busy);
217 addView(mBusyIndicator);
220 setBackgroundColor(BACKGROUND_COLOR);
223 public void setPage(int page, PointF size) {
224 // Cancel pending render task
225 if (mDrawEntire != null) {
226 mDrawEntire.cancelAndWait();
231 // Highlights may be missing because mIsBlank was true on last draw
232 if (mSearchView != null)
233 mSearchView.invalidate();
236 if (mEntire == null) {
237 mEntire = new OpaqueImageView(mContext);
238 mEntire.setScaleType(ImageView.ScaleType.MATRIX);
242 // Calculate scaled size that fits within the screen limits
243 // This is the size at minimum zoom
244 mSourceScale = Math.min(mParentSize.x/size.x, mParentSize.y/size.y);
245 Point newSize = new Point((int)(size.x*mSourceScale), (int)(size.y*mSourceScale));
248 mEntire.setImageBitmap(null);
249 mEntire.invalidate();
251 // Get the link info in the background
252 mGetLinkInfo = new AsyncTask<Void,Void,LinkInfo[]>() {
253 protected LinkInfo[] doInBackground(Void... v) {
254 return getLinkInfo();
257 protected void onPostExecute(LinkInfo[] v) {
259 if (mSearchView != null)
260 mSearchView.invalidate();
264 mGetLinkInfo.execute();
266 // Render the page in the background
267 mDrawEntire = new CancellableAsyncTask<Void, Void>(getDrawPageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
270 public void onPreExecute() {
271 setBackgroundColor(BACKGROUND_COLOR);
272 mEntire.setImageBitmap(null);
273 mEntire.invalidate();
275 if (mBusyIndicator == null) {
276 mBusyIndicator = new ProgressBar(mContext);
277 mBusyIndicator.setIndeterminate(true);
278 mBusyIndicator.setBackgroundResource(R.drawable.busy);
279 addView(mBusyIndicator);
280 mBusyIndicator.setVisibility(INVISIBLE);
281 mHandler.postDelayed(new Runnable() {
283 if (mBusyIndicator != null)
284 mBusyIndicator.setVisibility(VISIBLE);
286 }, PROGRESS_DIALOG_DELAY);
291 public void onPostExecute(Void result) {
292 removeView(mBusyIndicator);
293 mBusyIndicator = null;
294 mEntire.setImageBitmap(mEntireBm);
295 mEntire.invalidate();
296 setBackgroundColor(Color.TRANSPARENT);
301 mDrawEntire.execute();
303 if (mSearchView == null) {
304 mSearchView = new View(mContext) {
306 protected void onDraw(final Canvas canvas) {
307 super.onDraw(canvas);
308 // Work out current total scale factor
309 // from source to view
310 final float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
311 final Paint paint = new Paint();
313 if (!mIsBlank && mSearchBoxes != null) {
314 paint.setColor(HIGHLIGHT_COLOR);
315 for (RectF rect : mSearchBoxes)
316 canvas.drawRect(rect.left*scale, rect.top*scale,
317 rect.right*scale, rect.bottom*scale,
321 if (!mIsBlank && mLinks != null && mHighlightLinks) {
322 paint.setColor(LINK_COLOR);
323 for (LinkInfo link : mLinks)
324 canvas.drawRect(link.rect.left*scale, link.rect.top*scale,
325 link.rect.right*scale, link.rect.bottom*scale,
329 if (mSelectBox != null && mText != null) {
330 paint.setColor(HIGHLIGHT_COLOR);
331 processSelectedText(new TextProcessor() {
334 public void onStartLine() {
338 public void onWord(TextWord word) {
342 public void onEndLine() {
344 canvas.drawRect(rect.left*scale, rect.top*scale, rect.right*scale, rect.bottom*scale, paint);
349 if (mItemSelectBox != null) {
350 paint.setStyle(Paint.Style.STROKE);
351 paint.setColor(BOX_COLOR);
352 canvas.drawRect(mItemSelectBox.left*scale, mItemSelectBox.top*scale, mItemSelectBox.right*scale, mItemSelectBox.bottom*scale, paint);
355 if (mDrawing != null) {
356 Path path = new Path();
359 paint.setAntiAlias(true);
360 paint.setDither(true);
361 paint.setStrokeJoin(Paint.Join.ROUND);
362 paint.setStrokeCap(Paint.Cap.ROUND);
364 paint.setStyle(Paint.Style.FILL);
365 paint.setStrokeWidth(INK_THICKNESS * scale);
366 paint.setColor(INK_COLOR);
368 Iterator<ArrayList<PointF>> it = mDrawing.iterator();
369 while (it.hasNext()) {
370 ArrayList<PointF> arc = it.next();
371 if (arc.size() >= 2) {
372 Iterator<PointF> iit = arc.iterator();
374 float mX = p.x * scale;
375 float mY = p.y * scale;
377 while (iit.hasNext()) {
379 float x = p.x * scale;
380 float y = p.y * scale;
381 path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
388 canvas.drawCircle(p.x * scale, p.y * scale, INK_THICKNESS * scale / 2, paint);
392 paint.setStyle(Paint.Style.STROKE);
393 canvas.drawPath(path, paint);
398 addView(mSearchView);
403 public void setSearchBoxes(RectF searchBoxes[]) {
404 mSearchBoxes = searchBoxes;
405 if (mSearchView != null)
406 mSearchView.invalidate();
409 public void setLinkHighlighting(boolean f) {
411 if (mSearchView != null)
412 mSearchView.invalidate();
415 public void deselectText() {
417 mSearchView.invalidate();
420 public void selectText(float x0, float y0, float x1, float y1) {
421 float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
422 float docRelX0 = (x0 - getLeft())/scale;
423 float docRelY0 = (y0 - getTop())/scale;
424 float docRelX1 = (x1 - getLeft())/scale;
425 float docRelY1 = (y1 - getTop())/scale;
426 // Order on Y but maintain the point grouping
427 if (docRelY0 <= docRelY1)
428 mSelectBox = new RectF(docRelX0, docRelY0, docRelX1, docRelY1);
430 mSelectBox = new RectF(docRelX1, docRelY1, docRelX0, docRelY0);
432 mSearchView.invalidate();
434 if (mGetText == null) {
435 mGetText = new AsyncTask<Void,Void,TextWord[][]>() {
437 protected TextWord[][] doInBackground(Void... params) {
441 protected void onPostExecute(TextWord[][] result) {
443 mSearchView.invalidate();
451 public void startDraw(float x, float y) {
452 float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
453 float docRelX = (x - getLeft())/scale;
454 float docRelY = (y - getTop())/scale;
455 if (mDrawing == null)
456 mDrawing = new ArrayList<ArrayList<PointF>>();
458 ArrayList<PointF> arc = new ArrayList<PointF>();
459 arc.add(new PointF(docRelX, docRelY));
461 mSearchView.invalidate();
464 public void continueDraw(float x, float y) {
465 float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
466 float docRelX = (x - getLeft())/scale;
467 float docRelY = (y - getTop())/scale;
469 if (mDrawing != null && mDrawing.size() > 0) {
470 ArrayList<PointF> arc = mDrawing.get(mDrawing.size() - 1);
471 arc.add(new PointF(docRelX, docRelY));
472 mSearchView.invalidate();
476 public void cancelDraw() {
478 mSearchView.invalidate();
481 protected PointF[][] getDraw() {
482 if (mDrawing == null)
485 PointF[][] path = new PointF[mDrawing.size()][];
487 for (int i = 0; i < mDrawing.size(); i++) {
488 ArrayList<PointF> arc = mDrawing.get(i);
489 path[i] = arc.toArray(new PointF[arc.size()]);
495 protected void processSelectedText(TextProcessor tp) {
496 (new TextSelector(mText, mSelectBox)).select(tp);
499 public void setItemSelectBox(RectF rect) {
500 mItemSelectBox = rect;
501 if (mSearchView != null)
502 mSearchView.invalidate();
506 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
508 switch(View.MeasureSpec.getMode(widthMeasureSpec)) {
509 case View.MeasureSpec.UNSPECIFIED:
513 x = View.MeasureSpec.getSize(widthMeasureSpec);
515 switch(View.MeasureSpec.getMode(heightMeasureSpec)) {
516 case View.MeasureSpec.UNSPECIFIED:
520 y = View.MeasureSpec.getSize(heightMeasureSpec);
523 setMeasuredDimension(x, y);
525 if (mBusyIndicator != null) {
526 int limit = Math.min(mParentSize.x, mParentSize.y)/2;
527 mBusyIndicator.measure(View.MeasureSpec.AT_MOST | limit, View.MeasureSpec.AT_MOST | limit);
532 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
536 if (mEntire != null) {
537 if (mEntire.getWidth() != w || mEntire.getHeight() != h) {
538 mEntireMat.setScale(w/(float)mSize.x, h/(float)mSize.y);
539 mEntire.setImageMatrix(mEntireMat);
540 mEntire.invalidate();
542 mEntire.layout(0, 0, w, h);
545 if (mSearchView != null) {
546 mSearchView.layout(0, 0, w, h);
549 if (mPatchViewSize != null) {
550 if (mPatchViewSize.x != w || mPatchViewSize.y != h) {
551 // Zoomed since patch was created
552 mPatchViewSize = null;
554 if (mPatch != null) {
555 mPatch.setImageBitmap(null);
559 mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
563 if (mBusyIndicator != null) {
564 int bw = mBusyIndicator.getMeasuredWidth();
565 int bh = mBusyIndicator.getMeasuredHeight();
567 mBusyIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2);
571 public void updateHq(boolean update) {
572 Rect viewArea = new Rect(getLeft(),getTop(),getRight(),getBottom());
573 if (viewArea.width() == mSize.x || viewArea.height() == mSize.y) {
574 // If the viewArea's size matches the unzoomed size, there is no need for an hq patch
575 if (mPatch != null) {
576 mPatch.setImageBitmap(null);
580 final Point patchViewSize = new Point(viewArea.width(), viewArea.height());
581 final Rect patchArea = new Rect(0, 0, mParentSize.x, mParentSize.y);
583 // Intersect and test that there is an intersection
584 if (!patchArea.intersect(viewArea))
587 // Offset patch area to be relative to the view top left
588 patchArea.offset(-viewArea.left, -viewArea.top);
590 boolean area_unchanged = patchArea.equals(mPatchArea) && patchViewSize.equals(mPatchViewSize);
592 // If being asked for the same area as last time and not because of an update then nothing to do
593 if (area_unchanged && !update)
596 boolean completeRedraw = !(area_unchanged && update);
598 // Stop the drawing of previous patch if still going
599 if (mDrawPatch != null) {
600 mDrawPatch.cancelAndWait();
604 // Create and add the image view if not already done
605 if (mPatch == null) {
606 mPatch = new OpaqueImageView(mContext);
607 mPatch.setScaleType(ImageView.ScaleType.MATRIX);
609 mSearchView.bringToFront();
612 CancellableTaskDefinition<Void, Void> task;
615 task = getDrawPageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
616 patchArea.left, patchArea.top,
617 patchArea.width(), patchArea.height());
619 task = getUpdatePageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
620 patchArea.left, patchArea.top,
621 patchArea.width(), patchArea.height());
623 mDrawPatch = new CancellableAsyncTask<Void,Void>(task) {
625 public void onPostExecute(Void result) {
626 mPatchViewSize = patchViewSize;
627 mPatchArea = patchArea;
628 mPatch.setImageBitmap(mPatchBm);
631 // Calling requestLayout here doesn't lead to a later call to layout. No idea
632 // why, but apparently others have run into the problem.
633 mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
637 mDrawPatch.execute();
641 public void update() {
642 // Cancel pending render task
643 if (mDrawEntire != null) {
644 mDrawEntire.cancelAndWait();
648 if (mDrawPatch != null) {
649 mDrawPatch.cancelAndWait();
654 // Render the page in the background
655 mDrawEntire = new CancellableAsyncTask<Void, Void>(getUpdatePageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
657 public void onPostExecute(Void result) {
658 mEntire.setImageBitmap(mEntireBm);
659 mEntire.invalidate();
663 mDrawEntire.execute();
668 public void removeHq() {
669 // Stop the drawing of the patch if still going
670 if (mDrawPatch != null) {
671 mDrawPatch.cancelAndWait();
676 mPatchViewSize = null;
678 if (mPatch != null) {
679 mPatch.setImageBitmap(null);
684 public int getPage() {
689 public boolean isOpaque() {