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 }