/* * 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); } } }