1 package org.coolreader.crengine;
2 
3 import java.util.ArrayList;
4 import java.util.Collection;
5 import java.util.Collections;
6 
7 import org.coolreader.db.CRDBService;
8 
9 import android.graphics.Bitmap;
10 import android.graphics.Canvas;
11 import android.graphics.ColorFilter;
12 import android.graphics.Paint;
13 import android.graphics.PixelFormat;
14 import android.graphics.Rect;
15 import android.graphics.drawable.Drawable;
16 import android.util.Log;
17 import android.view.View;
18 import android.view.ViewGroup;
19 import android.widget.ImageView;
20 
21 public class CoverpageManager {
22 
23 	public static final Logger log = L.create("cp");
24 
25 	public static class ImageItem {
26 		public FileInfo file;
27 		public int maxWidth;
28 		public int maxHeight;
ImageItem(FileInfo file, int maxWidth, int maxHeight)29 		public ImageItem(FileInfo file, int maxWidth, int maxHeight) {
30 			this.file = file;
31 			this.maxWidth = maxWidth;
32 			this.maxHeight = maxHeight;
33 		}
fileMatches(ImageItem item)34 		public boolean fileMatches(ImageItem item) {
35 			return file.pathNameEquals(item.file);
36 		}
sizeMatches(ImageItem item)37 		public boolean sizeMatches(ImageItem item) {
38 			return (maxWidth == item.maxWidth && maxHeight == item.maxHeight)
39 					|| (item.maxHeight <= -1 && item.maxWidth <= -1)
40 					|| (maxHeight <= -1 && maxWidth <= -1);
41 		}
matches(ImageItem item)42 		public boolean matches(ImageItem item) {
43 			return fileMatches(item) && sizeMatches(item);
44 		}
45 		@Override
toString()46 		public String toString() {
47 			return "[" + file + " " + maxWidth
48 					+ "x" + maxHeight + "]";
49 		}
50 
51 	}
52 
53 	/**
54 	 * Callback on coverpage decoding finish.
55 	 */
56 	public interface CoverpageReadyListener {
onCoverpagesReady(ArrayList<ImageItem> file)57 		void onCoverpagesReady(ArrayList<ImageItem> file);
58 	}
59 
60 	public interface CoverpageBitmapReadyListener {
onCoverpageReady(ImageItem file, Bitmap bitmap)61 		void onCoverpageReady(ImageItem file, Bitmap bitmap);
62 	}
63 
64 	/**
65 	 * Cancel queued tasks for specified files.
66 	 */
unqueue(Collection<ImageItem> filesToUnqueue)67 	public void unqueue(Collection<ImageItem> filesToUnqueue) {
68 		synchronized(LOCK) {
69 			for (ImageItem file : filesToUnqueue) {
70 				mCheckFileCacheQueue.remove(file);
71 				mScanFileQueue.remove(file);
72 				mReadyQueue.remove(file);
73 				mCache.unqueue(file);
74 			}
75 		}
76 	}
77 
78 	/**
79 	 * Set listener for cover page load completion.
80 	 */
addCoverpageReadyListener(CoverpageReadyListener listener)81 	public void addCoverpageReadyListener(CoverpageReadyListener listener) {
82 		this.listeners.add(listener);
83 	}
84 
85 	/**
86 	 * Set listener for cover page load completion.
87 	 */
removeCoverpageReadyListener(CoverpageReadyListener listener)88 	public void removeCoverpageReadyListener(CoverpageReadyListener listener) {
89 		this.listeners.remove(listener);
90 	}
91 
setCoverpageSize(int width, int height)92 	public boolean setCoverpageSize(int width, int height) {
93 		synchronized(LOCK) {
94 			if (maxWidth == width && maxHeight == height)
95 				return false;
96 			//clear();
97 			maxWidth = width;
98 			maxHeight = height;
99 			return true;
100 		}
101 	}
102 
setFontFace(String face)103 	public boolean setFontFace(String face) {
104 		synchronized(LOCK) {
105 			clear();
106 			if (fontFace.equals(face))
107 				return false;
108 			fontFace = face;
109 			return true;
110 		}
111 	}
112 
setCoverpageData(final CRDBService.LocalBinder db, FileInfo fileInfo, byte[] data)113 	public void setCoverpageData(final CRDBService.LocalBinder db, FileInfo fileInfo, byte[] data) {
114 		synchronized(LOCK) {
115 			ImageItem item = new ImageItem(fileInfo, -1, -1);
116 			unqueue(Collections.singleton(item));
117 			mCache.remove(item);
118 			db.saveBookCoverpage(item.file, data);
119 			coverpageLoaded(item, data);
120 		}
121 	}
122 
clear()123 	public void clear() {
124 		log.d("CoverpageManager.clear()");
125 		synchronized(LOCK) {
126 			mCache.clear();
127 			mCheckFileCacheQueue.clear();
128 			mScanFileQueue.clear();
129 			mReadyQueue.clear();
130 		}
131 	}
132 
133 	/**
134 	 * Constructor.
135 	 */
CoverpageManager()136 	public CoverpageManager () {
137 	}
138 
139 	/**
140 	 * Returns coverpage drawable for book.
141 	 * Internally it will load coverpage in background.
142 	 * @param book is file to get coverpage for.
143 	 * @return Drawable which can be used to draw coverpage.
144 	 */
getCoverpageDrawableFor(final CRDBService.LocalBinder db, FileInfo book)145 	public Drawable getCoverpageDrawableFor(final CRDBService.LocalBinder db, FileInfo book) {
146 		return new CoverImage(db, new ImageItem(new FileInfo(book), maxWidth, maxHeight));
147 	}
148 
149 	/**
150 	 * Returns coverpage drawable for book.
151 	 * Internally it will load coverpage in background.
152 	 * @param book is file to get coverpage for.
153 	 * @param maxWidth is width in pixel of destination image size.
154 	 * @param maxHeight is height in pixel of destination image size.
155 	 * @return Drawable which can be used to draw coverpage.
156 	 */
getCoverpageDrawableFor(final CRDBService.LocalBinder db, FileInfo book, int maxWidth, int maxHeight)157 	public Drawable getCoverpageDrawableFor(final CRDBService.LocalBinder db, FileInfo book, int maxWidth, int maxHeight) {
158 		return new CoverImage(db, new ImageItem(new FileInfo(book), maxWidth, maxHeight));
159 	}
160 
161 	private int maxWidth = 110;
162 	private int maxHeight = 140;
163 	private String fontFace = "Droid Sans";
164 
165 	private enum State {
166 		UNINITIALIZED,
167 		LOAD_SCHEDULED,
168 		FILE_CACHE_LOOKUP,
169 		IMAGE_DRAW_SCHEDULED,
170 		DRAWING,
171 		READY,
172 	}
173 
174 	// hack for heap size limit
175 	private static final VMRuntimeHack runtime = new VMRuntimeHack();
176 
177 	private class BitmapCacheItem {
178 		private final ImageItem file;
179 		private Bitmap bitmap;
180 		private State state = State.UNINITIALIZED;
BitmapCacheItem(ImageItem file)181 		public BitmapCacheItem(ImageItem file) {
182 			this.file = file;
183 		}
canUnqueue()184 		private boolean canUnqueue() {
185 			switch (state) {
186 			case FILE_CACHE_LOOKUP:
187 			case LOAD_SCHEDULED:
188 			case UNINITIALIZED:
189 				return true;
190 			default:
191 				return false;
192 			}
193 		}
setBitmap(Bitmap bmp)194 		private void setBitmap(Bitmap bmp) {
195 			if (bitmap != null)
196 				removed();
197 			bitmap = bmp;
198 			if (bitmap != null) {
199 				int bytes = bitmap.getRowBytes() * bitmap.getHeight();
200 				runtime.trackFree(bytes); // hack for heap size limit
201 			}
202 		}
removed()203 		private void removed() {
204 			if (bitmap != null) {
205 				int bytes = bitmap.getRowBytes() * bitmap.getHeight();
206 				runtime.trackAlloc(bytes); // hack for heap size limit
207 				bitmap.recycle();
208 				bitmap = null;
209 			}
210 		}
211 		@Override
finalize()212 		protected void finalize() throws Throwable {
213 			// don't forget to free resource
214 			removed();
215 			super.finalize();
216 		}
217 
218 	}
219 
220 	private class BitmapCache {
BitmapCache(int maxSize)221 		public BitmapCache(int maxSize) {
222 			this.maxSize = maxSize;
223 		}
224 		private ArrayList<BitmapCacheItem> list = new ArrayList<BitmapCacheItem>();
225 		private int maxSize;
find(ImageItem file)226 		private int find(ImageItem file) {
227 			for (int i = 0; i < list.size(); i++) {
228 				BitmapCacheItem item = list.get(i);
229 				if (item.file.matches(file))
230 					return i;
231 			}
232 			return -1;
233 		}
moveOnTop(int index)234 		private void moveOnTop(int index) {
235 			if (index >= list.size() - 1)
236 				return;
237 			BitmapCacheItem item = list.get(index);
238 			list.remove(index);
239 			list.add(item);
240 		}
checkMaxSize()241 		private void checkMaxSize() {
242 			int itemsToRemove = list.size() - maxSize;
243 			for (int i = itemsToRemove - 1; i >= 0; i--) {
244 				BitmapCacheItem item = list.get(i);
245 				list.remove(i);
246 				item.removed();
247 			}
248 		}
clear()249 		public void clear() {
250 			for (BitmapCacheItem item : list) {
251 				if (item.bitmap != null)
252 					item.removed();
253 			}
254 			list.clear();
255 		}
getItem(ImageItem file)256 		public BitmapCacheItem getItem(ImageItem file) {
257 			int index = find(file);
258 			if (index < 0)
259 				return null;
260 			BitmapCacheItem item = list.get(index);
261 			moveOnTop(index);
262 			return item;
263 		}
addItem(ImageItem file)264 		public BitmapCacheItem addItem(ImageItem file) {
265 			BitmapCacheItem item = new BitmapCacheItem(file);
266 			list.add(item);
267 			checkMaxSize();
268 			return item;
269 		}
unqueue(ImageItem file)270 		public void unqueue(ImageItem file) {
271 			int index = find(file);
272 			if (index < 0)
273 				return;
274 			BitmapCacheItem item = list.get(index);
275 			if (item.canUnqueue()) {
276 				list.remove(index);
277 				item.removed();
278 			}
279 		}
remove(ImageItem file)280 		public void remove(ImageItem file) {
281 			int index = find(file);
282 			if (index < 0)
283 				return;
284 			BitmapCacheItem item = list.get(index);
285 			list.remove(index);
286 			item.removed();
287 		}
getBitmap(ImageItem file)288 		public Bitmap getBitmap(ImageItem file) {
289 			synchronized (LOCK) {
290 				BitmapCacheItem item = getItem(file);
291 				if (item == null || item.bitmap == null || item.bitmap.isRecycled())
292 					return null;
293 				return item.bitmap;
294 			}
295 		}
296 	}
297 	private BitmapCache mCache = new BitmapCache(32);
298 
299 	private FileInfoQueue mCheckFileCacheQueue = new FileInfoQueue();
300 	private FileInfoQueue mScanFileQueue = new FileInfoQueue();
301 	private FileInfoQueue mReadyQueue = new FileInfoQueue();
302 
303 	private static class FileInfoQueue {
304 		ArrayList<ImageItem> list = new ArrayList<>();
indexOf(ImageItem file)305 		public int indexOf(ImageItem file) {
306 			for (int i = list.size() - 1; i >= 0; i--) {
307 				if (file.matches(list.get(i))) {
308 					return i;
309 				}
310 			}
311 			return -1;
312 		}
remove(ImageItem file)313 		public void remove(ImageItem file) {
314 			int index = indexOf(file);
315 			if (index >= 0)
316 				list.remove(index);
317 		}
moveOnTop(ImageItem file)318 		public void moveOnTop(ImageItem file) {
319 			int index = indexOf(file);
320 			if (index == 0)
321 				return;
322 			moveOnTop(index);
323 		}
moveOnTop(int index)324 		public void moveOnTop(int index) {
325 			ImageItem item = list.get(index);
326 			list.remove(index);
327 			list.add(0, item);
328 		}
empty()329 		public boolean empty() {
330 			return list.size() == 0;
331 		}
add(ImageItem file)332 		public void add(ImageItem file) {
333 			int index = indexOf(file);
334 			if (index >= 0)
335 				return;
336 			list.add(file);
337 		}
clear()338 		public void clear() {
339 			list.clear();
340 		}
addOnTop(ImageItem file)341 		public boolean addOnTop(ImageItem file) {
342 			int index = indexOf(file);
343 			if (index >= 0) {
344 				if (index > 0)
345 					moveOnTop(index);
346 				return false;
347 			}
348 			list.add(0, file);
349 			return true;
350 		}
next()351 		public ImageItem next() {
352 			if (list.size() == 0)
353 				return null;
354 			ImageItem item = list.get(0);
355 			list.remove(0);
356 			return item;
357 		}
358 	}
359 
360 	private Object LOCK = new Object();
361 
362 	private Runnable lastCheckCacheTask = null;
363 	private Runnable lastScanFileTask = null;
setItemState(ImageItem file, State state)364 	private BitmapCacheItem setItemState(ImageItem file, State state) {
365 		synchronized(LOCK) {
366 			BitmapCacheItem item = mCache.getItem(file);
367 			if (item == null)
368 				item = mCache.addItem(file);
369 			item.state = state;
370 			return item;
371 		}
372 	}
373 
374 	private final static int COVERPAGE_UPDATE_DELAY = DeviceInfo.EINK_SCREEN ? 1000 : 100;
375 	private final static int COVERPAGE_MAX_UPDATE_DELAY = DeviceInfo.EINK_SCREEN ? 3000 : 300;
376 	private Runnable lastReadyNotifyTask;
377 	private long firstReadyTimestamp;
notifyBitmapIsReady(final ImageItem file)378 	private void notifyBitmapIsReady(final ImageItem file) {
379 		synchronized(LOCK) {
380 			if (mReadyQueue.empty())
381 				firstReadyTimestamp = Utils.timeStamp();
382 			mReadyQueue.add(file);
383 		}
384 		Runnable task = () -> {
385 //				if (lastReadyNotifyTask != this && Utils.timeInterval(firstReadyTimestamp) < COVERPAGE_MAX_UPDATE_DELAY) {
386 //					log.v("skipping update, " + Utils.timeInterval(firstReadyTimestamp));
387 //					return;
388 //				}
389 			ArrayList<ImageItem> list = new ArrayList<>();
390 			synchronized(LOCK) {
391 				for (;;) {
392 					ImageItem f = mReadyQueue.next();
393 					if (f == null)
394 						break;
395 					list.add(f);
396 				}
397 				mReadyQueue.clear();
398 				if (list.size() > 0)
399 					log.v("ready coverpages: " + list.size());
400 			}
401 			if (list.size() > 0) {
402 				for (CoverpageReadyListener listener : listeners)
403 					listener.onCoverpagesReady(list);
404 				firstReadyTimestamp = Utils.timeStamp();
405 			}
406 		};
407 		lastReadyNotifyTask = task;
408 		BackgroundThread.instance().postGUI(task, COVERPAGE_UPDATE_DELAY);
409 	}
410 
draw(ImageItem file, byte[] data)411 	private void draw(ImageItem file, byte[] data) {
412 		BitmapCacheItem item;
413 		synchronized(LOCK) {
414 			item = mCache.getItem(file);
415 			if (item == null)
416 				return;
417 			if (item.state == State.DRAWING || item.state == State.READY)
418 				return;
419 			item.state = State.DRAWING;
420 		}
421 		Bitmap bmp = drawCoverpage(data, file);
422 		if (bmp != null) {
423 			// successfully decoded
424 			log.v("coverpage is decoded for " + file);
425 			item.setBitmap(bmp);
426 			item.state = State.READY;
427 			notifyBitmapIsReady(file);
428 		}
429 	}
430 
coverpageLoaded(final ImageItem file, final byte[] data)431 	private void coverpageLoaded(final ImageItem file, final byte[] data) {
432 		log.v("coverpage data is loaded for " + file);
433 		setItemState(file, State.IMAGE_DRAW_SCHEDULED);
434 		BackgroundThread.instance().postBackground(() -> draw(file, data));
435 	}
scheduleCheckCache(final CRDBService.LocalBinder db)436 	private void scheduleCheckCache(final CRDBService.LocalBinder db) {
437 		// cache lookup
438 		lastCheckCacheTask = new Runnable() {
439 			@Override
440 			public void run() {
441 				ImageItem file = null;
442 				synchronized(LOCK) {
443 					if (lastCheckCacheTask == this) {
444 						file = mCheckFileCacheQueue.next();
445 					}
446 				}
447 				if (file != null) {
448 					final ImageItem request = file;
449 					db.loadBookCoverpage(file.file, (fileInfo, data) -> {
450 						if (data == null) {
451 							log.v("cover not found in DB for " + fileInfo + ", scheduling scan");
452 							mScanFileQueue.addOnTop(request);
453 							scheduleScanFile(db);
454 						} else {
455 							coverpageLoaded(request, data);
456 						}
457 					});
458 					scheduleCheckCache(db);
459 				}
460 			}
461 		};
462 		BackgroundThread.instance().postGUI(lastCheckCacheTask);
463 	}
scheduleScanFile(final CRDBService.LocalBinder db)464 	private void scheduleScanFile(final CRDBService.LocalBinder db) {
465 		// file scan
466 		lastScanFileTask = new Runnable() {
467 			@Override
468 			public void run() {
469 				ImageItem file = null;
470 				synchronized(LOCK) {
471 					if (lastScanFileTask == this) {
472 						file = mScanFileQueue.next();
473 					}
474 				}
475 				if (file != null) {
476 					final ImageItem fileInfo = file;
477 					if (fileInfo.file.format.canParseCoverpages) {
478 						BackgroundThread.instance().postBackground(() -> {
479 							byte[] data = Services.getEngine().scanBookCover(fileInfo.file.getPathName());
480 							if (data == null)
481 								data = new byte[] {};
482 							if (fileInfo.file.format.needCoverPageCaching())
483 								db.saveBookCoverpage(fileInfo.file, data);
484 							coverpageLoaded(fileInfo, data);
485 						});
486 					} else {
487 						coverpageLoaded(fileInfo, new byte[] {});
488 					}
489 					scheduleScanFile(db);
490 				}
491 			}
492 		};
493 		BackgroundThread.instance().postGUI(lastScanFileTask);
494 	}
495 
queueForDrawing(final CRDBService.LocalBinder db, ImageItem file)496 	private void queueForDrawing(final CRDBService.LocalBinder db, ImageItem file) {
497 		synchronized (LOCK) {
498 			if (file == null || file.file == null || file.file.format == null)
499 				return;
500 			BitmapCacheItem item = mCache.getItem(file);
501 			if (item != null && (item.state == State.READY || item.state == State.DRAWING))
502 				return;
503 			if (file.file.format.needCoverPageCaching()) {
504 				if (mCheckFileCacheQueue.addOnTop(file)) {
505 					log.v("Scheduled coverpage DB lookup for " + file);
506 					scheduleCheckCache(db);
507 				}
508 			} else {
509 				if (mScanFileQueue.addOnTop(file)) {
510 					log.v("Scheduled coverpage filescan for " + file);
511 					scheduleScanFile(db);
512 				}
513 			}
514 		}
515 	}
516 
517 	public static abstract class CoverImageBase extends Drawable {
518 		protected ImageItem book;
CoverImageBase(ImageItem book)519 		public CoverImageBase(ImageItem book) {
520 			this.book = book;
521 		}
522 	}
523 	private class CoverImage extends CoverImageBase {
524 
525 		Paint defPaint;
526 		final CRDBService.LocalBinder db;
527 		final static int alphaLevels = 16;
528 		final static int shadowSizePercent = 6;
529 		final static int minAlpha = 40;
530 		final static int maxAlpha = 180;
531 		final Paint[] shadowPaints = new Paint[alphaLevels + 1];
532 
CoverImage(final CRDBService.LocalBinder db, ImageItem book)533 		public CoverImage(final CRDBService.LocalBinder db, ImageItem book) {
534 			super(book);
535 			this.db = db;
536 			defPaint = new Paint();
537 			defPaint.setColor(0xFF000000);
538 			defPaint.setFilterBitmap(true);
539 			for (int i=0; i <= alphaLevels; i++) {
540 				int alpha = (maxAlpha - minAlpha) * i / alphaLevels + minAlpha;
541 				shadowPaints[i] = new Paint();
542 				shadowPaints[i].setColor((alpha << 24) | 0x101010);
543 			}
544 		}
545 
drawShadow(Canvas canvas, Rect bookRect, Rect shadowRect)546 		public void drawShadow(Canvas canvas, Rect bookRect, Rect shadowRect) {
547 			int d = shadowRect.bottom - bookRect.bottom;
548 			if (d <= 0)
549 				return;
550 			Rect l = new Rect(shadowRect);
551 			Rect r = new Rect(shadowRect);
552 			Rect t = new Rect(shadowRect);
553 			Rect b = new Rect(shadowRect);
554 			for (int i = 0; i < d; i++) {
555 				shadowRect.left++;
556 				shadowRect.right--;
557 				shadowRect.top++;
558 				shadowRect.bottom--;
559 				if (shadowRect.bottom < bookRect.bottom || shadowRect.right < bookRect.right)
560 					break;
561 				l.set(shadowRect);
562 				l.top = bookRect.bottom;
563 				l.right = l.left + 1;
564 				t.set(shadowRect);
565 				t.left = bookRect.right;
566 				t.right--;
567 				t.bottom = t.top + 1;
568 				r.set(shadowRect);
569 				r.left = r.right - 1;
570 				b.set(shadowRect);
571 				b.top = b.bottom - 1;
572 				b.left++;
573 				b.right--;
574 				int index = i * alphaLevels / d;
575 				Paint paint = shadowPaints[index];
576 				if (!l.isEmpty())
577 					canvas.drawRect(l, paint);
578 				if (!r.isEmpty())
579 					canvas.drawRect(r, paint);
580 				if (!t.isEmpty())
581 					canvas.drawRect(t, paint);
582 				if (!b.isEmpty())
583 					canvas.drawRect(b, paint);
584 			}
585 		}
checkShadowSize(int bookSize, int shadowSize)586 		boolean checkShadowSize(int bookSize, int shadowSize) {
587 			if (bookSize < 10)
588 				return false;
589 			int p = 100 * shadowSize / bookSize;
590 			if (p >= 0 && p >= shadowSizePercent - 2 && p <= shadowSizePercent + 2)
591 				return true;
592 			return false;
593 		}
594 		@Override
draw(Canvas canvas)595 		public void draw(Canvas canvas) {
596 			try {
597 				Rect fullrc = getBounds();
598 				if (fullrc.width() < 5 || fullrc.height() < 5)
599 					return;
600 				int w = book.maxWidth;
601 				int h = book.maxHeight;
602 				int shadowW = fullrc.width() - w;
603 				int shadowH = fullrc.height() - h;
604 				if (!checkShadowSize(w, shadowW) || !checkShadowSize(h, shadowH)) {
605 					w = fullrc.width() * 100 / (100 + shadowSizePercent);
606 					h = fullrc.height() * 100 / (100 + shadowSizePercent);
607 					shadowW = fullrc.width() - w;
608 					shadowH = fullrc.height() - h;
609 				}
610 				Rect rc = new Rect(fullrc.left, fullrc.top, fullrc.right - shadowW, fullrc.bottom - shadowH);
611 				synchronized (mCache) {
612 					Bitmap bitmap = mCache.getBitmap(book);
613 					if (bitmap != null) {
614 						log.d("Image for " + book + " is found in cache, drawing...");
615 						Rect dst = getBestCoverSize(rc, bitmap.getWidth(), bitmap.getHeight());
616 						canvas.drawBitmap(bitmap, null, dst, defPaint);
617 						if (shadowSizePercent > 0) {
618 							Rect shadowRect = new Rect(rc.left + shadowW, rc.top + shadowH, rc.right + shadowW, rc.bottom + shadowW);
619 							drawShadow(canvas, rc, shadowRect);
620 						}
621 						return;
622 					}
623 				}
624 				log.d("Image for " + book + " is not found in cache, scheduling generation...");
625 				queueForDrawing(db, book);
626 				//if (h * bestWidth / bestHeight > w)
627 				//canvas.drawRect(rc, defPaint);
628 			} catch (Exception e) {
629 				log.e("exception in draw", e);
630 			}
631 		}
632 
633 		@Override
getIntrinsicHeight()634 		public int getIntrinsicHeight() {
635 			return book.maxHeight * (100 + shadowSizePercent) / 100;
636 		}
637 
638 		@Override
getIntrinsicWidth()639 		public int getIntrinsicWidth() {
640 			return book.maxWidth * (100 + shadowSizePercent) / 100;
641 		}
642 
643 		@Override
getOpacity()644 		public int getOpacity() {
645 			return PixelFormat.TRANSPARENT; // part of pixels are transparent
646 		}
647 
648 		@Override
setAlpha(int alpha)649 		public void setAlpha(int alpha) {
650 			// ignore, not supported
651 		}
652 
653 		@Override
setColorFilter(ColorFilter cf)654 		public void setColorFilter(ColorFilter cf) {
655 			// ignore, not supported
656 		}
657 	}
658 
drawCoverpageFor(final CRDBService.LocalBinder db, final FileInfo file, final Bitmap buffer, final CoverpageBitmapReadyListener callback)659 	public void drawCoverpageFor(final CRDBService.LocalBinder db, final FileInfo file, final Bitmap buffer, final CoverpageBitmapReadyListener callback) {
660 		db.loadBookCoverpage(file, (fileInfo, data) -> BackgroundThread.instance().postBackground(() -> {
661 			byte[] imageData = data;
662 			if (data == null && file.format != null && file.format.canParseCoverpages) {
663 				imageData = Services.getEngine().scanBookCover(file.getPathName());
664 				if (imageData == null)
665 					imageData = new byte[] {};
666 				if (file.format.needCoverPageCaching())
667 					db.saveBookCoverpage(file, imageData);
668 			}
669 			Services.getEngine().drawBookCover(buffer, imageData, fontFace, file.getTitleOrFileName(), file.authors, file.series, file.seriesNumber, DeviceInfo.EINK_SCREEN ? 4 : 16);
670 			BackgroundThread.instance().postGUI(() -> {
671 				ImageItem item = new ImageItem(file, buffer.getWidth(), buffer.getHeight());
672 				callback.onCoverpageReady(item, buffer);
673 			});
674 		}));
675 	}
676 
getBestCoverSize(Rect dst, int srcWidth, int srcHeight)677 	private Rect getBestCoverSize(Rect dst, int srcWidth, int srcHeight) {
678 		int w = dst.width();
679 		int h = dst.height();
680 		if (srcWidth < 20 || srcHeight < 20) {
681 			return dst;
682 		}
683 		int sw = srcHeight * w / h;
684 		int sh = srcWidth * h / w;
685 		if (sw <= w)
686 			sh = h;
687 		else
688 			sw = w;
689 		int dx = (w - sw) / 2;
690 		int dy = (h - sh) / 2;
691 		return new Rect(dst.left + dx, dst.top + dy, dst.left + sw + dx, dst.top + sh + dy);
692 	}
693 
drawCoverpage(byte[] data, ImageItem file)694 	private Bitmap drawCoverpage(byte[] data, ImageItem file)
695 	{
696 		try {
697 			Bitmap bmp = Bitmap.createBitmap(file.maxWidth, file.maxHeight, DeviceInfo.BUFFER_COLOR_FORMAT);
698 			Services.getEngine().drawBookCover(bmp, data, fontFace, file.file.getTitleOrFileName(), file.file.authors, file.file.series, file.file.seriesNumber, DeviceInfo.EINK_SCREEN ? 4 : 16);
699 			return bmp;
700 		} catch ( Exception e ) {
701     		Log.e("cr3", "exception while decoding coverpage " + e.getMessage());
702     		return null;
703 		}
704 	}
705 
706 	private ArrayList<CoverpageReadyListener> listeners = new ArrayList<>();
707 
708 
invalidateChildImages(View view, ArrayList<CoverpageManager.ImageItem> files)709 	public static void invalidateChildImages(View view, ArrayList<CoverpageManager.ImageItem> files) {
710 		if (view instanceof ViewGroup) {
711 			ViewGroup vg = (ViewGroup)view;
712 			for (int i=0; i<vg.getChildCount(); i++) {
713 				invalidateChildImages(vg.getChildAt(i), files);
714 			}
715 		} else if (view instanceof ImageView) {
716 			if (view.getTag() instanceof CoverpageManager.ImageItem) {
717 				CoverpageManager.ImageItem item = (CoverpageManager.ImageItem)view.getTag();
718 				for (CoverpageManager.ImageItem v : files)
719 					if (v.matches(item)) {
720 						log.v("invalidating view for " + item);
721 						view.invalidate();
722 					}
723 			}
724 		}
725 	}
726 }
727