Home | History | Annotate | Download | only in renderer
      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
     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
     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
    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
    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 
    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 
    201         private void throwIfOpened() {
    202             if (mRenderer != null) {
    203                 throw new IllegalStateException("Already opened");
    204             }
    205         }
    206 
    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
    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
    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
    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
    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
    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 
    382         private void throwIfOpened() {
    383             if (mEditor != null) {
    384                 throw new IllegalStateException("Already opened");
    385             }
    386         }
    387 
    388         private void throwIfNotOpened() {
    389             if (mEditor == null) {
    390                 throw new IllegalStateException("Not opened");
    391             }
    392         }
    393     }
    394 
    395     private static int pointsFromMils(int mils) {
    396         return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH);
    397     }
    398 }
    399