Home | History | Annotate | Download | only in print
      1 /*
      2  * Copyright 2018 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 androidx.print;
     18 
     19 import android.content.Context;
     20 import android.graphics.Bitmap;
     21 import android.graphics.BitmapFactory;
     22 import android.graphics.Canvas;
     23 import android.graphics.ColorMatrix;
     24 import android.graphics.ColorMatrixColorFilter;
     25 import android.graphics.Matrix;
     26 import android.graphics.Paint;
     27 import android.graphics.RectF;
     28 import android.graphics.pdf.PdfDocument;
     29 import android.net.Uri;
     30 import android.os.AsyncTask;
     31 import android.os.Build;
     32 import android.os.Bundle;
     33 import android.os.CancellationSignal;
     34 import android.os.ParcelFileDescriptor;
     35 import android.print.PageRange;
     36 import android.print.PrintAttributes;
     37 import android.print.PrintDocumentAdapter;
     38 import android.print.PrintDocumentInfo;
     39 import android.print.PrintManager;
     40 import android.print.pdf.PrintedPdfDocument;
     41 import android.util.Log;
     42 
     43 import androidx.annotation.IntDef;
     44 import androidx.annotation.NonNull;
     45 import androidx.annotation.Nullable;
     46 import androidx.annotation.RequiresApi;
     47 
     48 import java.io.FileNotFoundException;
     49 import java.io.FileOutputStream;
     50 import java.io.IOException;
     51 import java.io.InputStream;
     52 import java.lang.annotation.Retention;
     53 import java.lang.annotation.RetentionPolicy;
     54 
     55 /**
     56  * Helper for printing bitmaps.
     57  */
     58 public final class PrintHelper {
     59     private static final String LOG_TAG = "PrintHelper";
     60     // will be <= 300 dpi on A4 (8.311.7) paper (worst case of 150 dpi)
     61     private static final int MAX_PRINT_SIZE = 3500;
     62 
     63     /**
     64      * Whether the PrintActivity respects the suggested orientation.
     65      *
     66      * There is a bug in the PrintActivity that causes it to ignore the orientation
     67      */
     68     private static final boolean PRINT_ACTIVITY_RESPECTS_ORIENTATION =
     69             Build.VERSION.SDK_INT < 20 || Build.VERSION.SDK_INT > 23;
     70 
     71     /**
     72      * Whether the print subsystem handles min margins correctly. If not the print helper needs
     73      * to fake this.
     74      */
     75     private static final boolean IS_MIN_MARGINS_HANDLING_CORRECT = Build.VERSION.SDK_INT != 23;
     76 
     77     /**
     78      * image will be scaled but leave white space
     79      */
     80     public static final int SCALE_MODE_FIT = 1;
     81 
     82     /**
     83      * image will fill the paper and be cropped (default)
     84      */
     85     public static final int SCALE_MODE_FILL = 2;
     86 
     87     /**
     88      * this is a black and white image
     89      */
     90     public static final int COLOR_MODE_MONOCHROME = PrintAttributes.COLOR_MODE_MONOCHROME;
     91 
     92     /**
     93      * this is a color image (default)
     94      */
     95     public static final int COLOR_MODE_COLOR = PrintAttributes.COLOR_MODE_COLOR;
     96 
     97     /**
     98      * Print the image in landscape orientation (default).
     99      */
    100     public static final int ORIENTATION_LANDSCAPE = 1;
    101 
    102     /**
    103      * Print the image in  portrait orientation.
    104      */
    105     public static final int ORIENTATION_PORTRAIT = 2;
    106 
    107     /**
    108      * Callback for observing when a print operation is completed.
    109      * When print is finished either the system acquired the
    110      * document to print or printing was cancelled.
    111      */
    112     public interface OnPrintFinishCallback {
    113         /**
    114          * Called when a print operation is finished.
    115          */
    116         void onFinish();
    117     }
    118 
    119     @IntDef({SCALE_MODE_FIT, SCALE_MODE_FILL})
    120     @Retention(RetentionPolicy.SOURCE)
    121     private @interface ScaleMode {}
    122 
    123     @IntDef({COLOR_MODE_MONOCHROME, COLOR_MODE_COLOR})
    124     @Retention(RetentionPolicy.SOURCE)
    125     private @interface ColorMode {}
    126 
    127     @IntDef({ORIENTATION_LANDSCAPE, ORIENTATION_PORTRAIT})
    128     @Retention(RetentionPolicy.SOURCE)
    129     private @interface Orientation {}
    130 
    131     private final Context mContext;
    132 
    133     BitmapFactory.Options mDecodeOptions = null;
    134     private final Object mLock = new Object();
    135 
    136     @ScaleMode int mScaleMode = SCALE_MODE_FILL;
    137     @ColorMode int mColorMode = COLOR_MODE_COLOR;
    138     @Orientation int mOrientation = ORIENTATION_LANDSCAPE;
    139 
    140     /**
    141      * Gets whether the system supports printing.
    142      *
    143      * @return True if printing is supported.
    144      */
    145     public static boolean systemSupportsPrint() {
    146         // Supported on Android 4.4 or later.
    147         return Build.VERSION.SDK_INT >= 19;
    148     }
    149 
    150     /**
    151      * Constructs the PrintHelper that can be used to print images.
    152      *
    153      * @param context A context for accessing system resources.
    154      */
    155     public PrintHelper(@NonNull Context context) {
    156         mContext = context;
    157     }
    158 
    159     /**
    160      * Selects whether the image will fill the paper and be cropped
    161      * {@link #SCALE_MODE_FIT}
    162      * or whether the image will be scaled but leave white space
    163      * {@link #SCALE_MODE_FILL}.
    164      *
    165      * @param scaleMode {@link #SCALE_MODE_FIT} or
    166      *                  {@link #SCALE_MODE_FILL}
    167      */
    168     public void setScaleMode(@ScaleMode int scaleMode) {
    169         mScaleMode = scaleMode;
    170     }
    171 
    172     /**
    173      * Returns the scale mode with which the image will fill the paper.
    174      *
    175      * @return The scale Mode: {@link #SCALE_MODE_FIT} or
    176      * {@link #SCALE_MODE_FILL}
    177      */
    178     @ScaleMode
    179     public int getScaleMode() {
    180         return mScaleMode;
    181     }
    182 
    183     /**
    184      * Sets whether the image will be printed in color (default)
    185      * {@link #COLOR_MODE_COLOR} or in back and white
    186      * {@link #COLOR_MODE_MONOCHROME}.
    187      *
    188      * @param colorMode The color mode which is one of
    189      * {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}.
    190      */
    191     public void setColorMode(@ColorMode int colorMode) {
    192         mColorMode = colorMode;
    193     }
    194 
    195     /**
    196      * Gets the color mode with which the image will be printed.
    197      *
    198      * @return The color mode which is one of {@link #COLOR_MODE_COLOR}
    199      * and {@link #COLOR_MODE_MONOCHROME}.
    200      */
    201     @ColorMode
    202     public int getColorMode() {
    203         return mColorMode;
    204     }
    205 
    206     /**
    207      * Sets whether the image will be printed in landscape {@link #ORIENTATION_LANDSCAPE} (default)
    208      * or portrait {@link #ORIENTATION_PORTRAIT}.
    209      *
    210      * @param orientation The page orientation which is one of
    211      *                    {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}.
    212      */
    213     public void setOrientation(int orientation) {
    214         mOrientation = orientation;
    215     }
    216 
    217     /**
    218      * Gets whether the image will be printed in landscape or portrait.
    219      *
    220      * @return The page orientation which is one of
    221      * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}.
    222      */
    223     public int getOrientation() {
    224         // Unset defaults to landscape but might turn image
    225         if (Build.VERSION.SDK_INT >= 19 && mOrientation == 0) {
    226             return ORIENTATION_LANDSCAPE;
    227         }
    228         return mOrientation;
    229     }
    230 
    231 
    232     /**
    233      * Prints a bitmap.
    234      *
    235      * @param jobName The print job name.
    236      * @param bitmap  The bitmap to print.
    237      */
    238     public void printBitmap(@NonNull String jobName, @NonNull Bitmap bitmap) {
    239         printBitmap(jobName, bitmap, null);
    240     }
    241 
    242     /**
    243      * Prints a bitmap.
    244      *
    245      * @param jobName The print job name.
    246      * @param bitmap  The bitmap to print.
    247      * @param callback Optional callback to observe when printing is finished.
    248      */
    249     public void printBitmap(@NonNull final String jobName, @NonNull final Bitmap bitmap,
    250             @Nullable final OnPrintFinishCallback callback) {
    251         if (Build.VERSION.SDK_INT < 19 || bitmap == null) {
    252             return;
    253         }
    254 
    255         PrintManager printManager =
    256                 (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
    257         PrintAttributes.MediaSize mediaSize;
    258         if (isPortrait(bitmap)) {
    259             mediaSize = PrintAttributes.MediaSize.UNKNOWN_PORTRAIT;
    260         } else {
    261             mediaSize = PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE;
    262         }
    263         PrintAttributes attr = new PrintAttributes.Builder()
    264                 .setMediaSize(mediaSize)
    265                 .setColorMode(mColorMode)
    266                 .build();
    267 
    268         printManager.print(jobName,
    269                 new PrintBitmapAdapter(jobName, mScaleMode, bitmap, callback), attr);
    270     }
    271 
    272     @RequiresApi(19)
    273     private class PrintBitmapAdapter extends PrintDocumentAdapter {
    274         private final String mJobName;
    275         private final int mFittingMode;
    276         private final Bitmap mBitmap;
    277         private final OnPrintFinishCallback mCallback;
    278         private PrintAttributes mAttributes;
    279 
    280         PrintBitmapAdapter(String jobName, int fittingMode, Bitmap bitmap,
    281                 OnPrintFinishCallback callback) {
    282             mJobName = jobName;
    283             mFittingMode = fittingMode;
    284             mBitmap = bitmap;
    285             mCallback = callback;
    286         }
    287 
    288         @Override
    289         public void onLayout(PrintAttributes oldPrintAttributes,
    290                 PrintAttributes newPrintAttributes,
    291                 CancellationSignal cancellationSignal,
    292                 LayoutResultCallback layoutResultCallback,
    293                 Bundle bundle) {
    294 
    295             mAttributes = newPrintAttributes;
    296 
    297             PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName)
    298                     .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
    299                     .setPageCount(1)
    300                     .build();
    301             boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
    302             layoutResultCallback.onLayoutFinished(info, changed);
    303         }
    304 
    305         @Override
    306         public void onWrite(PageRange[] pageRanges,
    307                 ParcelFileDescriptor fileDescriptor,
    308                 CancellationSignal cancellationSignal,
    309                 WriteResultCallback writeResultCallback) {
    310             writeBitmap(mAttributes, mFittingMode, mBitmap, fileDescriptor,
    311                     cancellationSignal, writeResultCallback);
    312         }
    313 
    314         @Override
    315         public void onFinish() {
    316             if (mCallback != null) {
    317                 mCallback.onFinish();
    318             }
    319         }
    320     }
    321 
    322     /**
    323      * Prints an image located at the Uri. Image types supported are those of
    324      * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream)
    325      * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)}
    326      *
    327      * @param jobName   The print job name.
    328      * @param imageFile The <code>Uri</code> pointing to an image to print.
    329      * @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image.
    330      */
    331     public void printBitmap(@NonNull String jobName, @NonNull Uri imageFile)
    332             throws FileNotFoundException {
    333         printBitmap(jobName, imageFile, null);
    334     }
    335 
    336     /**
    337      * Prints an image located at the Uri. Image types supported are those of
    338      * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream)
    339      * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)}
    340      *
    341      * @param jobName   The print job name.
    342      * @param imageFile The <code>Uri</code> pointing to an image to print.
    343      * @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image.
    344      * @param callback Optional callback to observe when printing is finished.
    345      */
    346     public void printBitmap(@NonNull final String jobName, @NonNull final Uri imageFile,
    347             @Nullable final OnPrintFinishCallback callback)
    348             throws FileNotFoundException {
    349         if (Build.VERSION.SDK_INT < 19) {
    350             return;
    351         }
    352 
    353         PrintDocumentAdapter printDocumentAdapter = new PrintUriAdapter(jobName, imageFile,
    354                 callback, mScaleMode);
    355 
    356         PrintManager printManager =
    357                 (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
    358         PrintAttributes.Builder builder = new PrintAttributes.Builder();
    359         builder.setColorMode(mColorMode);
    360 
    361         if (mOrientation == ORIENTATION_LANDSCAPE || mOrientation == 0) {
    362             builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE);
    363         } else if (mOrientation == ORIENTATION_PORTRAIT) {
    364             builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT);
    365         }
    366         PrintAttributes attr = builder.build();
    367 
    368         printManager.print(jobName, printDocumentAdapter, attr);
    369     }
    370 
    371     @RequiresApi(19)
    372     private class PrintUriAdapter extends PrintDocumentAdapter {
    373         private final String mJobName;
    374         private final Uri mImageFile;
    375         private final OnPrintFinishCallback mCallback;
    376         private final int mFittingMode;
    377         private PrintAttributes mAttributes;
    378         AsyncTask<Uri, Boolean, Bitmap> mLoadBitmap;
    379         Bitmap mBitmap;
    380 
    381         PrintUriAdapter(String jobName, Uri imageFile, OnPrintFinishCallback callback,
    382                 int fittingMode) {
    383             mJobName = jobName;
    384             mImageFile = imageFile;
    385             mCallback = callback;
    386             mFittingMode = fittingMode;
    387             mBitmap = null;
    388         }
    389 
    390         @Override
    391         public void onLayout(final PrintAttributes oldPrintAttributes,
    392                 final PrintAttributes newPrintAttributes,
    393                 final CancellationSignal cancellationSignal,
    394                 final LayoutResultCallback layoutResultCallback,
    395                 Bundle bundle) {
    396 
    397             synchronized (this) {
    398                 mAttributes = newPrintAttributes;
    399             }
    400 
    401             if (cancellationSignal.isCanceled()) {
    402                 layoutResultCallback.onLayoutCancelled();
    403                 return;
    404             }
    405             // we finished the load
    406             if (mBitmap != null) {
    407                 PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName)
    408                         .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
    409                         .setPageCount(1)
    410                         .build();
    411                 boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
    412                 layoutResultCallback.onLayoutFinished(info, changed);
    413                 return;
    414             }
    415 
    416             mLoadBitmap = new AsyncTask<Uri, Boolean, Bitmap>() {
    417                 @Override
    418                 protected void onPreExecute() {
    419                     // First register for cancellation requests.
    420                     cancellationSignal.setOnCancelListener(
    421                             new CancellationSignal.OnCancelListener() {
    422                                 @Override
    423                                 public void onCancel() { // on different thread
    424                                     cancelLoad();
    425                                     cancel(false);
    426                                 }
    427                             });
    428                 }
    429 
    430                 @Override
    431                 protected Bitmap doInBackground(Uri... uris) {
    432                     try {
    433                         return loadConstrainedBitmap(mImageFile);
    434                     } catch (FileNotFoundException e) {
    435                         /* ignore */
    436                     }
    437                     return null;
    438                 }
    439 
    440                 @Override
    441                 protected void onPostExecute(Bitmap bitmap) {
    442                     super.onPostExecute(bitmap);
    443 
    444                     // If orientation was not set by the caller, try to fit the bitmap on
    445                     // the current paper by potentially rotating the bitmap by 90 degrees.
    446                     if (bitmap != null
    447                             && (!PRINT_ACTIVITY_RESPECTS_ORIENTATION || mOrientation == 0)) {
    448                         PrintAttributes.MediaSize mediaSize;
    449 
    450                         synchronized (this) {
    451                             mediaSize = mAttributes.getMediaSize();
    452                         }
    453 
    454                         if (mediaSize != null) {
    455                             if (mediaSize.isPortrait() != isPortrait(bitmap)) {
    456                                 Matrix rotation = new Matrix();
    457 
    458                                 rotation.postRotate(90);
    459                                 bitmap = Bitmap.createBitmap(bitmap, 0, 0,
    460                                         bitmap.getWidth(), bitmap.getHeight(), rotation,
    461                                         true);
    462                             }
    463                         }
    464                     }
    465 
    466                     mBitmap = bitmap;
    467                     if (bitmap != null) {
    468                         PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName)
    469                                 .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
    470                                 .setPageCount(1)
    471                                 .build();
    472 
    473                         boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
    474 
    475                         layoutResultCallback.onLayoutFinished(info, changed);
    476 
    477                     } else {
    478                         layoutResultCallback.onLayoutFailed(null);
    479                     }
    480                     mLoadBitmap = null;
    481                 }
    482 
    483                 @Override
    484                 protected void onCancelled(Bitmap result) {
    485                     // Task was cancelled, report that.
    486                     layoutResultCallback.onLayoutCancelled();
    487                     mLoadBitmap = null;
    488                 }
    489             }.execute();
    490         }
    491 
    492         private void cancelLoad() {
    493             synchronized (mLock) { // prevent race with set null below
    494                 if (mDecodeOptions != null) {
    495                     mDecodeOptions.requestCancelDecode();
    496                     mDecodeOptions = null;
    497                 }
    498             }
    499         }
    500 
    501         @Override
    502         public void onFinish() {
    503             super.onFinish();
    504             cancelLoad();
    505             if (mLoadBitmap != null) {
    506                 mLoadBitmap.cancel(true);
    507             }
    508             if (mCallback != null) {
    509                 mCallback.onFinish();
    510             }
    511             if (mBitmap != null) {
    512                 mBitmap.recycle();
    513                 mBitmap = null;
    514             }
    515         }
    516 
    517         @Override
    518         public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor,
    519                 CancellationSignal cancellationSignal,
    520                 WriteResultCallback writeResultCallback) {
    521             writeBitmap(mAttributes, mFittingMode, mBitmap, fileDescriptor,
    522                     cancellationSignal, writeResultCallback);
    523         }
    524     }
    525 
    526     /**
    527      * Check if the supplied bitmap should best be printed on a portrait orientation paper.
    528      *
    529      * @param bitmap The bitmap to be printed.
    530      * @return true iff the picture should best be printed on a portrait orientation paper.
    531      */
    532     private static boolean isPortrait(Bitmap bitmap) {
    533         return bitmap.getWidth() <= bitmap.getHeight();
    534     }
    535 
    536     /**
    537      * Create a build with a copy from the other print attributes.
    538      *
    539      * @param other The other print attributes
    540      *
    541      * @return A builder that will build print attributes that match the other attributes
    542      */
    543     @RequiresApi(19)
    544     private static PrintAttributes.Builder copyAttributes(PrintAttributes other) {
    545         PrintAttributes.Builder b = (new PrintAttributes.Builder())
    546                 .setMediaSize(other.getMediaSize())
    547                 .setResolution(other.getResolution())
    548                 .setMinMargins(other.getMinMargins());
    549 
    550         if (other.getColorMode() != 0) {
    551             b.setColorMode(other.getColorMode());
    552         }
    553 
    554         if (Build.VERSION.SDK_INT >= 23) {
    555             if (other.getDuplexMode() != 0) {
    556                 b.setDuplexMode(other.getDuplexMode());
    557             }
    558         }
    559 
    560         return b;
    561     }
    562 
    563     /**
    564      * Calculates the transform the print an Image to fill the page
    565      *
    566      * @param imageWidth  with of bitmap
    567      * @param imageHeight height of bitmap
    568      * @param content     The output page dimensions
    569      * @param fittingMode The mode of fitting {@link #SCALE_MODE_FILL} vs
    570      *                    {@link #SCALE_MODE_FIT}
    571      * @return Matrix to be used in canvas.drawBitmap(bitmap, matrix, null) call
    572      */
    573     private static Matrix getMatrix(int imageWidth, int imageHeight, RectF content,
    574             @ScaleMode int fittingMode) {
    575         Matrix matrix = new Matrix();
    576 
    577         // Compute and apply scale to fill the page.
    578         float scale = content.width() / imageWidth;
    579         if (fittingMode == SCALE_MODE_FILL) {
    580             scale = Math.max(scale, content.height() / imageHeight);
    581         } else {
    582             scale = Math.min(scale, content.height() / imageHeight);
    583         }
    584         matrix.postScale(scale, scale);
    585 
    586         // Center the content.
    587         final float translateX = (content.width()
    588                 - imageWidth * scale) / 2;
    589         final float translateY = (content.height()
    590                 - imageHeight * scale) / 2;
    591         matrix.postTranslate(translateX, translateY);
    592         return matrix;
    593     }
    594 
    595     /**
    596      * Write a bitmap for a PDF document.
    597      *
    598      * @param attributes          The print attributes
    599      * @param fittingMode         How to fit the bitmap
    600      * @param bitmap              The bitmap to write
    601      * @param fileDescriptor      The file to write to
    602      * @param cancellationSignal  Signal cancelling operation
    603      * @param writeResultCallback Callback to call once written
    604      */
    605     @RequiresApi(19)
    606     private void writeBitmap(final PrintAttributes attributes, final int fittingMode,
    607             final Bitmap bitmap, final ParcelFileDescriptor fileDescriptor,
    608             final CancellationSignal cancellationSignal,
    609             final PrintDocumentAdapter.WriteResultCallback writeResultCallback) {
    610         final PrintAttributes pdfAttributes;
    611         if (IS_MIN_MARGINS_HANDLING_CORRECT) {
    612             pdfAttributes = attributes;
    613         } else {
    614             // If the handling of any margin != 0 is broken, strip the margins and add them to
    615             // the bitmap later
    616             pdfAttributes = copyAttributes(attributes)
    617                     .setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0)).build();
    618         }
    619 
    620         (new AsyncTask<Void, Void, Throwable>() {
    621             @Override
    622             protected Throwable doInBackground(Void... params) {
    623                 try {
    624                     if (cancellationSignal.isCanceled()) {
    625                         return null;
    626                     }
    627 
    628                     PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext,
    629                             pdfAttributes);
    630 
    631                     Bitmap maybeGrayscale = convertBitmapForColorMode(bitmap,
    632                             pdfAttributes.getColorMode());
    633 
    634                     if (cancellationSignal.isCanceled()) {
    635                         return null;
    636                     }
    637 
    638                     try {
    639                         PdfDocument.Page page = pdfDocument.startPage(1);
    640 
    641                         RectF contentRect;
    642                         if (IS_MIN_MARGINS_HANDLING_CORRECT) {
    643                             contentRect = new RectF(page.getInfo().getContentRect());
    644                         } else {
    645                             // Create dummy doc that has the margins to compute correctly sized
    646                             // content rectangle
    647                             PrintedPdfDocument dummyDocument = new PrintedPdfDocument(mContext,
    648                                     attributes);
    649                             PdfDocument.Page dummyPage = dummyDocument.startPage(1);
    650                             contentRect = new RectF(dummyPage.getInfo().getContentRect());
    651                             dummyDocument.finishPage(dummyPage);
    652                             dummyDocument.close();
    653                         }
    654 
    655                         // Resize bitmap
    656                         Matrix matrix = getMatrix(
    657                                 maybeGrayscale.getWidth(), maybeGrayscale.getHeight(),
    658                                 contentRect, fittingMode);
    659 
    660                         if (IS_MIN_MARGINS_HANDLING_CORRECT) {
    661                             // The pdfDocument takes care of the positioning and margins
    662                         } else {
    663                             // Move it to the correct position.
    664                             matrix.postTranslate(contentRect.left, contentRect.top);
    665 
    666                             // Cut off margins
    667                             page.getCanvas().clipRect(contentRect);
    668                         }
    669 
    670                         // Draw the bitmap.
    671                         page.getCanvas().drawBitmap(maybeGrayscale, matrix, null);
    672 
    673                         // Finish the page.
    674                         pdfDocument.finishPage(page);
    675 
    676                         if (cancellationSignal.isCanceled()) {
    677                             return null;
    678                         }
    679 
    680                         // Write the document.
    681                         pdfDocument.writeTo(
    682                                 new FileOutputStream(fileDescriptor.getFileDescriptor()));
    683                         return null;
    684                     } finally {
    685                         pdfDocument.close();
    686 
    687                         if (fileDescriptor != null) {
    688                             try {
    689                                 fileDescriptor.close();
    690                             } catch (IOException ioe) {
    691                                 // ignore
    692                             }
    693                         }
    694                         // If we created a new instance for grayscaling, then recycle it here.
    695                         if (maybeGrayscale != bitmap) {
    696                             maybeGrayscale.recycle();
    697                         }
    698                     }
    699                 } catch (Throwable t) {
    700                     return t;
    701                 }
    702             }
    703 
    704             @Override
    705             protected void onPostExecute(Throwable throwable) {
    706                 if (cancellationSignal.isCanceled()) {
    707                     // Cancelled.
    708                     writeResultCallback.onWriteCancelled();
    709                 } else if (throwable == null) {
    710                     // Done.
    711                     writeResultCallback.onWriteFinished(
    712                             new PageRange[] { PageRange.ALL_PAGES });
    713                 } else {
    714                     // Failed.
    715                     Log.e(LOG_TAG, "Error writing printed content", throwable);
    716                     writeResultCallback.onWriteFailed(null);
    717                 }
    718             }
    719         }).execute();
    720     }
    721 
    722     /**
    723      * Loads a bitmap while limiting its size
    724      *
    725      * @param uri           location of a valid image
    726      * @return the Bitmap
    727      * @throws FileNotFoundException if the Uri does not point to an image
    728      */
    729     private Bitmap loadConstrainedBitmap(Uri uri)
    730             throws FileNotFoundException {
    731         if (uri == null || mContext == null) {
    732             throw new IllegalArgumentException("bad argument to getScaledBitmap");
    733         }
    734         // Get width and height of stored bitmap
    735         BitmapFactory.Options opt = new BitmapFactory.Options();
    736         opt.inJustDecodeBounds = true;
    737         loadBitmap(uri, opt);
    738 
    739         int w = opt.outWidth;
    740         int h = opt.outHeight;
    741 
    742         // If bitmap cannot be decoded, return null
    743         if (w <= 0 || h <= 0) {
    744             return null;
    745         }
    746 
    747         // Find best downsampling size
    748         int imageSide = Math.max(w, h);
    749 
    750         int sampleSize = 1;
    751         while (imageSide > MAX_PRINT_SIZE) {
    752             imageSide >>>= 1;
    753             sampleSize <<= 1;
    754         }
    755 
    756         // Make sure sample size is reasonable
    757         if (sampleSize <= 0 || 0 >= (Math.min(w, h) / sampleSize)) {
    758             return null;
    759         }
    760         BitmapFactory.Options decodeOptions;
    761         synchronized (mLock) { // prevent race with set null below
    762             mDecodeOptions = new BitmapFactory.Options();
    763             mDecodeOptions.inMutable = true;
    764             mDecodeOptions.inSampleSize = sampleSize;
    765             decodeOptions = mDecodeOptions;
    766         }
    767         try {
    768             return loadBitmap(uri, decodeOptions);
    769         } finally {
    770             synchronized (mLock) {
    771                 mDecodeOptions = null;
    772             }
    773         }
    774     }
    775 
    776     /**
    777      * Returns the bitmap from the given uri loaded using the given options.
    778      * Returns null on failure.
    779      */
    780     private Bitmap loadBitmap(Uri uri, BitmapFactory.Options o) throws FileNotFoundException {
    781         if (uri == null || mContext == null) {
    782             throw new IllegalArgumentException("bad argument to loadBitmap");
    783         }
    784         InputStream is = null;
    785         try {
    786             is = mContext.getContentResolver().openInputStream(uri);
    787             return BitmapFactory.decodeStream(is, null, o);
    788         } finally {
    789             if (is != null) {
    790                 try {
    791                     is.close();
    792                 } catch (IOException t) {
    793                     Log.w(LOG_TAG, "close fail ", t);
    794                 }
    795             }
    796         }
    797     }
    798 
    799     private static Bitmap convertBitmapForColorMode(Bitmap original, @ColorMode int colorMode) {
    800         if (colorMode != COLOR_MODE_MONOCHROME) {
    801             return original;
    802         }
    803         // Create a grayscale bitmap
    804         Bitmap grayscale = Bitmap.createBitmap(original.getWidth(), original.getHeight(),
    805                 Bitmap.Config.ARGB_8888);
    806         Canvas c = new Canvas(grayscale);
    807         Paint p = new Paint();
    808         ColorMatrix cm = new ColorMatrix();
    809         cm.setSaturation(0);
    810         ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
    811         p.setColorFilter(f);
    812         c.drawBitmap(original, 0, 0, p);
    813         c.setBitmap(null);
    814 
    815         return grayscale;
    816     }
    817 }
    818