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                     PdfRenderer.Page page = mRenderer.openPage(pageIndex);
    112 
    113                     final int srcWidthPts = page.getWidth();
    114                     final int srcHeightPts = page.getHeight();
    115 
    116                     final int dstWidthPts = pointsFromMils(
    117                             attributes.getMediaSize().getWidthMils());
    118                     final int dstHeightPts = pointsFromMils(
    119                             attributes.getMediaSize().getHeightMils());
    120 
    121                     final boolean scaleContent = mRenderer.shouldScaleForPrinting();
    122                     final boolean contentLandscape = !attributes.getMediaSize().isPortrait();
    123 
    124                     final float displayScale;
    125                     Matrix matrix = new Matrix();
    126 
    127                     if (scaleContent) {
    128                         displayScale = Math.min((float) bitmapWidth / srcWidthPts,
    129                                 (float) bitmapHeight / srcHeightPts);
    130                     } else {
    131                         if (contentLandscape) {
    132                             displayScale = (float) bitmapHeight / dstHeightPts;
    133                         } else {
    134                             displayScale = (float) bitmapWidth / dstWidthPts;
    135                         }
    136                     }
    137                     matrix.postScale(displayScale, displayScale);
    138 
    139                     Configuration configuration = PdfManipulationService.this.getResources()
    140                             .getConfiguration();
    141                     if (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
    142                         matrix.postTranslate(bitmapWidth - srcWidthPts * displayScale, 0);
    143                     }
    144 
    145                     Margins minMargins = attributes.getMinMargins();
    146                     final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils());
    147                     final int paddingTopPts = pointsFromMils(minMargins.getTopMils());
    148                     final int paddingRightPts = pointsFromMils(minMargins.getRightMils());
    149                     final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils());
    150 
    151                     Rect clip = new Rect();
    152                     clip.left = (int) (paddingLeftPts * displayScale);
    153                     clip.top = (int) (paddingTopPts * displayScale);
    154                     clip.right = (int) (bitmapWidth - paddingRightPts * displayScale);
    155                     clip.bottom = (int) (bitmapHeight - paddingBottomPts * displayScale);
    156 
    157                     if (DEBUG) {
    158                         Log.i(LOG_TAG, "Rendering page:" + pageIndex);
    159                     }
    160 
    161                     Bitmap bitmap = getBitmapForSize(bitmapWidth, bitmapHeight);
    162                     page.render(bitmap, clip, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
    163 
    164                     page.close();
    165 
    166                     BitmapSerializeUtils.writeBitmapPixels(bitmap, destination);
    167                 } finally {
    168                     IoUtils.closeQuietly(destination);
    169                 }
    170             }
    171         }
    172 
    173         @Override
    174         public void closeDocument() {
    175             synchronized (mLock) {
    176                 throwIfNotOpened();
    177                 if (DEBUG) {
    178                     Log.i(LOG_TAG, "closeDocument()");
    179                 }
    180                 mRenderer.close();
    181                 mRenderer = null;
    182             }
    183         }
    184 
    185         private Bitmap getBitmapForSize(int width, int height) {
    186             if (mBitmap != null) {
    187                 if (mBitmap.getWidth() == width && mBitmap.getHeight() == height) {
    188                     mBitmap.eraseColor(Color.WHITE);
    189                     return mBitmap;
    190                 }
    191                 mBitmap.recycle();
    192             }
    193             mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
    194             mBitmap.eraseColor(Color.WHITE);
    195             return mBitmap;
    196         }
    197 
    198         private void throwIfOpened() {
    199             if (mRenderer != null) {
    200                 throw new IllegalStateException("Already opened");
    201             }
    202         }
    203 
    204         private void throwIfNotOpened() {
    205             if (mRenderer == null) {
    206                 throw new IllegalStateException("Not opened");
    207             }
    208         }
    209     }
    210 
    211     private final class PdfEditorImpl extends IPdfEditor.Stub {
    212         private final Object mLock = new Object();
    213 
    214         private PdfEditor mEditor;
    215 
    216         @Override
    217         public int openDocument(ParcelFileDescriptor source) throws RemoteException {
    218             synchronized (mLock) {
    219                 try {
    220                     throwIfOpened();
    221                     if (DEBUG) {
    222                         Log.i(LOG_TAG, "openDocument()");
    223                     }
    224                     mEditor = new PdfEditor(source);
    225                     return mEditor.getPageCount();
    226                 } catch (IOException | IllegalStateException e) {
    227                     IoUtils.closeQuietly(source);
    228                     Log.e(LOG_TAG, "Cannot open file", e);
    229                     throw new RemoteException(e.toString());
    230                 }
    231             }
    232         }
    233 
    234         @Override
    235         public void removePages(PageRange[] ranges) {
    236             synchronized (mLock) {
    237                 throwIfNotOpened();
    238                 if (DEBUG) {
    239                     Log.i(LOG_TAG, "removePages()");
    240                 }
    241 
    242                 ranges = PageRangeUtils.normalize(ranges);
    243 
    244                 final int rangeCount = ranges.length;
    245                 for (int i = rangeCount - 1; i >= 0; i--) {
    246                     PageRange range = ranges[i];
    247                     for (int j = range.getEnd(); j >= range.getStart(); j--) {
    248                         mEditor.removePage(j);
    249                     }
    250                 }
    251             }
    252         }
    253 
    254         @Override
    255         public void applyPrintAttributes(PrintAttributes attributes) {
    256             synchronized (mLock) {
    257                 throwIfNotOpened();
    258                 if (DEBUG) {
    259                     Log.i(LOG_TAG, "applyPrintAttributes()");
    260                 }
    261 
    262                 Rect mediaBox = new Rect();
    263                 Rect cropBox = new Rect();
    264                 Matrix transform = new Matrix();
    265 
    266                 final boolean contentPortrait = attributes.getMediaSize().isPortrait();
    267 
    268                 final boolean layoutDirectionRtl = getResources().getConfiguration()
    269                         .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
    270 
    271                 // We do not want to rotate the media box, so take into account orientation.
    272                 final int dstWidthPts = contentPortrait
    273                         ? pointsFromMils(attributes.getMediaSize().getWidthMils())
    274                         : pointsFromMils(attributes.getMediaSize().getHeightMils());
    275                 final int dstHeightPts = contentPortrait
    276                         ? pointsFromMils(attributes.getMediaSize().getHeightMils())
    277                         : pointsFromMils(attributes.getMediaSize().getWidthMils());
    278 
    279                 final boolean scaleForPrinting = mEditor.shouldScaleForPrinting();
    280 
    281                 final int pageCount = mEditor.getPageCount();
    282                 for (int i = 0; i < pageCount; i++) {
    283                     if (!mEditor.getPageMediaBox(i, mediaBox)) {
    284                         Log.e(LOG_TAG, "Malformed PDF file");
    285                         return;
    286                     }
    287 
    288                     final int srcWidthPts = mediaBox.width();
    289                     final int srcHeightPts = mediaBox.height();
    290 
    291                     // Update the media box with the desired size.
    292                     mediaBox.right = dstWidthPts;
    293                     mediaBox.bottom = dstHeightPts;
    294                     mEditor.setPageMediaBox(i, mediaBox);
    295 
    296                     // Make sure content is top-left after media box resize.
    297                     transform.setTranslate(0, srcHeightPts - dstHeightPts);
    298 
    299                     // Rotate the content if in landscape.
    300                     if (!contentPortrait) {
    301                         transform.postRotate(270);
    302                         transform.postTranslate(0, dstHeightPts);
    303                     }
    304 
    305                     // Scale the content if document allows it.
    306                     final float scale;
    307                     if (scaleForPrinting) {
    308                         if (contentPortrait) {
    309                             scale = Math.min((float) dstWidthPts / srcWidthPts,
    310                                     (float) dstHeightPts / srcHeightPts);
    311                             transform.postScale(scale, scale);
    312                         } else {
    313                             scale = Math.min((float) dstWidthPts / srcHeightPts,
    314                                     (float) dstHeightPts / srcWidthPts);
    315                             transform.postScale(scale, scale, mediaBox.left, mediaBox.bottom);
    316                         }
    317                     } else {
    318                         scale = 1.0f;
    319                     }
    320 
    321                     // Update the crop box relatively to the media box change, if needed.
    322                     if (mEditor.getPageCropBox(i, cropBox)) {
    323                         cropBox.left = (int) (cropBox.left * scale + 0.5f);
    324                         cropBox.top = (int) (cropBox.top * scale + 0.5f);
    325                         cropBox.right = (int) (cropBox.right * scale + 0.5f);
    326                         cropBox.bottom = (int) (cropBox.bottom * scale + 0.5f);
    327                         cropBox.intersect(mediaBox);
    328                         mEditor.setPageCropBox(i, cropBox);
    329                     }
    330 
    331                     // If in RTL mode put the content in the logical top-right corner.
    332                     if (layoutDirectionRtl) {
    333                         final float dx = contentPortrait
    334                                 ? dstWidthPts - (int) (srcWidthPts * scale + 0.5f) : 0;
    335                         final float dy = contentPortrait
    336                                 ? 0 : - (dstHeightPts - (int) (srcWidthPts * scale + 0.5f));
    337                         transform.postTranslate(dx, dy);
    338                     }
    339 
    340                     // Adjust the physical margins if needed.
    341                     Margins minMargins = attributes.getMinMargins();
    342                     final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils());
    343                     final int paddingTopPts = pointsFromMils(minMargins.getTopMils());
    344                     final int paddingRightPts = pointsFromMils(minMargins.getRightMils());
    345                     final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils());
    346 
    347                     Rect clip = new Rect(mediaBox);
    348                     clip.left += paddingLeftPts;
    349                     clip.top += paddingTopPts;
    350                     clip.right -= paddingRightPts;
    351                     clip.bottom -= paddingBottomPts;
    352 
    353                     // Apply the accumulated transforms.
    354                     mEditor.setTransformAndClip(i, transform, clip);
    355                 }
    356             }
    357         }
    358 
    359         @Override
    360         public void write(ParcelFileDescriptor destination) throws RemoteException {
    361             synchronized (mLock) {
    362                 try {
    363                     throwIfNotOpened();
    364                     if (DEBUG) {
    365                         Log.i(LOG_TAG, "write()");
    366                     }
    367                     mEditor.write(destination);
    368                 } catch (IOException | IllegalStateException e) {
    369                     IoUtils.closeQuietly(destination);
    370                     Log.e(LOG_TAG, "Error writing PDF to file.", e);
    371                     throw new RemoteException(e.toString());
    372                 }
    373             }
    374         }
    375 
    376         @Override
    377         public void closeDocument() {
    378             synchronized (mLock) {
    379                 throwIfNotOpened();
    380                 if (DEBUG) {
    381                     Log.i(LOG_TAG, "closeDocument()");
    382                 }
    383                 mEditor.close();
    384                 mEditor = null;
    385             }
    386         }
    387 
    388         private void throwIfOpened() {
    389             if (mEditor != null) {
    390                 throw new IllegalStateException("Already opened");
    391             }
    392         }
    393 
    394         private void throwIfNotOpened() {
    395             if (mEditor == null) {
    396                 throw new IllegalStateException("Not opened");
    397             }
    398         }
    399     }
    400 
    401     private static int pointsFromMils(int mils) {
    402         return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH);
    403     }
    404 }
    405