Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
Download
7859 views
1
package com.artifex.mupdfdemo;
2
3
import java.util.ArrayList;
4
import java.util.Iterator;
5
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;
23
24
// Make our ImageViews opaque to optimize redraw
25
class OpaqueImageView extends ImageView {
26
27
public OpaqueImageView(Context context) {
28
super(context);
29
}
30
31
@Override
32
public boolean isOpaque() {
33
return true;
34
}
35
}
36
37
interface TextProcessor {
38
void onStartLine();
39
void onWord(TextWord word);
40
void onEndLine();
41
}
42
43
class TextSelector {
44
final private TextWord[][] mText;
45
final private RectF mSelectBox;
46
47
public TextSelector(TextWord[][] text, RectF selectBox) {
48
mText = text;
49
mSelectBox = selectBox;
50
}
51
52
public void select(TextProcessor tp) {
53
if (mText == null || mSelectBox == null)
54
return;
55
56
ArrayList<TextWord[]> lines = new ArrayList<TextWord[]>();
57
for (TextWord[] line : mText)
58
if (line[0].bottom > mSelectBox.top && line[0].top < mSelectBox.bottom)
59
lines.add(line);
60
61
Iterator<TextWord[]> it = lines.iterator();
62
while (it.hasNext()) {
63
TextWord[] line = it.next();
64
boolean firstLine = line[0].top < mSelectBox.top;
65
boolean lastLine = line[0].bottom > mSelectBox.bottom;
66
float start = Float.NEGATIVE_INFINITY;
67
float end = Float.POSITIVE_INFINITY;
68
69
if (firstLine && lastLine) {
70
start = Math.min(mSelectBox.left, mSelectBox.right);
71
end = Math.max(mSelectBox.left, mSelectBox.right);
72
} else if (firstLine) {
73
start = mSelectBox.left;
74
} else if (lastLine) {
75
end = mSelectBox.right;
76
}
77
78
tp.onStartLine();
79
80
for (TextWord word : line)
81
if (word.right > start && word.left < end)
82
tp.onWord(word);
83
84
tp.onEndLine();
85
}
86
}
87
}
88
89
public abstract class PageView extends ViewGroup {
90
private static final int HIGHLIGHT_COLOR = 0x802572AC;
91
private static final int LINK_COLOR = 0x80AC7225;
92
private static final int BOX_COLOR = 0xFF4444FF;
93
private static final int INK_COLOR = 0xFFFF0000;
94
private static final float INK_THICKNESS = 10.0f;
95
private static final int BACKGROUND_COLOR = 0xFFFFFFFF;
96
private static final int PROGRESS_DIALOG_DELAY = 200;
97
protected final Context mContext;
98
protected int mPageNumber;
99
private Point mParentSize;
100
protected Point mSize; // Size of page at minimum zoom
101
protected float mSourceScale;
102
103
private ImageView mEntire; // Image rendered at minimum zoom
104
private Bitmap mEntireBm;
105
private Matrix mEntireMat;
106
private AsyncTask<Void,Void,TextWord[][]> mGetText;
107
private AsyncTask<Void,Void,LinkInfo[]> mGetLinkInfo;
108
private CancellableAsyncTask<Void, Void> mDrawEntire;
109
110
private Point mPatchViewSize; // View size on the basis of which the patch was created
111
private Rect mPatchArea;
112
private ImageView mPatch;
113
private Bitmap mPatchBm;
114
private CancellableAsyncTask<Void,Void> mDrawPatch;
115
private RectF mSearchBoxes[];
116
protected LinkInfo mLinks[];
117
private RectF mSelectBox;
118
private TextWord mText[][];
119
private RectF mItemSelectBox;
120
protected ArrayList<ArrayList<PointF>> mDrawing;
121
private View mSearchView;
122
private boolean mIsBlank;
123
private boolean mHighlightLinks;
124
125
private ProgressBar mBusyIndicator;
126
private final Handler mHandler = new Handler();
127
128
public PageView(Context c, Point parentSize, Bitmap sharedHqBm) {
129
super(c);
130
mContext = c;
131
mParentSize = parentSize;
132
setBackgroundColor(BACKGROUND_COLOR);
133
mEntireBm = Bitmap.createBitmap(parentSize.x, parentSize.y, Config.ARGB_8888);
134
mPatchBm = sharedHqBm;
135
mEntireMat = new Matrix();
136
}
137
138
protected abstract CancellableTaskDefinition<Void, Void> getDrawPageTask(Bitmap bm, int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight);
139
protected abstract CancellableTaskDefinition<Void, Void> getUpdatePageTask(Bitmap bm, int sizeX, int sizeY, int patchX, int patchY, int patchWidth, int patchHeight);
140
protected abstract LinkInfo[] getLinkInfo();
141
protected abstract TextWord[][] getText();
142
protected abstract void addMarkup(PointF[] quadPoints, Annotation.Type type);
143
144
private void reinit() {
145
// Cancel pending render task
146
if (mDrawEntire != null) {
147
mDrawEntire.cancelAndWait();
148
mDrawEntire = null;
149
}
150
151
if (mDrawPatch != null) {
152
mDrawPatch.cancelAndWait();
153
mDrawPatch = null;
154
}
155
156
if (mGetLinkInfo != null) {
157
mGetLinkInfo.cancel(true);
158
mGetLinkInfo = null;
159
}
160
161
if (mGetText != null) {
162
mGetText.cancel(true);
163
mGetText = null;
164
}
165
166
mIsBlank = true;
167
mPageNumber = 0;
168
169
if (mSize == null)
170
mSize = mParentSize;
171
172
if (mEntire != null) {
173
mEntire.setImageBitmap(null);
174
mEntire.invalidate();
175
}
176
177
if (mPatch != null) {
178
mPatch.setImageBitmap(null);
179
mPatch.invalidate();
180
}
181
182
mPatchViewSize = null;
183
mPatchArea = null;
184
185
mSearchBoxes = null;
186
mLinks = null;
187
mSelectBox = null;
188
mText = null;
189
mItemSelectBox = null;
190
}
191
192
public void releaseResources() {
193
reinit();
194
195
if (mBusyIndicator != null) {
196
removeView(mBusyIndicator);
197
mBusyIndicator = null;
198
}
199
}
200
201
public void releaseBitmaps() {
202
reinit();
203
mEntireBm = null;
204
mPatchBm = null;
205
}
206
207
public void blank(int page) {
208
reinit();
209
mPageNumber = page;
210
211
if (mBusyIndicator == null) {
212
mBusyIndicator = new ProgressBar(mContext);
213
mBusyIndicator.setIndeterminate(true);
214
mBusyIndicator.setBackgroundResource(R.drawable.busy);
215
addView(mBusyIndicator);
216
}
217
218
setBackgroundColor(BACKGROUND_COLOR);
219
}
220
221
public void setPage(int page, PointF size) {
222
// Cancel pending render task
223
if (mDrawEntire != null) {
224
mDrawEntire.cancelAndWait();
225
mDrawEntire = null;
226
}
227
228
mIsBlank = false;
229
// Highlights may be missing because mIsBlank was true on last draw
230
if (mSearchView != null)
231
mSearchView.invalidate();
232
233
mPageNumber = page;
234
if (mEntire == null) {
235
mEntire = new OpaqueImageView(mContext);
236
mEntire.setScaleType(ImageView.ScaleType.MATRIX);
237
addView(mEntire);
238
}
239
240
// Calculate scaled size that fits within the screen limits
241
// This is the size at minimum zoom
242
mSourceScale = Math.min(mParentSize.x/size.x, mParentSize.y/size.y);
243
Point newSize = new Point((int)(size.x*mSourceScale), (int)(size.y*mSourceScale));
244
mSize = newSize;
245
246
mEntire.setImageBitmap(null);
247
mEntire.invalidate();
248
249
// Get the link info in the background
250
mGetLinkInfo = new AsyncTask<Void,Void,LinkInfo[]>() {
251
protected LinkInfo[] doInBackground(Void... v) {
252
return getLinkInfo();
253
}
254
255
protected void onPostExecute(LinkInfo[] v) {
256
mLinks = v;
257
if (mSearchView != null)
258
mSearchView.invalidate();
259
}
260
};
261
262
mGetLinkInfo.execute();
263
264
// Render the page in the background
265
mDrawEntire = new CancellableAsyncTask<Void, Void>(getDrawPageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
266
267
@Override
268
public void onPreExecute() {
269
setBackgroundColor(BACKGROUND_COLOR);
270
mEntire.setImageBitmap(null);
271
mEntire.invalidate();
272
273
if (mBusyIndicator == null) {
274
mBusyIndicator = new ProgressBar(mContext);
275
mBusyIndicator.setIndeterminate(true);
276
mBusyIndicator.setBackgroundResource(R.drawable.busy);
277
addView(mBusyIndicator);
278
mBusyIndicator.setVisibility(INVISIBLE);
279
mHandler.postDelayed(new Runnable() {
280
public void run() {
281
if (mBusyIndicator != null)
282
mBusyIndicator.setVisibility(VISIBLE);
283
}
284
}, PROGRESS_DIALOG_DELAY);
285
}
286
}
287
288
@Override
289
public void onPostExecute(Void result) {
290
removeView(mBusyIndicator);
291
mBusyIndicator = null;
292
mEntire.setImageBitmap(mEntireBm);
293
mEntire.invalidate();
294
setBackgroundColor(Color.TRANSPARENT);
295
296
}
297
};
298
299
mDrawEntire.execute();
300
301
if (mSearchView == null) {
302
mSearchView = new View(mContext) {
303
@Override
304
protected void onDraw(final Canvas canvas) {
305
super.onDraw(canvas);
306
// Work out current total scale factor
307
// from source to view
308
final float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
309
final Paint paint = new Paint();
310
311
if (!mIsBlank && mSearchBoxes != null) {
312
paint.setColor(HIGHLIGHT_COLOR);
313
for (RectF rect : mSearchBoxes)
314
canvas.drawRect(rect.left*scale, rect.top*scale,
315
rect.right*scale, rect.bottom*scale,
316
paint);
317
}
318
319
if (!mIsBlank && mLinks != null && mHighlightLinks) {
320
paint.setColor(LINK_COLOR);
321
for (LinkInfo link : mLinks)
322
canvas.drawRect(link.rect.left*scale, link.rect.top*scale,
323
link.rect.right*scale, link.rect.bottom*scale,
324
paint);
325
}
326
327
if (mSelectBox != null && mText != null) {
328
paint.setColor(HIGHLIGHT_COLOR);
329
processSelectedText(new TextProcessor() {
330
RectF rect;
331
332
public void onStartLine() {
333
rect = new RectF();
334
}
335
336
public void onWord(TextWord word) {
337
rect.union(word);
338
}
339
340
public void onEndLine() {
341
if (!rect.isEmpty())
342
canvas.drawRect(rect.left*scale, rect.top*scale, rect.right*scale, rect.bottom*scale, paint);
343
}
344
});
345
}
346
347
if (mItemSelectBox != null) {
348
paint.setStyle(Paint.Style.STROKE);
349
paint.setColor(BOX_COLOR);
350
canvas.drawRect(mItemSelectBox.left*scale, mItemSelectBox.top*scale, mItemSelectBox.right*scale, mItemSelectBox.bottom*scale, paint);
351
}
352
353
if (mDrawing != null) {
354
Path path = new Path();
355
PointF p;
356
357
paint.setAntiAlias(true);
358
paint.setDither(true);
359
paint.setStrokeJoin(Paint.Join.ROUND);
360
paint.setStrokeCap(Paint.Cap.ROUND);
361
362
paint.setStyle(Paint.Style.FILL);
363
paint.setStrokeWidth(INK_THICKNESS * scale);
364
paint.setColor(INK_COLOR);
365
366
Iterator<ArrayList<PointF>> it = mDrawing.iterator();
367
while (it.hasNext()) {
368
ArrayList<PointF> arc = it.next();
369
if (arc.size() >= 2) {
370
Iterator<PointF> iit = arc.iterator();
371
p = iit.next();
372
float mX = p.x * scale;
373
float mY = p.y * scale;
374
path.moveTo(mX, mY);
375
while (iit.hasNext()) {
376
p = iit.next();
377
float x = p.x * scale;
378
float y = p.y * scale;
379
path.quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
380
mX = x;
381
mY = y;
382
}
383
path.lineTo(mX, mY);
384
} else {
385
p = arc.get(0);
386
canvas.drawCircle(p.x * scale, p.y * scale, INK_THICKNESS * scale / 2, paint);
387
}
388
}
389
390
paint.setStyle(Paint.Style.STROKE);
391
canvas.drawPath(path, paint);
392
}
393
}
394
};
395
396
addView(mSearchView);
397
}
398
requestLayout();
399
}
400
401
public void setSearchBoxes(RectF searchBoxes[]) {
402
mSearchBoxes = searchBoxes;
403
if (mSearchView != null)
404
mSearchView.invalidate();
405
}
406
407
public void setLinkHighlighting(boolean f) {
408
mHighlightLinks = f;
409
if (mSearchView != null)
410
mSearchView.invalidate();
411
}
412
413
public void deselectText() {
414
mSelectBox = null;
415
mSearchView.invalidate();
416
}
417
418
public void selectText(float x0, float y0, float x1, float y1) {
419
float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
420
float docRelX0 = (x0 - getLeft())/scale;
421
float docRelY0 = (y0 - getTop())/scale;
422
float docRelX1 = (x1 - getLeft())/scale;
423
float docRelY1 = (y1 - getTop())/scale;
424
// Order on Y but maintain the point grouping
425
if (docRelY0 <= docRelY1)
426
mSelectBox = new RectF(docRelX0, docRelY0, docRelX1, docRelY1);
427
else
428
mSelectBox = new RectF(docRelX1, docRelY1, docRelX0, docRelY0);
429
430
mSearchView.invalidate();
431
432
if (mGetText == null) {
433
mGetText = new AsyncTask<Void,Void,TextWord[][]>() {
434
@Override
435
protected TextWord[][] doInBackground(Void... params) {
436
return getText();
437
}
438
@Override
439
protected void onPostExecute(TextWord[][] result) {
440
mText = result;
441
mSearchView.invalidate();
442
}
443
};
444
445
mGetText.execute();
446
}
447
}
448
449
public void startDraw(float x, float y) {
450
float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
451
float docRelX = (x - getLeft())/scale;
452
float docRelY = (y - getTop())/scale;
453
if (mDrawing == null)
454
mDrawing = new ArrayList<ArrayList<PointF>>();
455
456
ArrayList<PointF> arc = new ArrayList<PointF>();
457
arc.add(new PointF(docRelX, docRelY));
458
mDrawing.add(arc);
459
mSearchView.invalidate();
460
}
461
462
public void continueDraw(float x, float y) {
463
float scale = mSourceScale*(float)getWidth()/(float)mSize.x;
464
float docRelX = (x - getLeft())/scale;
465
float docRelY = (y - getTop())/scale;
466
467
if (mDrawing != null && mDrawing.size() > 0) {
468
ArrayList<PointF> arc = mDrawing.get(mDrawing.size() - 1);
469
arc.add(new PointF(docRelX, docRelY));
470
mSearchView.invalidate();
471
}
472
}
473
474
public void cancelDraw() {
475
mDrawing = null;
476
mSearchView.invalidate();
477
}
478
479
protected PointF[][] getDraw() {
480
if (mDrawing == null)
481
return null;
482
483
PointF[][] path = new PointF[mDrawing.size()][];
484
485
for (int i = 0; i < mDrawing.size(); i++) {
486
ArrayList<PointF> arc = mDrawing.get(i);
487
path[i] = arc.toArray(new PointF[arc.size()]);
488
}
489
490
return path;
491
}
492
493
protected void processSelectedText(TextProcessor tp) {
494
(new TextSelector(mText, mSelectBox)).select(tp);
495
}
496
497
public void setItemSelectBox(RectF rect) {
498
mItemSelectBox = rect;
499
if (mSearchView != null)
500
mSearchView.invalidate();
501
}
502
503
@Override
504
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
505
int x, y;
506
switch(View.MeasureSpec.getMode(widthMeasureSpec)) {
507
case View.MeasureSpec.UNSPECIFIED:
508
x = mSize.x;
509
break;
510
default:
511
x = View.MeasureSpec.getSize(widthMeasureSpec);
512
}
513
switch(View.MeasureSpec.getMode(heightMeasureSpec)) {
514
case View.MeasureSpec.UNSPECIFIED:
515
y = mSize.y;
516
break;
517
default:
518
y = View.MeasureSpec.getSize(heightMeasureSpec);
519
}
520
521
setMeasuredDimension(x, y);
522
523
if (mBusyIndicator != null) {
524
int limit = Math.min(mParentSize.x, mParentSize.y)/2;
525
mBusyIndicator.measure(View.MeasureSpec.AT_MOST | limit, View.MeasureSpec.AT_MOST | limit);
526
}
527
}
528
529
@Override
530
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
531
int w = right-left;
532
int h = bottom-top;
533
534
if (mEntire != null) {
535
if (mEntire.getWidth() != w || mEntire.getHeight() != h) {
536
mEntireMat.setScale(w/(float)mSize.x, h/(float)mSize.y);
537
mEntire.setImageMatrix(mEntireMat);
538
mEntire.invalidate();
539
}
540
mEntire.layout(0, 0, w, h);
541
}
542
543
if (mSearchView != null) {
544
mSearchView.layout(0, 0, w, h);
545
}
546
547
if (mPatchViewSize != null) {
548
if (mPatchViewSize.x != w || mPatchViewSize.y != h) {
549
// Zoomed since patch was created
550
mPatchViewSize = null;
551
mPatchArea = null;
552
if (mPatch != null) {
553
mPatch.setImageBitmap(null);
554
mPatch.invalidate();
555
}
556
} else {
557
mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
558
}
559
}
560
561
if (mBusyIndicator != null) {
562
int bw = mBusyIndicator.getMeasuredWidth();
563
int bh = mBusyIndicator.getMeasuredHeight();
564
565
mBusyIndicator.layout((w-bw)/2, (h-bh)/2, (w+bw)/2, (h+bh)/2);
566
}
567
}
568
569
public void updateHq(boolean update) {
570
Rect viewArea = new Rect(getLeft(),getTop(),getRight(),getBottom());
571
if (viewArea.width() == mSize.x || viewArea.height() == mSize.y) {
572
// If the viewArea's size matches the unzoomed size, there is no need for an hq patch
573
if (mPatch != null) {
574
mPatch.setImageBitmap(null);
575
mPatch.invalidate();
576
}
577
} else {
578
final Point patchViewSize = new Point(viewArea.width(), viewArea.height());
579
final Rect patchArea = new Rect(0, 0, mParentSize.x, mParentSize.y);
580
581
// Intersect and test that there is an intersection
582
if (!patchArea.intersect(viewArea))
583
return;
584
585
// Offset patch area to be relative to the view top left
586
patchArea.offset(-viewArea.left, -viewArea.top);
587
588
boolean area_unchanged = patchArea.equals(mPatchArea) && patchViewSize.equals(mPatchViewSize);
589
590
// If being asked for the same area as last time and not because of an update then nothing to do
591
if (area_unchanged && !update)
592
return;
593
594
boolean completeRedraw = !(area_unchanged && update);
595
596
// Stop the drawing of previous patch if still going
597
if (mDrawPatch != null) {
598
mDrawPatch.cancelAndWait();
599
mDrawPatch = null;
600
}
601
602
// Create and add the image view if not already done
603
if (mPatch == null) {
604
mPatch = new OpaqueImageView(mContext);
605
mPatch.setScaleType(ImageView.ScaleType.MATRIX);
606
addView(mPatch);
607
mSearchView.bringToFront();
608
}
609
610
CancellableTaskDefinition<Void, Void> task;
611
612
if (completeRedraw)
613
task = getDrawPageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
614
patchArea.left, patchArea.top,
615
patchArea.width(), patchArea.height());
616
else
617
task = getUpdatePageTask(mPatchBm, patchViewSize.x, patchViewSize.y,
618
patchArea.left, patchArea.top,
619
patchArea.width(), patchArea.height());
620
621
mDrawPatch = new CancellableAsyncTask<Void,Void>(task) {
622
623
public void onPostExecute(Void result) {
624
mPatchViewSize = patchViewSize;
625
mPatchArea = patchArea;
626
mPatch.setImageBitmap(mPatchBm);
627
mPatch.invalidate();
628
//requestLayout();
629
// Calling requestLayout here doesn't lead to a later call to layout. No idea
630
// why, but apparently others have run into the problem.
631
mPatch.layout(mPatchArea.left, mPatchArea.top, mPatchArea.right, mPatchArea.bottom);
632
}
633
};
634
635
mDrawPatch.execute();
636
}
637
}
638
639
public void update() {
640
// Cancel pending render task
641
if (mDrawEntire != null) {
642
mDrawEntire.cancelAndWait();
643
mDrawEntire = null;
644
}
645
646
if (mDrawPatch != null) {
647
mDrawPatch.cancelAndWait();
648
mDrawPatch = null;
649
}
650
651
652
// Render the page in the background
653
mDrawEntire = new CancellableAsyncTask<Void, Void>(getUpdatePageTask(mEntireBm, mSize.x, mSize.y, 0, 0, mSize.x, mSize.y)) {
654
655
public void onPostExecute(Void result) {
656
mEntire.setImageBitmap(mEntireBm);
657
mEntire.invalidate();
658
}
659
};
660
661
mDrawEntire.execute();
662
663
updateHq(true);
664
}
665
666
public void removeHq() {
667
// Stop the drawing of the patch if still going
668
if (mDrawPatch != null) {
669
mDrawPatch.cancelAndWait();
670
mDrawPatch = null;
671
}
672
673
// And get rid of it
674
mPatchViewSize = null;
675
mPatchArea = null;
676
if (mPatch != null) {
677
mPatch.setImageBitmap(null);
678
mPatch.invalidate();
679
}
680
}
681
682
public int getPage() {
683
return mPageNumber;
684
}
685
686
@Override
687
public boolean isOpaque() {
688
return true;
689
}
690
}
691
692