1 /*
2  * Copyright (C) 2014 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.printspooler.renderer;
18 
19 import android.app.Service;
20 import android.content.Intent;
21 import android.content.res.Configuration;
22 import android.graphics.Bitmap;
23 import android.graphics.Color;
24 import android.graphics.Matrix;
25 import android.graphics.Rect;
26 import android.graphics.pdf.PdfEditor;
27 import android.graphics.pdf.PdfRenderer;
28 import android.os.IBinder;
29 import android.os.ParcelFileDescriptor;
30 import android.os.RemoteException;
31 import android.print.PageRange;
32 import android.print.PrintAttributes;
33 import android.print.PrintAttributes.Margins;
34 import android.util.Log;
35 import android.view.View;
36 import com.android.printspooler.util.PageRangeUtils;
37 import libcore.io.IoUtils;
38 import com.android.printspooler.util.BitmapSerializeUtils;
39 import java.io.IOException;
40 
41 /**
42  * Service for manipulation of PDF documents in an isolated process.
43  */
44 public final class PdfManipulationService extends Service {
45     public static final String ACTION_GET_RENDERER =
46             "com.android.printspooler.renderer.ACTION_GET_RENDERER";
47     public static final String ACTION_GET_EDITOR =
48             "com.android.printspooler.renderer.ACTION_GET_EDITOR";
49 
50     public static final int ERROR_MALFORMED_PDF_FILE = -2;
51 
52     public static final int ERROR_SECURE_PDF_FILE = -3;
53 
54     private static final String LOG_TAG = "PdfManipulationService";
55     private static final boolean DEBUG = false;
56 
57     private static final int MILS_PER_INCH = 1000;
58     private static final int POINTS_IN_INCH = 72;
59 
60     @Override
onBind(Intent intent)61     public IBinder onBind(Intent intent) {
62         String action = intent.getAction();
63         switch (action) {
64             case ACTION_GET_RENDERER: {
65                 return new PdfRendererImpl();
66             }
67             case ACTION_GET_EDITOR: {
68                 return new PdfEditorImpl();
69             }
70             default: {
71                 throw new IllegalArgumentException("Invalid intent action:" + action);
72             }
73         }
74     }
75 
76     private final class PdfRendererImpl extends IPdfRenderer.Stub {
77         private final Object mLock = new Object();
78 
79         private Bitmap mBitmap;
80         private PdfRenderer mRenderer;
81 
82         @Override
openDocument(ParcelFileDescriptor source)83         public int openDocument(ParcelFileDescriptor source) throws RemoteException {
84             synchronized (mLock) {
85                 try {
86                     throwIfOpened();
87                     if (DEBUG) {
88                         Log.i(LOG_TAG, "openDocument()");
89                     }
90                     mRenderer = new PdfRenderer(source);
91                     return mRenderer.getPageCount();
92                 } catch (IOException | IllegalStateException e) {
93                     IoUtils.closeQuietly(source);
94                     Log.e(LOG_TAG, "Cannot open file", e);
95                     return ERROR_MALFORMED_PDF_FILE;
96                 } catch (SecurityException e) {
97                     IoUtils.closeQuietly(source);
98                     Log.e(LOG_TAG, "Cannot open file", e);
99                     return ERROR_SECURE_PDF_FILE;
100                 }
101             }
102         }
103 
104         @Override
renderPage(int pageIndex, int bitmapWidth, int bitmapHeight, PrintAttributes attributes, ParcelFileDescriptor destination)105         public void renderPage(int pageIndex, int bitmapWidth, int bitmapHeight,
106                 PrintAttributes attributes, ParcelFileDescriptor destination) {
107             synchronized (mLock) {
108                 try {
109                     throwIfNotOpened();
110 
111                     try (PdfRenderer.Page page = mRenderer.openPage(pageIndex)) {
112                         final int srcWidthPts = page.getWidth();
113                         final int srcHeightPts = page.getHeight();
114 
115                         final int dstWidthPts = pointsFromMils(
116                                 attributes.getMediaSize().getWidthMils());
117                         final int dstHeightPts = pointsFromMils(
118                                 attributes.getMediaSize().getHeightMils());
119 
120                         final boolean scaleContent = mRenderer.shouldScaleForPrinting();
121                         final boolean contentLandscape = !attributes.getMediaSize().isPortrait();
122 
123                         final float displayScale;
124                         Matrix matrix = new Matrix();
125 
126                         if (scaleContent) {
127                             displayScale = Math.min((float) bitmapWidth / srcWidthPts,
128                                     (float) bitmapHeight / srcHeightPts);
129                         } else {
130                             if (contentLandscape) {
131                                 displayScale = (float) bitmapHeight / dstHeightPts;
132                             } else {
133                                 displayScale = (float) bitmapWidth / dstWidthPts;
134                             }
135                         }
136                         matrix.postScale(displayScale, displayScale);
137 
138                         Configuration configuration = PdfManipulationService.this.getResources()
139                                 .getConfiguration();
140                         if (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
141                             matrix.postTranslate(bitmapWidth - srcWidthPts * displayScale, 0);
142                         }
143 
144                         Margins minMargins = attributes.getMinMargins();
145                         final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils());
146                         final int paddingTopPts = pointsFromMils(minMargins.getTopMils());
147                         final int paddingRightPts = pointsFromMils(minMargins.getRightMils());
148                         final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils());
149 
150                         Rect clip = new Rect();
151                         clip.left = (int) (paddingLeftPts * displayScale);
152                         clip.top = (int) (paddingTopPts * displayScale);
153                         clip.right = (int) (bitmapWidth - paddingRightPts * displayScale);
154                         clip.bottom = (int) (bitmapHeight - paddingBottomPts * displayScale);
155 
156                         if (DEBUG) {
157                             Log.i(LOG_TAG, "Rendering page:" + pageIndex);
158                         }
159 
160                         Bitmap bitmap = getBitmapForSize(bitmapWidth, bitmapHeight);
161                         page.render(bitmap, clip, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
162 
163                         BitmapSerializeUtils.writeBitmapPixels(bitmap, destination);
164                     }
165                 } catch (Throwable e) {
166                     Log.e(LOG_TAG, "Cannot render page", e);
167 
168                     // The error is propagated to the caller when it tries to read the bitmap and
169                     // the pipe is closed prematurely
170                 } finally {
171                     IoUtils.closeQuietly(destination);
172                 }
173             }
174         }
175 
176         @Override
closeDocument()177         public void closeDocument() {
178             synchronized (mLock) {
179                 throwIfNotOpened();
180                 if (DEBUG) {
181                     Log.i(LOG_TAG, "closeDocument()");
182                 }
183                 mRenderer.close();
184                 mRenderer = null;
185             }
186         }
187 
getBitmapForSize(int width, int height)188         private Bitmap getBitmapForSize(int width, int height) {
189             if (mBitmap != null) {
190                 if (mBitmap.getWidth() == width && mBitmap.getHeight() == height) {
191                     mBitmap.eraseColor(Color.WHITE);
192                     return mBitmap;
193                 }
194                 mBitmap.recycle();
195             }
196             mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
197             mBitmap.eraseColor(Color.WHITE);
198             return mBitmap;
199         }
200 
throwIfOpened()201         private void throwIfOpened() {
202             if (mRenderer != null) {
203                 throw new IllegalStateException("Already opened");
204             }
205         }
206 
throwIfNotOpened()207         private void throwIfNotOpened() {
208             if (mRenderer == null) {
209                 throw new IllegalStateException("Not opened");
210             }
211         }
212     }
213 
214     private final class PdfEditorImpl extends IPdfEditor.Stub {
215         private final Object mLock = new Object();
216 
217         private PdfEditor mEditor;
218 
219         @Override
openDocument(ParcelFileDescriptor source)220         public int openDocument(ParcelFileDescriptor source) throws RemoteException {
221             synchronized (mLock) {
222                 try {
223                     throwIfOpened();
224                     if (DEBUG) {
225                         Log.i(LOG_TAG, "openDocument()");
226                     }
227                     mEditor = new PdfEditor(source);
228                     return mEditor.getPageCount();
229                 } catch (IOException | IllegalStateException e) {
230                     IoUtils.closeQuietly(source);
231                     Log.e(LOG_TAG, "Cannot open file", e);
232                     throw new RemoteException(e.toString());
233                 }
234             }
235         }
236 
237         @Override
removePages(PageRange[] ranges)238         public void removePages(PageRange[] ranges) {
239             synchronized (mLock) {
240                 throwIfNotOpened();
241                 if (DEBUG) {
242                     Log.i(LOG_TAG, "removePages()");
243                 }
244 
245                 ranges = PageRangeUtils.normalize(ranges);
246 
247                 int lastPageIdx = mEditor.getPageCount() - 1;
248 
249                 final int rangeCount = ranges.length;
250                 for (int i = rangeCount - 1; i >= 0; i--) {
251                     PageRange range = ranges[i];
252 
253                     // Ignore removal of pages that are outside the document
254                     if (range.getEnd() > lastPageIdx) {
255                         if (range.getStart() > lastPageIdx) {
256                             continue;
257                         }
258                         range = new PageRange(range.getStart(), lastPageIdx);
259                     }
260 
261                     for (int j = range.getEnd(); j >= range.getStart(); j--) {
262                         mEditor.removePage(j);
263                     }
264                 }
265             }
266         }
267 
268         @Override
applyPrintAttributes(PrintAttributes attributes)269         public void applyPrintAttributes(PrintAttributes attributes) {
270             synchronized (mLock) {
271                 throwIfNotOpened();
272                 if (DEBUG) {
273                     Log.i(LOG_TAG, "applyPrintAttributes()");
274                 }
275 
276                 Rect mediaBox = new Rect();
277                 Rect cropBox = new Rect();
278                 Matrix transform = new Matrix();
279 
280                 final boolean layoutDirectionRtl = getResources().getConfiguration()
281                         .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
282 
283                 // We do not want to rotate the media box, so take into account orientation.
284                 final int dstWidthPts = pointsFromMils(attributes.getMediaSize().getWidthMils());
285                 final int dstHeightPts = pointsFromMils(attributes.getMediaSize().getHeightMils());
286 
287                 final boolean scaleForPrinting = mEditor.shouldScaleForPrinting();
288 
289                 final int pageCount = mEditor.getPageCount();
290                 for (int i = 0; i < pageCount; i++) {
291                     if (!mEditor.getPageMediaBox(i, mediaBox)) {
292                         Log.e(LOG_TAG, "Malformed PDF file");
293                         return;
294                     }
295 
296                     final int srcWidthPts = mediaBox.width();
297                     final int srcHeightPts = mediaBox.height();
298 
299                     // Update the media box with the desired size.
300                     mediaBox.right = dstWidthPts;
301                     mediaBox.bottom = dstHeightPts;
302                     mEditor.setPageMediaBox(i, mediaBox);
303 
304                     // Make sure content is top-left after media box resize.
305                     transform.setTranslate(0, srcHeightPts - dstHeightPts);
306 
307                     // Scale the content if document allows it.
308                     final float scale;
309                     if (scaleForPrinting) {
310                         scale = Math.min((float) dstWidthPts / srcWidthPts,
311                                 (float) dstHeightPts / srcHeightPts);
312                         transform.postScale(scale, scale);
313                     } else {
314                         scale = 1.0f;
315                     }
316 
317                     // Update the crop box relatively to the media box change, if needed.
318                     if (mEditor.getPageCropBox(i, cropBox)) {
319                         cropBox.left = (int) (cropBox.left * scale + 0.5f);
320                         cropBox.top = (int) (cropBox.top * scale + 0.5f);
321                         cropBox.right = (int) (cropBox.right * scale + 0.5f);
322                         cropBox.bottom = (int) (cropBox.bottom * scale + 0.5f);
323                         cropBox.intersect(mediaBox);
324                         mEditor.setPageCropBox(i, cropBox);
325                     }
326 
327                     // If in RTL mode put the content in the logical top-right corner.
328                     if (layoutDirectionRtl) {
329                         final float dx = dstWidthPts - (int) (srcWidthPts * scale + 0.5f);
330                         final float dy = 0;
331                         transform.postTranslate(dx, dy);
332                     }
333 
334                     // Adjust the physical margins if needed.
335                     Margins minMargins = attributes.getMinMargins();
336                     final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils());
337                     final int paddingTopPts = pointsFromMils(minMargins.getTopMils());
338                     final int paddingRightPts = pointsFromMils(minMargins.getRightMils());
339                     final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils());
340 
341                     Rect clip = new Rect(mediaBox);
342                     clip.left += paddingLeftPts;
343                     clip.top += paddingTopPts;
344                     clip.right -= paddingRightPts;
345                     clip.bottom -= paddingBottomPts;
346 
347                     // Apply the accumulated transforms.
348                     mEditor.setTransformAndClip(i, transform, clip);
349                 }
350             }
351         }
352 
353         @Override
write(ParcelFileDescriptor destination)354         public void write(ParcelFileDescriptor destination) throws RemoteException {
355             synchronized (mLock) {
356                 try {
357                     throwIfNotOpened();
358                     if (DEBUG) {
359                         Log.i(LOG_TAG, "write()");
360                     }
361                     mEditor.write(destination);
362                 } catch (IOException | IllegalStateException e) {
363                     IoUtils.closeQuietly(destination);
364                     Log.e(LOG_TAG, "Error writing PDF to file.", e);
365                     throw new RemoteException(e.toString());
366                 }
367             }
368         }
369 
370         @Override
closeDocument()371         public void closeDocument() {
372             synchronized (mLock) {
373                 throwIfNotOpened();
374                 if (DEBUG) {
375                     Log.i(LOG_TAG, "closeDocument()");
376                 }
377                 mEditor.close();
378                 mEditor = null;
379             }
380         }
381 
throwIfOpened()382         private void throwIfOpened() {
383             if (mEditor != null) {
384                 throw new IllegalStateException("Already opened");
385             }
386         }
387 
throwIfNotOpened()388         private void throwIfNotOpened() {
389             if (mEditor == null) {
390                 throw new IllegalStateException("Not opened");
391             }
392         }
393     }
394 
pointsFromMils(int mils)395     private static int pointsFromMils(int mils) {
396         return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH);
397     }
398 }
399