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)) return 0; 67 return mRenderer.getPageCount(); 68 } 69 70 @Override 71 public SizeD getPageSize(int page) throws RemoteException { 72 if (!openPage(page)) return null; 73 return new SizeD(mPage.getWidth(), mPage.getHeight()); 74 } 75 76 @Override 77 public ParcelFileDescriptor renderPageStripe(int page, int y, int width, int height, 78 double zoomFactor) 79 throws RemoteException { 80 if (!openPage(page)) return null; 81 82 // Create a pipe with input and output sides 83 ParcelFileDescriptor pipes[]; 84 try { 85 pipes = ParcelFileDescriptor.createPipe(); 86 } catch (IOException e) { 87 return null; 88 } 89 90 // Use a thread to spool out the bitmap data 91 new RenderThread(mPage, y, width, height, zoomFactor, pipes[1]).start(); 92 93 // Return the corresponding input stream. 94 return pipes[0]; 95 } 96 97 @Override 98 public void closeDocument() throws RemoteException { 99 if (DEBUG) Log.d(TAG, "closeDocument"); 100 closeAll(); 101 } 102 103 /** 104 * Ensure the specified PDF file is open, closing the old file if necessary, and returning 105 * true if successful. 106 */ 107 private boolean open(ParcelFileDescriptor pfd) { 108 closeAll(); 109 110 try { 111 mRenderer = new PdfRenderer(pfd); 112 } catch (IOException e) { 113 Log.w(TAG, "Could not open file descriptor for rendering", e); 114 return false; 115 } 116 return true; 117 } 118 119 /** 120 * Ensure the specified PDF file and page are open, closing the old file if necessary, and 121 * returning true if successful. 122 */ 123 private boolean openPage(int page) { 124 if (mRenderer == null) return false; 125 126 // Close old page if this is a new page 127 if (mPage != null && mPage.getIndex() != page) { 128 closePage(); 129 } 130 131 // Open new page if necessary 132 if (mPage == null) { 133 mPage = mRenderer.openPage(page); 134 } 135 return true; 136 } 137 }; 138 139 /** Close the current page if one is open */ closePage()140 private void closePage() { 141 if (mPage != null) { 142 synchronized (mPageOpenLock) { 143 mPage.close(); 144 } 145 mPage = null; 146 } 147 } 148 149 /** 150 * Close the current page and file if open 151 */ closeAll()152 private void closeAll() { 153 closePage(); 154 155 if (mRenderer != null) { 156 mRenderer.close(); 157 mRenderer = null; 158 } 159 } 160 161 /** 162 * Renders page data to RGB bytes and writes them to an output stream 163 */ 164 private class RenderThread extends Thread { 165 private final PdfRenderer.Page mPage; 166 private final int mWidth; 167 private final int mYOffset; 168 private final int mHeight; 169 private final double mZoomFactor; 170 private final int mRowsPerStripe; 171 private final ParcelFileDescriptor mOutput; 172 private final ByteBuffer mBuffer; 173 RenderThread(PdfRenderer.Page page, int y, int width, int height, double zoom, ParcelFileDescriptor output)174 RenderThread(PdfRenderer.Page page, int y, int width, int height, double zoom, 175 ParcelFileDescriptor output) { 176 mPage = page; 177 mWidth = width; 178 mYOffset = y; 179 mHeight = height; 180 mZoomFactor = zoom; 181 mOutput = output; 182 183 // Buffer will temporarily hold RGBA data from Bitmap 184 mRowsPerStripe = MAX_BYTES_PER_CHUNK / mWidth / 4; 185 mBuffer = ByteBuffer.allocate(mWidth * mRowsPerStripe * 4); 186 } 187 188 @Override run()189 public void run() { 190 Bitmap bitmap = null; 191 192 // Make sure nobody closes page while we're using it 193 synchronized(mPageOpenLock) { 194 try (OutputStream outputStream = new ParcelFileDescriptor.AutoCloseOutputStream( 195 mOutput)) { 196 if (mPage == null) { 197 // If page was closed before we synchronized, this closes the outputStream 198 Log.e(TAG, "Page lost"); 199 return; 200 } 201 // Allocate and clear bitmap to white with no transparency 202 bitmap = Bitmap.createBitmap(mWidth, mRowsPerStripe, Bitmap.Config.ARGB_8888); 203 204 // Render each stripe to output 205 for (int startRow = mYOffset; startRow < mYOffset + mHeight; startRow += 206 mRowsPerStripe) { 207 int stripeRows = Math.min(mRowsPerStripe, (mYOffset + mHeight) - startRow); 208 renderToBitmap(startRow, bitmap); 209 writeRgb(bitmap, stripeRows, outputStream); 210 } 211 } catch (IOException e) { 212 Log.e(TAG, "Failed to write", e); 213 } finally { 214 if (bitmap != null) bitmap.recycle(); 215 } 216 } 217 } 218 219 /** From the specified starting row, render from the current page into the target bitmap */ renderToBitmap(int startRow, Bitmap bitmap)220 private void renderToBitmap(int startRow, Bitmap bitmap) { 221 Matrix matrix = new Matrix(); 222 // The scaling matrix increases DPI (default is 72dpi) to page output 223 matrix.setScale((float) mZoomFactor, (float) mZoomFactor); 224 // The translate specifies adjusts which part of the page we are rendering 225 matrix.postTranslate(0, 0 - startRow); 226 bitmap.eraseColor(0xFFFFFFFF); 227 228 mPage.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_PRINT); 229 } 230 231 /** Copy rows of RGB bytes from the bitmap to the output stream */ writeRgb(Bitmap bitmap, int rows, OutputStream out)232 private void writeRgb(Bitmap bitmap, int rows, OutputStream out) 233 throws IOException { 234 mBuffer.clear(); 235 bitmap.copyPixelsToBuffer(mBuffer); 236 int alphaPixelSize = mWidth * rows * 4; 237 238 // Chop out the alpha byte 239 byte array[] = mBuffer.array(); 240 int from, to; 241 for (from = 0, to = 0; from < alphaPixelSize; from += 4, to += 3) { 242 array[to] = array[from]; 243 array[to + 1] = array[from + 1]; 244 array[to + 2] = array[from + 2]; 245 } 246 247 // Write it 248 out.write(mBuffer.array(), 0, to); 249 } 250 } 251 }