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