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