1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * Copyright (C) 2016 Mopria Alliance, Inc. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package com.android.bips.render; 19 20 import android.app.Service; 21 import android.content.Intent; 22 import android.graphics.Bitmap; 23 import android.graphics.Matrix; 24 import android.graphics.pdf.PdfRenderer; 25 import android.os.IBinder; 26 import android.os.ParcelFileDescriptor; 27 import android.os.RemoteException; 28 import android.util.Log; 29 30 import com.android.bips.jni.SizeD; 31 32 import java.io.IOException; 33 import java.io.OutputStream; 34 import java.nio.ByteBuffer; 35 36 /** 37 * Implements a PDF rendering service which can be run in an isolated process 38 */ 39 public class PdfRenderService extends Service { 40 private static final String TAG = PdfRenderService.class.getSimpleName(); 41 private static final boolean DEBUG = false; 42 43 /** How large of a chunk of Bitmap data to copy at once to the output stream */ 44 private static final int MAX_BYTES_PER_CHUNK = 1024 * 1024 * 5; 45 46 private PdfRenderer mRenderer; 47 private PdfRenderer.Page mPage; 48 49 /** Lock held to protect against close() of current page during rendering. */ 50 private final Object mPageOpenLock = new Object(); 51 52 @Override onBind(Intent intent)53 public IBinder onBind(Intent intent) { 54 return mBinder; 55 } 56 57 @Override onUnbind(Intent intent)58 public boolean onUnbind(Intent intent) { 59 closeAll(); 60 return super.onUnbind(intent); 61 } 62 63 private final IPdfRender.Stub mBinder = new IPdfRender.Stub() { 64 @Override 65 public int openDocument(ParcelFileDescriptor pfd) throws RemoteException { 66 if (!open(pfd)) { 67 return 0; 68 } 69 return mRenderer.getPageCount(); 70 } 71 72 @Override 73 public SizeD getPageSize(int page) throws RemoteException { 74 if (!openPage(page)) { 75 return null; 76 } 77 return new SizeD(mPage.getWidth(), mPage.getHeight()); 78 } 79 80 @Override 81 public ParcelFileDescriptor renderPageStripe(int page, int y, int width, int height, 82 double zoomFactor) 83 throws RemoteException { 84 if (!openPage(page)) { 85 return null; 86 } 87 88 // Create a pipe with input and output sides 89 ParcelFileDescriptor[] pipes; 90 try { 91 pipes = ParcelFileDescriptor.createPipe(); 92 } catch (IOException e) { 93 return null; 94 } 95 96 // Use a thread to spool out the bitmap data 97 new RenderThread(mPage, y, width, height, zoomFactor, pipes[1]).start(); 98 99 // Return the corresponding input stream. 100 return pipes[0]; 101 } 102 103 @Override 104 public void closeDocument() throws RemoteException { 105 if (DEBUG) Log.d(TAG, "closeDocument"); 106 closeAll(); 107 } 108 109 /** 110 * Ensure the specified PDF file is open, closing the old file if necessary, and returning 111 * true if successful. 112 */ 113 private boolean open(ParcelFileDescriptor pfd) { 114 closeAll(); 115 116 try { 117 mRenderer = new PdfRenderer(pfd); 118 } catch (IOException e) { 119 Log.w(TAG, "Could not open file descriptor for rendering", e); 120 return false; 121 } 122 return true; 123 } 124 125 /** 126 * Ensure the specified PDF file and page are open, closing the old file if necessary, and 127 * returning true if successful. 128 */ 129 private boolean openPage(int page) { 130 if (mRenderer == null) { 131 return false; 132 } 133 134 // Close old page if this is a new page 135 if (mPage != null && mPage.getIndex() != page) { 136 closePage(); 137 } 138 139 // Open new page if necessary 140 if (mPage == null) { 141 mPage = mRenderer.openPage(page); 142 } 143 return true; 144 } 145 }; 146 147 /** Close the current page if one is open */ closePage()148 private void closePage() { 149 if (mPage != null) { 150 synchronized (mPageOpenLock) { 151 mPage.close(); 152 } 153 mPage = null; 154 } 155 } 156 157 /** 158 * Close the current page and file if open 159 */ closeAll()160 private void closeAll() { 161 closePage(); 162 163 if (mRenderer != null) { 164 mRenderer.close(); 165 mRenderer = null; 166 } 167 } 168 169 /** 170 * Renders page data to RGB bytes and writes them to an output stream 171 */ 172 private class RenderThread extends Thread { 173 private final PdfRenderer.Page mPage; 174 private final int mWidth; 175 private final int mYOffset; 176 private final int mHeight; 177 private final double mZoomFactor; 178 private final int mRowsPerStripe; 179 private final ParcelFileDescriptor mOutput; 180 private final ByteBuffer mBuffer; 181 RenderThread(PdfRenderer.Page page, int y, int width, int height, double zoom, ParcelFileDescriptor output)182 RenderThread(PdfRenderer.Page page, int y, int width, int height, double zoom, 183 ParcelFileDescriptor output) { 184 mPage = page; 185 mWidth = width; 186 mYOffset = y; 187 mHeight = height; 188 mZoomFactor = zoom; 189 mOutput = output; 190 191 // Buffer will temporarily hold RGBA data from Bitmap 192 mRowsPerStripe = MAX_BYTES_PER_CHUNK / mWidth / 4; 193 mBuffer = ByteBuffer.allocate(mWidth * mRowsPerStripe * 4); 194 } 195 196 @Override run()197 public void run() { 198 Bitmap bitmap = null; 199 200 // Make sure nobody closes page while we're using it 201 synchronized (mPageOpenLock) { 202 try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream( 203 mOutput)) { 204 if (mPage == null) { 205 // If page was closed before we synchronized, this closes the outputStream 206 Log.e(TAG, "Page lost"); 207 return; 208 } 209 // Allocate and clear bitmap to white with no transparency 210 bitmap = Bitmap.createBitmap(mWidth, mRowsPerStripe, Bitmap.Config.ARGB_8888); 211 212 // Render each stripe to output 213 for (int startRow = mYOffset; startRow < mYOffset + mHeight; startRow += 214 mRowsPerStripe) { 215 int stripeRows = Math.min(mRowsPerStripe, (mYOffset + mHeight) - startRow); 216 renderToBitmap(startRow, bitmap); 217 writeRgb(bitmap, stripeRows, outputStream); 218 } 219 } catch (IOException e) { 220 Log.e(TAG, "Failed to write", e); 221 } finally { 222 if (bitmap != null) { 223 bitmap.recycle(); 224 } 225 } 226 } 227 } 228 229 /** From the specified starting row, render from the current page into the target bitmap */ renderToBitmap(int startRow, Bitmap bitmap)230 private void renderToBitmap(int startRow, Bitmap bitmap) { 231 Matrix matrix = new Matrix(); 232 // The scaling matrix increases DPI (default is 72dpi) to page output 233 matrix.setScale((float) mZoomFactor, (float) mZoomFactor); 234 // The translate specifies adjusts which part of the page we are rendering 235 matrix.postTranslate(0, 0 - startRow); 236 bitmap.eraseColor(0xFFFFFFFF); 237 238 mPage.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_PRINT); 239 } 240 241 /** Copy rows of RGB bytes from the bitmap to the output stream */ writeRgb(Bitmap bitmap, int rows, OutputStream out)242 private void writeRgb(Bitmap bitmap, int rows, OutputStream out) 243 throws IOException { 244 mBuffer.clear(); 245 bitmap.copyPixelsToBuffer(mBuffer); 246 int alphaPixelSize = mWidth * rows * 4; 247 248 // Chop out the alpha byte 249 byte[] array = mBuffer.array(); 250 int from, to; 251 for (from = 0, to = 0; from < alphaPixelSize; from += 4, to += 3) { 252 array[to] = array[from]; 253 array[to + 1] = array[from + 1]; 254 array[to + 2] = array[from + 2]; 255 } 256 257 // Write it 258 out.write(mBuffer.array(), 0, to); 259 } 260 } 261 } 262