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.BitmapFactory; 22import android.graphics.Matrix; 23import android.graphics.RectF; 24import android.graphics.pdf.PdfDocument.Page; 25import android.net.Uri; 26import android.os.AsyncTask; 27import android.os.Bundle; 28import android.os.CancellationSignal; 29import android.os.ParcelFileDescriptor; 30import android.print.PageRange; 31import android.print.PrintAttributes; 32import android.print.PrintDocumentAdapter; 33import android.print.PrintDocumentInfo; 34import android.print.PrintManager; 35import android.print.pdf.PrintedPdfDocument; 36import android.util.Log; 37 38import java.io.FileNotFoundException; 39import java.io.FileOutputStream; 40import java.io.IOException; 41import java.io.InputStream; 42 43/** 44 * Kitkat specific PrintManager API implementation. 45 */ 46class PrintHelperKitkat { 47 private static final String LOG_TAG = "PrintHelperKitkat"; 48 // will be <= 300 dpi on A4 (8.3×11.7) paper (worst case of 150 dpi) 49 private final static int MAX_PRINT_SIZE = 3500; 50 final Context mContext; 51 BitmapFactory.Options mDecodeOptions = null; 52 private final Object mLock = new Object(); 53 /** 54 * image will be scaled but leave white space 55 */ 56 public static final int SCALE_MODE_FIT = 1; 57 /** 58 * image will fill the paper and be cropped (default) 59 */ 60 public static final int SCALE_MODE_FILL = 2; 61 62 /** 63 * select landscape (default) 64 */ 65 public static final int ORIENTATION_LANDSCAPE = 1; 66 67 /** 68 * select portrait 69 */ 70 public static final int ORIENTATION_PORTRAIT = 2; 71 72 /** 73 * this is a black and white image 74 */ 75 public static final int COLOR_MODE_MONOCHROME = 1; 76 /** 77 * this is a color image (default) 78 */ 79 public static final int COLOR_MODE_COLOR = 2; 80 81 public interface OnPrintFinishCallback { 82 public void onFinish(); 83 } 84 85 int mScaleMode = SCALE_MODE_FILL; 86 87 int mColorMode = COLOR_MODE_COLOR; 88 89 int mOrientation = ORIENTATION_LANDSCAPE; 90 91 PrintHelperKitkat(Context context) { 92 mContext = context; 93 } 94 95 /** 96 * Selects whether the image will fill the paper and be cropped 97 * <p/> 98 * {@link #SCALE_MODE_FIT} 99 * or whether the image will be scaled but leave white space 100 * {@link #SCALE_MODE_FILL}. 101 * 102 * @param scaleMode {@link #SCALE_MODE_FIT} or 103 * {@link #SCALE_MODE_FILL} 104 */ 105 public void setScaleMode(int scaleMode) { 106 mScaleMode = scaleMode; 107 } 108 109 /** 110 * Returns the scale mode with which the image will fill the paper. 111 * 112 * @return The scale Mode: {@link #SCALE_MODE_FIT} or 113 * {@link #SCALE_MODE_FILL} 114 */ 115 public int getScaleMode() { 116 return mScaleMode; 117 } 118 119 /** 120 * Sets whether the image will be printed in color (default) 121 * {@link #COLOR_MODE_COLOR} or in back and white 122 * {@link #COLOR_MODE_MONOCHROME}. 123 * 124 * @param colorMode The color mode which is one of 125 * {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}. 126 */ 127 public void setColorMode(int colorMode) { 128 mColorMode = colorMode; 129 } 130 131 /** 132 * Sets whether to select landscape (default), {@link #ORIENTATION_LANDSCAPE} 133 * or portrait {@link #ORIENTATION_PORTRAIT} 134 * @param orientation The page orientation which is one of 135 * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}. 136 */ 137 public void setOrientation(int orientation) { 138 mOrientation = orientation; 139 } 140 141 /** 142 * Gets the page orientation with which the image will be printed. 143 * 144 * @return The preferred orientation which is one of 145 * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT} 146 */ 147 public int getOrientation() { 148 return mOrientation; 149 } 150 151 /** 152 * Gets the color mode with which the image will be printed. 153 * 154 * @return The color mode which is one of {@link #COLOR_MODE_COLOR} 155 * and {@link #COLOR_MODE_MONOCHROME}. 156 */ 157 public int getColorMode() { 158 return mColorMode; 159 } 160 161 /** 162 * Prints a bitmap. 163 * 164 * @param jobName The print job name. 165 * @param bitmap The bitmap to print. 166 * @param callback Optional callback to observe when printing is finished. 167 */ 168 public void printBitmap(final String jobName, final Bitmap bitmap, 169 final OnPrintFinishCallback callback) { 170 if (bitmap == null) { 171 return; 172 } 173 final int fittingMode = mScaleMode; // grab the fitting mode at time of call 174 PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE); 175 PrintAttributes.MediaSize mediaSize = PrintAttributes.MediaSize.UNKNOWN_PORTRAIT; 176 if (bitmap.getWidth() > bitmap.getHeight()) { 177 mediaSize = PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE; 178 } 179 PrintAttributes attr = new PrintAttributes.Builder() 180 .setMediaSize(mediaSize) 181 .setColorMode(mColorMode) 182 .build(); 183 184 printManager.print(jobName, 185 new PrintDocumentAdapter() { 186 private PrintAttributes mAttributes; 187 188 @Override 189 public void onLayout(PrintAttributes oldPrintAttributes, 190 PrintAttributes newPrintAttributes, 191 CancellationSignal cancellationSignal, 192 LayoutResultCallback layoutResultCallback, 193 Bundle bundle) { 194 195 mAttributes = newPrintAttributes; 196 197 PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName) 198 .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) 199 .setPageCount(1) 200 .build(); 201 boolean changed = !newPrintAttributes.equals(oldPrintAttributes); 202 layoutResultCallback.onLayoutFinished(info, changed); 203 } 204 205 @Override 206 public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, 207 CancellationSignal cancellationSignal, 208 WriteResultCallback writeResultCallback) { 209 PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext, 210 mAttributes); 211 try { 212 Page page = pdfDocument.startPage(1); 213 214 RectF content = new RectF(page.getInfo().getContentRect()); 215 216 Matrix matrix = getMatrix(bitmap.getWidth(), bitmap.getHeight(), 217 content, fittingMode); 218 219 // Draw the bitmap. 220 page.getCanvas().drawBitmap(bitmap, matrix, null); 221 222 // Finish the page. 223 pdfDocument.finishPage(page); 224 225 try { 226 // Write the document. 227 pdfDocument.writeTo(new FileOutputStream( 228 fileDescriptor.getFileDescriptor())); 229 // Done. 230 writeResultCallback.onWriteFinished( 231 new PageRange[]{PageRange.ALL_PAGES}); 232 } catch (IOException ioe) { 233 // Failed. 234 Log.e(LOG_TAG, "Error writing printed content", ioe); 235 writeResultCallback.onWriteFailed(null); 236 } 237 } finally { 238 if (pdfDocument != null) { 239 pdfDocument.close(); 240 } 241 if (fileDescriptor != null) { 242 try { 243 fileDescriptor.close(); 244 } catch (IOException ioe) { 245 /* ignore */ 246 } 247 } 248 } 249 } 250 251 @Override 252 public void onFinish() { 253 if (callback != null) { 254 callback.onFinish(); 255 } 256 } 257 }, attr); 258 } 259 260 /** 261 * Calculates the transform the print an Image to fill the page 262 * 263 * @param imageWidth with of bitmap 264 * @param imageHeight height of bitmap 265 * @param content The output page dimensions 266 * @param fittingMode The mode of fitting {@link #SCALE_MODE_FILL} vs {@link #SCALE_MODE_FIT} 267 * @return Matrix to be used in canvas.drawBitmap(bitmap, matrix, null) call 268 */ 269 private Matrix getMatrix(int imageWidth, int imageHeight, RectF content, int fittingMode) { 270 Matrix matrix = new Matrix(); 271 272 // Compute and apply scale to fill the page. 273 float scale = content.width() / imageWidth; 274 if (fittingMode == SCALE_MODE_FILL) { 275 scale = Math.max(scale, content.height() / imageHeight); 276 } else { 277 scale = Math.min(scale, content.height() / imageHeight); 278 } 279 matrix.postScale(scale, scale); 280 281 // Center the content. 282 final float translateX = (content.width() 283 - imageWidth * scale) / 2; 284 final float translateY = (content.height() 285 - imageHeight * scale) / 2; 286 matrix.postTranslate(translateX, translateY); 287 return matrix; 288 } 289 290 /** 291 * Prints an image located at the Uri. Image types supported are those of 292 * <code>BitmapFactory.decodeStream</code> (JPEG, GIF, PNG, BMP, WEBP) 293 * 294 * @param jobName The print job name. 295 * @param imageFile The <code>Uri</code> pointing to an image to print. 296 * @param callback Optional callback to observe when printing is finished. 297 * @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image. 298 */ 299 public void printBitmap(final String jobName, final Uri imageFile, 300 final OnPrintFinishCallback callback) throws FileNotFoundException { 301 final int fittingMode = mScaleMode; 302 303 PrintDocumentAdapter printDocumentAdapter = new PrintDocumentAdapter() { 304 private PrintAttributes mAttributes; 305 AsyncTask<Uri, Boolean, Bitmap> mLoadBitmap; 306 Bitmap mBitmap = null; 307 308 @Override 309 public void onLayout(final PrintAttributes oldPrintAttributes, 310 final PrintAttributes newPrintAttributes, 311 final CancellationSignal cancellationSignal, 312 final LayoutResultCallback layoutResultCallback, 313 Bundle bundle) { 314 315 mAttributes = newPrintAttributes; 316 317 if (cancellationSignal.isCanceled()) { 318 layoutResultCallback.onLayoutCancelled(); 319 return; 320 } 321 // we finished the load 322 if (mBitmap != null) { 323 PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName) 324 .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) 325 .setPageCount(1) 326 .build(); 327 boolean changed = !newPrintAttributes.equals(oldPrintAttributes); 328 layoutResultCallback.onLayoutFinished(info, changed); 329 return; 330 } 331 332 mLoadBitmap = new AsyncTask<Uri, Boolean, Bitmap>() { 333 334 @Override 335 protected void onPreExecute() { 336 // First register for cancellation requests. 337 cancellationSignal.setOnCancelListener( 338 new CancellationSignal.OnCancelListener() { 339 @Override 340 public void onCancel() { // on different thread 341 cancelLoad(); 342 cancel(false); 343 } 344 }); 345 } 346 347 @Override 348 protected Bitmap doInBackground(Uri... uris) { 349 try { 350 return loadConstrainedBitmap(imageFile, MAX_PRINT_SIZE); 351 } catch (FileNotFoundException e) { 352 /* ignore */ 353 } 354 return null; 355 } 356 357 @Override 358 protected void onPostExecute(Bitmap bitmap) { 359 super.onPostExecute(bitmap); 360 mBitmap = bitmap; 361 if (bitmap != null) { 362 PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName) 363 .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO) 364 .setPageCount(1) 365 .build(); 366 boolean changed = !newPrintAttributes.equals(oldPrintAttributes); 367 368 layoutResultCallback.onLayoutFinished(info, changed); 369 370 } else { 371 layoutResultCallback.onLayoutFailed(null); 372 } 373 mLoadBitmap = null; 374 } 375 376 @Override 377 protected void onCancelled(Bitmap result) { 378 // Task was cancelled, report that. 379 layoutResultCallback.onLayoutCancelled(); 380 mLoadBitmap = null; 381 } 382 }.execute(); 383 } 384 385 private void cancelLoad() { 386 synchronized (mLock) { // prevent race with set null below 387 if (mDecodeOptions != null) { 388 mDecodeOptions.requestCancelDecode(); 389 mDecodeOptions = null; 390 } 391 } 392 } 393 394 @Override 395 public void onFinish() { 396 super.onFinish(); 397 cancelLoad(); 398 if (mLoadBitmap != null) { 399 mLoadBitmap.cancel(true); 400 } 401 if (callback != null) { 402 callback.onFinish(); 403 } 404 } 405 406 @Override 407 public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor, 408 CancellationSignal cancellationSignal, 409 WriteResultCallback writeResultCallback) { 410 PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext, 411 mAttributes); 412 try { 413 414 Page page = pdfDocument.startPage(1); 415 RectF content = new RectF(page.getInfo().getContentRect()); 416 417 // Compute and apply scale to fill the page. 418 Matrix matrix = getMatrix(mBitmap.getWidth(), mBitmap.getHeight(), 419 content, fittingMode); 420 421 // Draw the bitmap. 422 page.getCanvas().drawBitmap(mBitmap, matrix, null); 423 424 // Finish the page. 425 pdfDocument.finishPage(page); 426 427 try { 428 // Write the document. 429 pdfDocument.writeTo(new FileOutputStream( 430 fileDescriptor.getFileDescriptor())); 431 // Done. 432 writeResultCallback.onWriteFinished( 433 new PageRange[]{PageRange.ALL_PAGES}); 434 } catch (IOException ioe) { 435 // Failed. 436 Log.e(LOG_TAG, "Error writing printed content", ioe); 437 writeResultCallback.onWriteFailed(null); 438 } 439 } finally { 440 if (pdfDocument != null) { 441 pdfDocument.close(); 442 } 443 if (fileDescriptor != null) { 444 try { 445 fileDescriptor.close(); 446 } catch (IOException ioe) { 447 /* ignore */ 448 } 449 } 450 } 451 } 452 }; 453 454 PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE); 455 PrintAttributes.Builder builder = new PrintAttributes.Builder(); 456 builder.setColorMode(mColorMode); 457 458 if (mOrientation == ORIENTATION_LANDSCAPE) { 459 builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE); 460 } else if (mOrientation == ORIENTATION_PORTRAIT) { 461 builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT); 462 } 463 PrintAttributes attr = builder.build(); 464 465 printManager.print(jobName, printDocumentAdapter, attr); 466 } 467 468 /** 469 * Loads a bitmap while limiting its size 470 * 471 * @param uri location of a valid image 472 * @param maxSideLength the maximum length of a size 473 * @return the Bitmap 474 * @throws FileNotFoundException if the Uri does not point to an image 475 */ 476 private Bitmap loadConstrainedBitmap(Uri uri, int maxSideLength) throws FileNotFoundException { 477 if (maxSideLength <= 0 || uri == null || mContext == null) { 478 throw new IllegalArgumentException("bad argument to getScaledBitmap"); 479 } 480 // Get width and height of stored bitmap 481 BitmapFactory.Options opt = new BitmapFactory.Options(); 482 opt.inJustDecodeBounds = true; 483 loadBitmap(uri, opt); 484 485 int w = opt.outWidth; 486 int h = opt.outHeight; 487 488 // If bitmap cannot be decoded, return null 489 if (w <= 0 || h <= 0) { 490 return null; 491 } 492 493 // Find best downsampling size 494 int imageSide = Math.max(w, h); 495 496 int sampleSize = 1; 497 while (imageSide > maxSideLength) { 498 imageSide >>>= 1; 499 sampleSize <<= 1; 500 } 501 502 // Make sure sample size is reasonable 503 if (sampleSize <= 0 || 0 >= (int) (Math.min(w, h) / sampleSize)) { 504 return null; 505 } 506 BitmapFactory.Options decodeOptions = null; 507 synchronized (mLock) { // prevent race with set null below 508 mDecodeOptions = new BitmapFactory.Options(); 509 mDecodeOptions.inMutable = true; 510 mDecodeOptions.inSampleSize = sampleSize; 511 decodeOptions = mDecodeOptions; 512 } 513 try { 514 return loadBitmap(uri, decodeOptions); 515 } finally { 516 synchronized (mLock) { 517 mDecodeOptions = null; 518 } 519 } 520 } 521 522 /** 523 * Returns the bitmap from the given uri loaded using the given options. 524 * Returns null on failure. 525 */ 526 private Bitmap loadBitmap(Uri uri, BitmapFactory.Options o) throws FileNotFoundException { 527 if (uri == null || mContext == null) { 528 throw new IllegalArgumentException("bad argument to loadBitmap"); 529 } 530 InputStream is = null; 531 try { 532 is = mContext.getContentResolver().openInputStream(uri); 533 return BitmapFactory.decodeStream(is, null, o); 534 } finally { 535 if (is != null) { 536 try { 537 is.close(); 538 } catch (IOException t) { 539 Log.w(LOG_TAG, "close fail ", t); 540 } 541 } 542 } 543 } 544}