| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * Copyright (C) 2016 Mopria Alliance, Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.bips.render; |
| |
| import android.app.Service; |
| import android.content.Intent; |
| import android.graphics.Bitmap; |
| import android.graphics.Matrix; |
| import android.graphics.pdf.PdfRenderer; |
| import android.os.IBinder; |
| import android.os.ParcelFileDescriptor; |
| import android.os.RemoteException; |
| import android.util.Log; |
| |
| import com.android.bips.jni.SizeD; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.nio.ByteBuffer; |
| |
| /** |
| * Implements a PDF rendering service which can be run in an isolated process |
| */ |
| public class PdfRenderService extends Service { |
| private static final String TAG = PdfRenderService.class.getSimpleName(); |
| private static final boolean DEBUG = false; |
| |
| /** How large of a chunk of Bitmap data to copy at once to the output stream */ |
| private static final int MAX_BYTES_PER_CHUNK = 1024 * 1024 * 5; |
| |
| private PdfRenderer mRenderer; |
| private PdfRenderer.Page mPage; |
| |
| /** Lock held to protect against close() of current page during rendering. */ |
| private final Object mPageOpenLock = new Object(); |
| |
| @Override |
| public IBinder onBind(Intent intent) { |
| return mBinder; |
| } |
| |
| @Override |
| public boolean onUnbind(Intent intent) { |
| closeAll(); |
| return super.onUnbind(intent); |
| } |
| |
| private final IPdfRender.Stub mBinder = new IPdfRender.Stub() { |
| @Override |
| public int openDocument(ParcelFileDescriptor pfd) throws RemoteException { |
| if (!open(pfd)) { |
| return 0; |
| } |
| return mRenderer.getPageCount(); |
| } |
| |
| @Override |
| public SizeD getPageSize(int page) throws RemoteException { |
| if (!openPage(page)) { |
| return null; |
| } |
| return new SizeD(mPage.getWidth(), mPage.getHeight()); |
| } |
| |
| @Override |
| public ParcelFileDescriptor renderPageStripe(int page, int y, int width, int height, |
| double zoomFactor) |
| throws RemoteException { |
| if (!openPage(page)) { |
| return null; |
| } |
| |
| // Create a pipe with input and output sides |
| ParcelFileDescriptor[] pipes; |
| try { |
| pipes = ParcelFileDescriptor.createPipe(); |
| } catch (IOException e) { |
| return null; |
| } |
| |
| // Use a thread to spool out the bitmap data |
| new RenderThread(mPage, y, width, height, zoomFactor, pipes[1]).start(); |
| |
| // Return the corresponding input stream. |
| return pipes[0]; |
| } |
| |
| @Override |
| public void closeDocument() throws RemoteException { |
| if (DEBUG) Log.d(TAG, "closeDocument"); |
| closeAll(); |
| } |
| |
| /** |
| * Ensure the specified PDF file is open, closing the old file if necessary, and returning |
| * true if successful. |
| */ |
| private boolean open(ParcelFileDescriptor pfd) { |
| closeAll(); |
| |
| try { |
| mRenderer = new PdfRenderer(pfd); |
| } catch (IOException e) { |
| Log.w(TAG, "Could not open file descriptor for rendering", e); |
| return false; |
| } |
| return true; |
| } |
| |
| /** |
| * Ensure the specified PDF file and page are open, closing the old file if necessary, and |
| * returning true if successful. |
| */ |
| private boolean openPage(int page) { |
| if (mRenderer == null) { |
| return false; |
| } |
| |
| // Close old page if this is a new page |
| if (mPage != null && mPage.getIndex() != page) { |
| closePage(); |
| } |
| |
| // Open new page if necessary |
| if (mPage == null) { |
| mPage = mRenderer.openPage(page); |
| } |
| return true; |
| } |
| }; |
| |
| /** Close the current page if one is open */ |
| private void closePage() { |
| if (mPage != null) { |
| synchronized (mPageOpenLock) { |
| mPage.close(); |
| } |
| mPage = null; |
| } |
| } |
| |
| /** |
| * Close the current page and file if open |
| */ |
| private void closeAll() { |
| closePage(); |
| |
| if (mRenderer != null) { |
| mRenderer.close(); |
| mRenderer = null; |
| } |
| } |
| |
| /** |
| * Renders page data to RGB bytes and writes them to an output stream |
| */ |
| private class RenderThread extends Thread { |
| private final PdfRenderer.Page mPage; |
| private final int mWidth; |
| private final int mYOffset; |
| private final int mHeight; |
| private final double mZoomFactor; |
| private final int mRowsPerStripe; |
| private final ParcelFileDescriptor mOutput; |
| private final ByteBuffer mBuffer; |
| |
| RenderThread(PdfRenderer.Page page, int y, int width, int height, double zoom, |
| ParcelFileDescriptor output) { |
| mPage = page; |
| mWidth = width; |
| mYOffset = y; |
| mHeight = height; |
| mZoomFactor = zoom; |
| mOutput = output; |
| |
| // Buffer will temporarily hold RGBA data from Bitmap |
| mRowsPerStripe = MAX_BYTES_PER_CHUNK / mWidth / 4; |
| mBuffer = ByteBuffer.allocate(mWidth * mRowsPerStripe * 4); |
| } |
| |
| @Override |
| public void run() { |
| Bitmap bitmap = null; |
| |
| // Make sure nobody closes page while we're using it |
| synchronized (mPageOpenLock) { |
| try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream( |
| mOutput)) { |
| if (mPage == null) { |
| // If page was closed before we synchronized, this closes the outputStream |
| Log.e(TAG, "Page lost"); |
| return; |
| } |
| // Allocate and clear bitmap to white with no transparency |
| bitmap = Bitmap.createBitmap(mWidth, mRowsPerStripe, Bitmap.Config.ARGB_8888); |
| |
| // Render each stripe to output |
| for (int startRow = mYOffset; startRow < mYOffset + mHeight; startRow += |
| mRowsPerStripe) { |
| int stripeRows = Math.min(mRowsPerStripe, (mYOffset + mHeight) - startRow); |
| renderToBitmap(startRow, bitmap); |
| writeRgb(bitmap, stripeRows, outputStream); |
| } |
| } catch (IOException e) { |
| Log.e(TAG, "Failed to write", e); |
| } finally { |
| if (bitmap != null) { |
| bitmap.recycle(); |
| } |
| } |
| } |
| } |
| |
| /** From the specified starting row, render from the current page into the target bitmap */ |
| private void renderToBitmap(int startRow, Bitmap bitmap) { |
| Matrix matrix = new Matrix(); |
| // The scaling matrix increases DPI (default is 72dpi) to page output |
| matrix.setScale((float) mZoomFactor, (float) mZoomFactor); |
| // The translate specifies adjusts which part of the page we are rendering |
| matrix.postTranslate(0, 0 - startRow); |
| bitmap.eraseColor(0xFFFFFFFF); |
| |
| mPage.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_PRINT); |
| } |
| |
| /** Copy rows of RGB bytes from the bitmap to the output stream */ |
| private void writeRgb(Bitmap bitmap, int rows, OutputStream out) |
| throws IOException { |
| mBuffer.clear(); |
| bitmap.copyPixelsToBuffer(mBuffer); |
| int alphaPixelSize = mWidth * rows * 4; |
| |
| // Chop out the alpha byte |
| byte[] array = mBuffer.array(); |
| int from, to; |
| for (from = 0, to = 0; from < alphaPixelSize; from += 4, to += 3) { |
| array[to] = array[from]; |
| array[to + 1] = array[from + 1]; |
| array[to + 2] = array[from + 2]; |
| } |
| |
| // Write it |
| out.write(mBuffer.array(), 0, to); |
| } |
| } |
| } |