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