package org.ebookdroid.core;
import org.ebookdroid.common.bitmaps.BitmapManager;
import org.ebookdroid.common.bitmaps.ByteBufferBitmap;
import org.ebookdroid.common.bitmaps.ByteBufferManager;
import org.ebookdroid.common.bitmaps.IBitmapRef;
import org.ebookdroid.common.settings.AppSettings;
import org.ebookdroid.common.settings.books.BookSettings;
import org.ebookdroid.core.codec.CodecContext;
import org.ebookdroid.core.codec.CodecDocument;
import org.ebookdroid.core.codec.CodecFeatures;
import org.ebookdroid.core.codec.CodecPage;
import org.ebookdroid.core.codec.CodecPageHolder;
import org.ebookdroid.core.codec.CodecPageInfo;
import org.ebookdroid.core.codec.OutlineLink;
import org.ebookdroid.core.crop.PageCropper;
import org.ebookdroid.ui.viewer.IViewController.InvalidateSizeReason;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.graphics.RectF;
import android.support.annotation.WorkerThread;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.ReentrantLock;
import org.emdev.common.log.LogContext;
import org.emdev.common.log.LogManager;
import org.emdev.utils.CompareUtils;
import org.emdev.utils.LengthUtils;
import org.emdev.utils.MathUtils;
public class DecodeServiceBase implements DecodeService {
public static final LogContext LCTX = LogManager.root().lctx("Decoding", false);
static final AtomicLong TASK_ID_SEQ = new AtomicLong();
final CodecContext codecContext;
final AtomicBoolean isRecycled;
final AtomicReference<ViewState> viewState;
final Map<Integer, CodecPageHolder> pages;
final Executor executor;
CodecDocument document;
public DecodeServiceBase(final CodecContext codecContext) {
this.codecContext = codecContext;
isRecycled = new AtomicBoolean();
viewState = new AtomicReference<ViewState>();
pages = new PageCache();
executor = new Executor();
executor.start();
}
@Override
public boolean isFeatureSupported(final int feature) {
return codecContext.isFeatureSupported(feature);
}
@Override
@WorkerThread
public void open(final String fileName, final String password) {
document = codecContext.openDocument(fileName, password);
}
@Override
public CodecPageInfo getUnifiedPageInfo() {
return document != null ? document.getUnifiedPageInfo() : null;
}
@Override
public CodecPageInfo getPageInfo(final int pageIndex) {
return document != null ? document.getPageInfo(pageIndex) : null;
}
@Override
public void updateViewState(final ViewState viewState) {
this.viewState.set(viewState);
}
@Override
public void searchText(final Page page, final String pattern, final SearchCallback callback) {
if (isRecycled.get()) {
if (LCTX.isDebugEnabled()) {
LCTX.d("Searching not allowed on recycling");
}
return;
}
final SearchTask decodeTask = new SearchTask(page, pattern, callback);
executor.add(decodeTask);
}
@Override
public void stopSearch(final String pattern) {
executor.stopSearch(pattern);
}
@Override
public void decodePage(final ViewState viewState, final PageTreeNode node) {
if (isRecycled.get()) {
if (LCTX.isDebugEnabled()) {
LCTX.d("Decoding not allowed on recycling");
}
return;
}
final DecodeTask decodeTask = new DecodeTask(viewState, node);
updateViewState(viewState);
executor.add(decodeTask);
}
@Override
public void stopDecoding(final PageTreeNode node, final String reason) {
executor.stopDecoding(null, node, reason);
}
void performDecode(final DecodeTask task) {
if (executor.isTaskDead(task)) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": Skipping dead decode task for "
+ task.node);
}
return;
}
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": Starting decoding for " + task.node);
}
CodecPageHolder holder = null;
CodecPage vuPage = null;
Rect r = null;
RectF croppedPageBounds = null;
try {
holder = getPageHolder(task.id, task.pageNumber);
vuPage = holder.getPage(task.id);
if (executor.isTaskDead(task)) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": Abort dead decode task for "
+ task.node);
}
return;
}
// Checks if cropping setting is set and node crop region is not set
if (codecContext.isFeatureSupported(CodecFeatures.FEATURE_CROP_SUPPORT) && task.node.page.shouldCrop() && task.node.getCropping() == null) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id
+ ": no cropping bounds for task node");
}
// Calculate node cropping
croppedPageBounds = calculateNodeCropping(task, vuPage);
}
if (executor.isTaskDead(task)) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": Abort dead decode task for "
+ task.node);
}
return;
}
r = getScaledSize(task.node, task.viewState.zoom, croppedPageBounds, vuPage);
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": Rendering rect: " + r);
}
final RectF cropping = task.node.page.getCropping(task.node);
final RectF actualSliceBounds = cropping != null ? cropping : task.node.pageSliceBounds;
final ByteBufferBitmap bitmap = vuPage.renderBitmap(task.viewState, r.width(), r.height(),
actualSliceBounds);
if (executor.isTaskDead(task)) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": Abort dead decode task for "
+ task.node);
}
ByteBufferManager.release(bitmap);
return;
}
if (task.node.page.links == null) {
task.node.page.links = vuPage.getPageLinks();
if (LengthUtils.isNotEmpty(task.node.page.links)) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": Found links on page "
+ task.pageNumber + ": " + task.node.page.links);
}
}
}
finishDecoding(task, vuPage, bitmap, r, croppedPageBounds);
} catch (final OutOfMemoryError ex) {
LCTX.e(Thread.currentThread().getName() + ": Task " + task.id + ": No memory to decode " + task.node);
for (int i = 0; i <= AppSettings.current().pagesInMemory; i++) {
pages.put(Integer.MAX_VALUE - i, null);
}
pages.clear();
if (vuPage != null) {
vuPage.recycle();
}
BitmapManager.clear("DecodeService OutOfMemoryError: ");
ByteBufferManager.clear("DecodeService OutOfMemoryError: ");
abortDecoding(task, null, null);
} catch (final Throwable th) {
LCTX.e(Thread.currentThread().getName() + ": Task " + task.id + ": Decoding failed for " + task.node + ": "
+ th.getMessage(), th);
abortDecoding(task, vuPage, null);
} finally {
if (holder != null) {
holder.unlock();
}
}
}
protected RectF calculateNodeCropping(final DecodeTask task, final CodecPage vuPage) {
final PageTreeNode root = task.node.page.nodes.root;
RectF croppedPageBounds = null;
// Checks if page root node has not been cropped before
if (root.getCropping() == null) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": Decode full page to crop");
}
croppedPageBounds = calculateRootCropping(task, root, vuPage);
}
if (task.node != root) {
task.node.evaluateCroppedPageSliceBounds();
}
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": cropping bounds for task node: "
+ task.node.getCropping());
}
return croppedPageBounds;
}
protected RectF calculateRootCropping(final DecodeTask task, final PageTreeNode root, final CodecPage vuPage) {
final RectF rootBounds = root.pageSliceBounds;
final ByteBufferBitmap rootBitmap = vuPage.renderBitmap(task.viewState, PageCropper.BMP_SIZE,
PageCropper.BMP_SIZE, rootBounds);
final BookSettings bs = task.viewState.book;
if (bs != null) {
rootBitmap.applyEffects(bs);
}
root.setAutoCropping(PageCropper.getCropBounds(rootBitmap, root.pageSliceBounds), true);
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": cropping root bounds: "
+ root.getCropping());
}
ByteBufferManager.release(rootBitmap);
final ViewState viewState = task.viewState;
final PageIndex currentPage = viewState.book.getCurrentPage();
final float offsetX = viewState.book.offsetX;
final float offsetY = viewState.book.offsetY;
viewState.ctrl.invalidatePageSizes(InvalidateSizeReason.PAGE_LOADED, task.node.page);
final RectF croppedPageBounds = root.page.getBounds(task.viewState.zoom);
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + task.id + ": cropping page bounds: "
+ croppedPageBounds);
}
return croppedPageBounds;
}
Rect getScaledSize(final PageTreeNode node, final float zoom, final RectF croppedPageBounds, final CodecPage vuPage) {
final RectF pageBounds = MathUtils.zoom(croppedPageBounds != null ? croppedPageBounds : node.page.bounds, zoom);
final RectF r = Page.getTargetRect(node.page.type, pageBounds, node.pageSliceBounds);
return new Rect(0, 0, (int) r.width(), (int) r.height());
}
void finishDecoding(final DecodeTask currentDecodeTask, final CodecPage page, final ByteBufferBitmap bitmap,
final Rect bitmapBounds, final RectF croppedPageBounds) {
stopDecoding(currentDecodeTask.node, "complete");
updateImage(currentDecodeTask, page, bitmap, bitmapBounds, croppedPageBounds);
}
void abortDecoding(final DecodeTask currentDecodeTask, final CodecPage page, final ByteBufferBitmap bitmap) {
stopDecoding(currentDecodeTask.node, "failed");
updateImage(currentDecodeTask, page, bitmap, null, null);
}
CodecPage getPage(final int pageIndex) {
return getPageHolder(-2, pageIndex).getPage(-2);
}
private synchronized CodecPageHolder getPageHolder(final long taskId, final int pageIndex) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + "Task " + taskId + ": Codec pages in cache: " + pages.size());
}
for (final Iterator<Map.Entry<Integer, CodecPageHolder>> i = pages.entrySet().iterator(); i.hasNext();) {
final Map.Entry<Integer, CodecPageHolder> entry = i.next();
final int index = entry.getKey();
final CodecPageHolder ref = entry.getValue();
if (ref.isInvalid(-1)) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + "Task " + taskId
+ ": Remove auto-recycled codec page reference: " + index);
}
i.remove();
}
}
CodecPageHolder holder = pages.get(pageIndex);
if (holder == null) {
holder = new CodecPageHolder(document, pageIndex);
pages.put(pageIndex, holder);
}
// Preventing problem inside the MuPDF
if (!codecContext.isFeatureSupported(CodecFeatures.FEATURE_PARALLEL_PAGE_ACCESS)) {
holder.getPage(taskId);
}
return holder;
}
void updateImage(final DecodeTask currentDecodeTask, final CodecPage page, final ByteBufferBitmap bitmap,
final Rect bitmapBounds, final RectF croppedPageBounds) {
currentDecodeTask.node.decodeComplete(page, bitmap, croppedPageBounds);
}
@Override
public int getPageCount() {
return document != null ? document.getPageCount() : 0;
}
@Override
public List<OutlineLink> getOutline() {
return document != null ? document.getOutline() : null;
}
@Override
public void recycle() {
if (isRecycled.compareAndSet(false, true)) {
executor.recycle();
}
}
protected int getCacheSize() {
final ViewState vs = viewState.get();
int minSize = 1;
if (vs != null) {
minSize = vs.pages.lastVisible - vs.pages.firstVisible + 1;
}
final int pagesInMemory = AppSettings.current().pagesInMemory;
return pagesInMemory == 0 ? 0 : Math.max(minSize, pagesInMemory);
}
class PageCache extends LinkedHashMap<Integer, CodecPageHolder> {
private static final long serialVersionUID = -8845124816503128098L;
@Override
protected boolean removeEldestEntry(final Map.Entry<Integer, CodecPageHolder> eldest) {
if (this.size() > getCacheSize()) {
final CodecPageHolder value = eldest != null ? eldest.getValue() : null;
if (value != null) {
if (value.isInvalid(-1)) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Remove auto-recycled codec page reference: "
+ eldest.getKey());
}
return true;
} else {
final boolean recycled = value.recycle(-1, false);
if (LCTX.isDebugEnabled()) {
if (recycled) {
LCTX.d(Thread.currentThread().getName() + ": Recycle and remove old codec page: "
+ eldest.getKey());
} else {
LCTX.d(Thread.currentThread().getName()
+ ": Codec page locked and cannot be recycled: " + eldest.getKey());
}
}
return recycled;
}
}
}
return false;
}
}
class Executor implements Runnable {
final Map<PageTreeNode, DecodeTask> decodingTasks = new IdentityHashMap<PageTreeNode, DecodeTask>();
final ArrayList<Task> tasks;
final Thread[] threads;
final ReentrantLock lock = new ReentrantLock();
final AtomicBoolean run = new AtomicBoolean(true);
Executor() {
tasks = new ArrayList<Task>();
threads = new Thread[AppSettings.current().decodingThreads];
LCTX.i("Number of decoding threads: " + threads.length);
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(this, "DecodingThread-" + i);
}
}
void start() {
final int decodingThreadPriority = AppSettings.current().decodingThreadPriority;
LCTX.i("Decoding thread priority: " + decodingThreadPriority);
for (int i = 0; i < threads.length; i++) {
threads[i].setPriority(decodingThreadPriority);
threads[i].start();
}
}
@Override
public void run() {
try {
while (run.get()) {
final Runnable r = nextTask();
if (r != null) {
BitmapManager.release();
ByteBufferManager.release();
r.run();
}
}
} catch (final Throwable th) {
LCTX.e(Thread.currentThread().getName() + ": Decoding service executor failed: " + th.getMessage(), th);
LogManager.onUnexpectedError(th);
} finally {
BitmapManager.release();
}
}
Runnable nextTask() {
final ViewState vs = viewState != null ? viewState.get() : null;
if (vs == null || vs.app == null || vs.app.decodingOnScroll || vs.ctrl.getView().isScrollFinished()) {
lock.lock();
try {
if (!tasks.isEmpty()) {
return selectBestTask();
}
} finally {
lock.unlock();
}
} else {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": view in scrolling");
}
}
synchronized (run) {
try {
run.wait(500);
} catch (final InterruptedException ex) {
Thread.interrupted();
}
}
return null;
}
private Runnable selectBestTask() {
final TaskComparator comp = new TaskComparator(viewState.get());
Task candidate = null;
int cindex = 0;
int index = 0;
while (index < tasks.size() && candidate == null) {
candidate = tasks.get(index);
if (candidate != null && candidate.cancelled.get()) {
if (LCTX.isDebugEnabled()) {
LCTX.d("---: " + index + "/" + tasks.size() + " " + candidate);
}
tasks.set(index, null);
candidate = null;
}
cindex = index;
index++;
}
if (candidate == null) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": No tasks in queue");
}
tasks.clear();
} else {
while (index < tasks.size()) {
final Task next = tasks.get(index);
if (next != null) {
if (next.cancelled.get()) {
if (LCTX.isDebugEnabled()) {
LCTX.d("---: " + index + "/" + tasks.size() + " " + next);
}
tasks.set(index, null);
} else if (comp.compare(next, candidate) < 0) {
candidate = next;
cindex = index;
}
}
index++;
}
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": <<<: " + cindex + "/" + tasks.size() + ": "
+ candidate);
}
tasks.set(cindex, null);
}
return candidate;
}
public void add(final SearchTask task) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Adding search task: " + task + " for " + task.page.index);
}
lock.lock();
try {
boolean added = false;
for (int index = 0; index < tasks.size(); index++) {
if (null == tasks.get(index)) {
tasks.set(index, task);
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": >>>: " + index + "/" + tasks.size() + ": "
+ task);
}
added = true;
break;
}
}
if (!added) {
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": +++: " + tasks.size() + "/" + tasks.size() + ": "
+ task);
}
tasks.add(task);
}
synchronized (run) {
run.notifyAll();
}
} finally {
lock.unlock();
}
}
public void stopSearch(final String pattern) {
if (LCTX.isDebugEnabled()) {
LCTX.d("Stop search tasks: " + pattern);
}
lock.lock();
try {
for (int index = 0; index < tasks.size(); index++) {
final Task task = tasks.get(index);
if (task instanceof SearchTask) {
final SearchTask st = (SearchTask) task;
if (st.pattern.equals(pattern)) {
tasks.set(index, null);
}
}
}
} finally {
lock.unlock();
}
}
public void add(final DecodeTask task) {
if (LCTX.isDebugEnabled()) {
LCTX.d("Adding decoding task: " + task + " for " + task.node);
}
lock.lock();
try {
final DecodeTask running = decodingTasks.get(task.node);
if (running != null && running.equals(task) && !isTaskDead(running)) {
if (LCTX.isDebugEnabled()) {
LCTX.d("The similar task is running: " + running.id + " for " + task.node);
}
return;
} else if (running != null) {
if (LCTX.isDebugEnabled()) {
LCTX.d("The another task is running: " + running.id + " for " + task.node);
}
}
decodingTasks.put(task.node, task);
boolean added = false;
for (int index = 0; index < tasks.size(); index++) {
if (null == tasks.get(index)) {
tasks.set(index, task);
if (LCTX.isDebugEnabled()) {
LCTX.d(">>>: " + index + "/" + tasks.size() + ": " + task);
}
added = true;
break;
}
}
if (!added) {
if (LCTX.isDebugEnabled()) {
LCTX.d("+++: " + tasks.size() + "/" + tasks.size() + ": " + task);
}
tasks.add(task);
}
synchronized (run) {
run.notifyAll();
}
if (running != null) {
stopDecoding(running, null, "canceled by new one");
}
} finally {
lock.unlock();
}
}
public void stopDecoding(final DecodeTask task, final PageTreeNode node, final String reason) {
lock.lock();
try {
final DecodeTask removed = task == null ? decodingTasks.remove(node) : task;
if (removed != null) {
removed.cancelled.set(true);
if (LCTX.isDebugEnabled()) {
LCTX.d(Thread.currentThread().getName() + ": Task " + removed.id
+ ": Stop decoding task with reason: " + reason + " for " + removed.node);
}
}
} finally {
lock.unlock();
}
}
public boolean isTaskDead(final DecodeTask task) {
return task.cancelled.get();
}
public void recycle() {
lock.lock();
try {
for (final DecodeTask task : decodingTasks.values()) {
stopDecoding(task, null, "recycling");
}
tasks.add(new ShutdownTask());
synchronized (run) {
run.notifyAll();
}
} finally {
lock.unlock();
}
}
void shutdown() {
for (final CodecPageHolder ref : pages.values()) {
ref.recycle(-3, true);
}
pages.clear();
if (document != null) {
document.recycle();
}
codecContext.recycle();
run.set(false);
}
}
class TaskComparator implements Comparator<Task> {
final PageTreeNodeComparator cmp;
public TaskComparator(final ViewState viewState) {
cmp = viewState != null ? new PageTreeNodeComparator(viewState) : null;
}
@Override
public int compare(final Task r1, final Task r2) {
if (r1.priority < r2.priority) {
return -1;
}
if (r2.priority < r1.priority) {
return +1;
}
if (r1 instanceof DecodeTask && r2 instanceof DecodeTask) {
final DecodeTask t1 = (DecodeTask) r1;
final DecodeTask t2 = (DecodeTask) r2;
if (cmp != null) {
return cmp.compare(t1.node, t2.node);
}
return 0;
}
return CompareUtils.compare(r1.id, r2.id);
}
}
abstract class Task implements Runnable {
final long id = TASK_ID_SEQ.incrementAndGet();
final AtomicBoolean cancelled = new AtomicBoolean();
final int priority;
Task(final int priority) {
this.priority = priority;
}
}
class ShutdownTask extends Task {
public ShutdownTask() {
super(0);
}
@Override
public void run() {
executor.shutdown();
}
}
class SearchTask extends Task {
final Page page;
final String pattern;
final SearchCallback callback;
public SearchTask(final Page page, final String pattern, final SearchCallback callback) {
super(1);
this.page = page;
this.pattern = pattern;
this.callback = callback;
}
@Override
public void run() {
List<? extends RectF> regions = null;
if (document != null) {
try {
if (codecContext.isFeatureSupported(CodecFeatures.FEATURE_DOCUMENT_TEXT_SEARCH)) {
regions = document.searchText(page.index.docIndex, pattern);
} else if (codecContext.isFeatureSupported(CodecFeatures.FEATURE_PAGE_TEXT_SEARCH)) {
regions = getPage(page.index.docIndex).searchText(pattern);
}
callback.searchComplete(page, regions);
} catch (final Throwable th) {
LCTX.e("Unexpected error: ", th);
callback.searchComplete(page, null);
}
}
}
}
class DecodeTask extends Task {
final long id = TASK_ID_SEQ.incrementAndGet();
final AtomicBoolean cancelled = new AtomicBoolean();
final PageTreeNode node;
final ViewState viewState;
final int pageNumber;
DecodeTask(final ViewState viewState, final PageTreeNode node) {
super(2);
this.pageNumber = node.page.index.docIndex;
this.viewState = viewState;
this.node = node;
}
@Override
public void run() {
performDecode(this);
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof DecodeTask) {
final DecodeTask that = (DecodeTask) obj;
return this.pageNumber == that.pageNumber
&& this.viewState.viewRect.width() == that.viewState.viewRect.width()
&& this.viewState.zoom == that.viewState.zoom;
}
return false;
}
@Override
public String toString() {
final StringBuilder buf = new StringBuilder("DecodeTask");
buf.append("[");
buf.append("id").append("=").append(id);
buf.append(", ");
buf.append("target").append("=").append(node);
buf.append(", ");
buf.append("width").append("=").append((int) viewState.viewRect.width());
buf.append(", ");
buf.append("zoom").append("=").append(viewState.zoom);
buf.append("]");
return buf.toString();
}
}
@Override
public IBitmapRef createThumbnail(final boolean useEmbeddedIfAvailable, int width, int height, final int pageNo,
final RectF region) {
if (document == null) {
return null;
}
final Bitmap thumbnail = useEmbeddedIfAvailable ? document.getEmbeddedThumbnail() : null;
if (thumbnail != null) {
width = 200;
height = 200;
final int tw = thumbnail.getWidth();
final int th = thumbnail.getHeight();
if (th > tw) {
width = width * tw / th;
} else {
height = height * th / tw;
}
final Bitmap scaled = Bitmap.createScaledBitmap(thumbnail, width, height, true);
final IBitmapRef ref = BitmapManager.addBitmap("Thumbnail", scaled);
return ref;
} else {
final CodecPage page = getPage(pageNo);
return page.renderBitmap(null, width, height, region).toBitmap();
}
}
@Override
public ByteBufferBitmap createPageThumbnail(final int width, final int height, final int pageNo, final RectF region) {
if (document == null) {
return null;
}
final CodecPage page = getPage(pageNo);
return page.renderBitmap(null, width, height, region);
}
}