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.3×11.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          */
onFinish()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      */
systemSupportsPrint()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      */
PrintHelper(@onNull Context context)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      */
setScaleMode(@caleMode int scaleMode)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
getScaleMode()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      */
setColorMode(@olorMode int colorMode)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
getColorMode()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      */
setOrientation(int orientation)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      */
getOrientation()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      */
printBitmap(@onNull String jobName, @NonNull Bitmap bitmap)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      */
printBitmap(@onNull final String jobName, @NonNull final Bitmap bitmap, @Nullable final OnPrintFinishCallback callback)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 
PrintBitmapAdapter(String jobName, int fittingMode, Bitmap bitmap, OnPrintFinishCallback callback)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
onLayout(PrintAttributes oldPrintAttributes, PrintAttributes newPrintAttributes, CancellationSignal cancellationSignal, LayoutResultCallback layoutResultCallback, Bundle bundle)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
onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, CancellationSignal cancellationSignal, WriteResultCallback writeResultCallback)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
onFinish()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      */
printBitmap(@onNull String jobName, @NonNull Uri imageFile)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      */
printBitmap(@onNull final String jobName, @NonNull final Uri imageFile, @Nullable final OnPrintFinishCallback callback)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 
PrintUriAdapter(String jobName, Uri imageFile, OnPrintFinishCallback callback, int fittingMode)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
onLayout(final PrintAttributes oldPrintAttributes, final PrintAttributes newPrintAttributes, final CancellationSignal cancellationSignal, final LayoutResultCallback layoutResultCallback, Bundle bundle)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 
cancelLoad()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
onFinish()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
onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, CancellationSignal cancellationSignal, WriteResultCallback writeResultCallback)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      */
isPortrait(Bitmap bitmap)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)
copyAttributes(PrintAttributes other)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      */
getMatrix(int imageWidth, int imageHeight, RectF content, @ScaleMode int fittingMode)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)
writeBitmap(final PrintAttributes attributes, final int fittingMode, final Bitmap bitmap, final ParcelFileDescriptor fileDescriptor, final CancellationSignal cancellationSignal, final PrintDocumentAdapter.WriteResultCallback writeResultCallback)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      */
loadConstrainedBitmap(Uri uri)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      */
loadBitmap(Uri uri, BitmapFactory.Options o)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 
convertBitmapForColorMode(Bitmap original, @ColorMode int colorMode)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