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