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