blob: 89cc9e73278af6cd0f9c4f99b4af7ce8768436b1 [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.v4.print;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.pdf.PdfDocument.Page;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentInfo;
import android.print.PrintManager;
import android.print.pdf.PrintedPdfDocument;
import android.util.Log;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Kitkat specific PrintManager API implementation.
*/
class PrintHelperKitkat {
private static final String LOG_TAG = "PrintHelperKitkat";
// will be <= 300 dpi on A4 (8.3×11.7) paper (worst case of 150 dpi)
private final static int MAX_PRINT_SIZE = 3500;
final Context mContext;
BitmapFactory.Options mDecodeOptions = null;
private final Object mLock = new Object();
/**
* image will be scaled but leave white space
*/
public static final int SCALE_MODE_FIT = 1;
/**
* image will fill the paper and be cropped (default)
*/
public static final int SCALE_MODE_FILL = 2;
/**
* select landscape (default)
*/
public static final int ORIENTATION_LANDSCAPE = 1;
/**
* select portrait
*/
public static final int ORIENTATION_PORTRAIT = 2;
/**
* this is a black and white image
*/
public static final int COLOR_MODE_MONOCHROME = 1;
/**
* this is a color image (default)
*/
public static final int COLOR_MODE_COLOR = 2;
int mScaleMode = SCALE_MODE_FILL;
int mColorMode = COLOR_MODE_COLOR;
int mOrientation = ORIENTATION_LANDSCAPE;
PrintHelperKitkat(Context context) {
mContext = context;
}
/**
* Selects whether the image will fill the paper and be cropped
* <p/>
* {@link #SCALE_MODE_FIT}
* or whether the image will be scaled but leave white space
* {@link #SCALE_MODE_FILL}.
*
* @param scaleMode {@link #SCALE_MODE_FIT} or
* {@link #SCALE_MODE_FILL}
*/
public void setScaleMode(int scaleMode) {
mScaleMode = scaleMode;
}
/**
* Returns the scale mode with which the image will fill the paper.
*
* @return The scale Mode: {@link #SCALE_MODE_FIT} or
* {@link #SCALE_MODE_FILL}
*/
public int getScaleMode() {
return mScaleMode;
}
/**
* Sets whether the image will be printed in color (default)
* {@link #COLOR_MODE_COLOR} or in back and white
* {@link #COLOR_MODE_MONOCHROME}.
*
* @param colorMode The color mode which is one of
* {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}.
*/
public void setColorMode(int colorMode) {
mColorMode = colorMode;
}
/**
* Sets whether to select landscape (default), {@link #ORIENTATION_LANDSCAPE}
* or portrait {@link #ORIENTATION_PORTRAIT}
* @param orientation The page orientation which is one of
* {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}.
*/
public void setOrientation(int orientation) {
mOrientation = orientation;
}
/**
* Gets the page orientation with which the image will be printed.
*
* @return The preferred orientation which is one of
* {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}
*/
public int getOrientation() {
return mOrientation;
}
/**
* Gets the color mode with which the image will be printed.
*
* @return The color mode which is one of {@link #COLOR_MODE_COLOR}
* and {@link #COLOR_MODE_MONOCHROME}.
*/
public int getColorMode() {
return mColorMode;
}
/**
* Prints a bitmap.
*
* @param jobName The print job name.
* @param bitmap The bitmap to print.
*/
public void printBitmap(final String jobName, final Bitmap bitmap) {
if (bitmap == null) {
return;
}
final int fittingMode = mScaleMode; // grab the fitting mode at time of call
PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
PrintAttributes.MediaSize mediaSize = PrintAttributes.MediaSize.UNKNOWN_PORTRAIT;
if (bitmap.getWidth() > bitmap.getHeight()) {
mediaSize = PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE;
}
PrintAttributes attr = new PrintAttributes.Builder()
.setMediaSize(mediaSize)
.setColorMode(mColorMode)
.build();
printManager.print(jobName,
new PrintDocumentAdapter() {
private PrintAttributes mAttributes;
@Override
public void onLayout(PrintAttributes oldPrintAttributes,
PrintAttributes newPrintAttributes,
CancellationSignal cancellationSignal,
LayoutResultCallback layoutResultCallback,
Bundle bundle) {
mAttributes = newPrintAttributes;
PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName)
.setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
.setPageCount(1)
.build();
boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
layoutResultCallback.onLayoutFinished(info, changed);
}
@Override
public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor,
CancellationSignal cancellationSignal,
WriteResultCallback writeResultCallback) {
PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext,
mAttributes);
try {
Page page = pdfDocument.startPage(1);
RectF content = new RectF(page.getInfo().getContentRect());
Matrix matrix = getMatrix(bitmap.getWidth(), bitmap.getHeight(),
content, fittingMode);
// Draw the bitmap.
page.getCanvas().drawBitmap(bitmap, matrix, null);
// Finish the page.
pdfDocument.finishPage(page);
try {
// Write the document.
pdfDocument.writeTo(new FileOutputStream(
fileDescriptor.getFileDescriptor()));
// Done.
writeResultCallback.onWriteFinished(
new PageRange[]{PageRange.ALL_PAGES});
} catch (IOException ioe) {
// Failed.
Log.e(LOG_TAG, "Error writing printed content", ioe);
writeResultCallback.onWriteFailed(null);
}
} finally {
if (pdfDocument != null) {
pdfDocument.close();
}
if (fileDescriptor != null) {
try {
fileDescriptor.close();
} catch (IOException ioe) {
/* ignore */
}
}
}
}
}, attr);
}
/**
* Calculates the transform the print an Image to fill the page
*
* @param imageWidth with of bitmap
* @param imageHeight height of bitmap
* @param content The output page dimensions
* @param fittingMode The mode of fitting {@link #SCALE_MODE_FILL} vs {@link #SCALE_MODE_FIT}
* @return Matrix to be used in canvas.drawBitmap(bitmap, matrix, null) call
*/
private Matrix getMatrix(int imageWidth, int imageHeight, RectF content, int fittingMode) {
Matrix matrix = new Matrix();
// Compute and apply scale to fill the page.
float scale = content.width() / imageWidth;
if (fittingMode == SCALE_MODE_FILL) {
scale = Math.max(scale, content.height() / imageHeight);
} else {
scale = Math.min(scale, content.height() / imageHeight);
}
matrix.postScale(scale, scale);
// Center the content.
final float translateX = (content.width()
- imageWidth * scale) / 2;
final float translateY = (content.height()
- imageHeight * scale) / 2;
matrix.postTranslate(translateX, translateY);
return matrix;
}
/**
* Prints an image located at the Uri. Image types supported are those of
* <code>BitmapFactory.decodeStream</code> (JPEG, GIF, PNG, BMP, WEBP)
*
* @param jobName The print job name.
* @param imageFile The <code>Uri</code> pointing to an image to print.
* @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image.
*/
public void printBitmap(final String jobName, final Uri imageFile)
throws FileNotFoundException {
final int fittingMode = mScaleMode;
PrintDocumentAdapter printDocumentAdapter = new PrintDocumentAdapter() {
private PrintAttributes mAttributes;
AsyncTask<Uri, Boolean, Bitmap> loadBitmap;
Bitmap mBitmap = null;
@Override
public void onLayout(final PrintAttributes oldPrintAttributes,
final PrintAttributes newPrintAttributes,
final CancellationSignal cancellationSignal,
final LayoutResultCallback layoutResultCallback,
Bundle bundle) {
if (cancellationSignal.isCanceled()) {
layoutResultCallback.onLayoutCancelled();
mAttributes = newPrintAttributes;
return;
}
// we finished the load
if (mBitmap != null) {
PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName)
.setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
.setPageCount(1)
.build();
boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
layoutResultCallback.onLayoutFinished(info, changed);
return;
}
loadBitmap = new AsyncTask<Uri, Boolean, Bitmap>() {
@Override
protected void onPreExecute() {
// First register for cancellation requests.
cancellationSignal.setOnCancelListener(
new CancellationSignal.OnCancelListener() {
@Override
public void onCancel() { // on different thread
cancelLoad();
cancel(false);
}
});
}
@Override
protected Bitmap doInBackground(Uri... uris) {
try {
return loadConstrainedBitmap(imageFile, MAX_PRINT_SIZE);
} catch (FileNotFoundException e) {
/* ignore */
}
return null;
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
mBitmap = bitmap;
if (bitmap != null) {
PrintDocumentInfo info = new PrintDocumentInfo.Builder(jobName)
.setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
.setPageCount(1)
.build();
boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
layoutResultCallback.onLayoutFinished(info, changed);
} else {
layoutResultCallback.onLayoutFailed(null);
}
}
@Override
protected void onCancelled(Bitmap result) {
// Task was cancelled, report that.
layoutResultCallback.onLayoutCancelled();
}
};
loadBitmap.execute();
mAttributes = newPrintAttributes;
}
private void cancelLoad() {
synchronized (mLock) { // prevent race with set null below
if (mDecodeOptions != null) {
mDecodeOptions.requestCancelDecode();
mDecodeOptions = null;
}
}
}
@Override
public void onFinish() {
super.onFinish();
cancelLoad();
loadBitmap.cancel(true);
}
@Override
public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor,
CancellationSignal cancellationSignal,
WriteResultCallback writeResultCallback) {
PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext,
mAttributes);
try {
Page page = pdfDocument.startPage(1);
RectF content = new RectF(page.getInfo().getContentRect());
// Compute and apply scale to fill the page.
Matrix matrix = getMatrix(mBitmap.getWidth(), mBitmap.getHeight(),
content, fittingMode);
// Draw the bitmap.
page.getCanvas().drawBitmap(mBitmap, matrix, null);
// Finish the page.
pdfDocument.finishPage(page);
try {
// Write the document.
pdfDocument.writeTo(new FileOutputStream(
fileDescriptor.getFileDescriptor()));
// Done.
writeResultCallback.onWriteFinished(
new PageRange[]{PageRange.ALL_PAGES});
} catch (IOException ioe) {
// Failed.
Log.e(LOG_TAG, "Error writing printed content", ioe);
writeResultCallback.onWriteFailed(null);
}
} finally {
if (pdfDocument != null) {
pdfDocument.close();
}
if (fileDescriptor != null) {
try {
fileDescriptor.close();
} catch (IOException ioe) {
/* ignore */
}
}
}
}
};
PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
PrintAttributes.Builder builder = new PrintAttributes.Builder();
builder.setColorMode(mColorMode);
if (mOrientation == ORIENTATION_LANDSCAPE) {
builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE);
} else if (mOrientation == ORIENTATION_PORTRAIT) {
builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT);
}
PrintAttributes attr = builder.build();
printManager.print(jobName, printDocumentAdapter, attr);
}
/**
* Loads a bitmap while limiting its size
*
* @param uri location of a valid image
* @param maxSideLength the maximum length of a size
* @return the Bitmap
* @throws FileNotFoundException if the Uri does not point to an image
*/
private Bitmap loadConstrainedBitmap(Uri uri, int maxSideLength) throws FileNotFoundException {
if (maxSideLength <= 0 || uri == null || mContext == null) {
throw new IllegalArgumentException("bad argument to getScaledBitmap");
}
// Get width and height of stored bitmap
BitmapFactory.Options opt = new BitmapFactory.Options();
opt.inJustDecodeBounds = true;
loadBitmap(uri, opt);
int w = opt.outWidth;
int h = opt.outHeight;
// If bitmap cannot be decoded, return null
if (w <= 0 || h <= 0) {
return null;
}
// Find best downsampling size
int imageSide = Math.max(w, h);
int sampleSize = 1;
while (imageSide > maxSideLength) {
imageSide >>>= 1;
sampleSize <<= 1;
}
// Make sure sample size is reasonable
if (sampleSize <= 0 || 0 >= (int) (Math.min(w, h) / sampleSize)) {
return null;
}
BitmapFactory.Options decodeOptions = null;
synchronized (mLock) { // prevent race with set null below
mDecodeOptions = new BitmapFactory.Options();
mDecodeOptions.inMutable = true;
mDecodeOptions.inSampleSize = sampleSize;
decodeOptions = mDecodeOptions;
}
try {
return loadBitmap(uri, decodeOptions);
} finally {
synchronized (mLock) {
mDecodeOptions = null;
}
}
}
/**
* Returns the bitmap from the given uri loaded using the given options.
* Returns null on failure.
*/
private Bitmap loadBitmap(Uri uri, BitmapFactory.Options o) throws FileNotFoundException {
if (uri == null || mContext == null) {
throw new IllegalArgumentException("bad argument to loadBitmap");
}
InputStream is = null;
try {
is = mContext.getContentResolver().openInputStream(uri);
return BitmapFactory.decodeStream(is, null, o);
} finally {
if (is != null) {
try {
is.close();
} catch (IOException t) {
Log.w(LOG_TAG, "close fail ", t);
}
}
}
}
}