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