blob: 6e2149edc42b1719562da4e8f07ff99933092ec6 [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.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Matrix;
import android.graphics.Paint;
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.PrintAttributes.MediaSize;
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;
public interface OnPrintFinishCallback {
public void onFinish();
}
/**
* Whether the PrintActivity respects the suggested orientation
*/
protected boolean mPrintActivityRespectsOrientation;
/**
* Whether the print subsystem handles min margins correctly. If not the print helper needs to
* fake this.
*/
protected boolean mIsMinMarginsHandlingCorrect;
int mScaleMode = SCALE_MODE_FILL;
int mColorMode = COLOR_MODE_COLOR;
int mOrientation;
PrintHelperKitkat(Context context) {
mPrintActivityRespectsOrientation = true;
mIsMinMarginsHandlingCorrect = true;
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() {
/// Unset defaults to landscape but might turn image
if (mOrientation == 0) {
return ORIENTATION_LANDSCAPE;
}
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;
}
/**
* Check if the supplied bitmap should best be printed on a portrait orientation paper.
*
* @param bitmap The bitmap to be printed.
* @return true iff the picture should best be printed on a portrait orientation paper.
*/
private static boolean isPortrait(Bitmap bitmap) {
if (bitmap.getWidth() <= bitmap.getHeight()) {
return true;
} else {
return false;
}
}
/**
* Create a build with a copy from the other print attributes.
*
* @param other The other print attributes
*
* @return A builder that will build print attributes that match the other attributes
*/
protected PrintAttributes.Builder copyAttributes(PrintAttributes other) {
PrintAttributes.Builder b = (new PrintAttributes.Builder())
.setMediaSize(other.getMediaSize())
.setResolution(other.getResolution())
.setMinMargins(other.getMinMargins());
if (other.getColorMode() != 0) {
b.setColorMode(other.getColorMode());
}
return b;
}
/**
* Prints a bitmap.
*
* @param jobName The print job name.
* @param bitmap The bitmap to print.
* @param callback Optional callback to observe when printing is finished.
*/
public void printBitmap(final String jobName, final Bitmap bitmap,
final OnPrintFinishCallback callback) {
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;
if (isPortrait(bitmap)) {
mediaSize = PrintAttributes.MediaSize.UNKNOWN_PORTRAIT;
} else {
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) {
writeBitmap(mAttributes, fittingMode, bitmap, fileDescriptor,
cancellationSignal, writeResultCallback);
}
@Override
public void onFinish() {
if (callback != null) {
callback.onFinish();
}
}
}, 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;
}
/**
* Write a bitmap for a PDF document.
*
* @param attributes The print attributes
* @param fittingMode How to fit the bitmap
* @param bitmap The bitmap to write
* @param fileDescriptor The file to write to
* @param cancellationSignal Signal cancelling operation
* @param writeResultCallback Callback to call once written
*/
private void writeBitmap(final PrintAttributes attributes, final int fittingMode,
final Bitmap bitmap, final ParcelFileDescriptor fileDescriptor,
final CancellationSignal cancellationSignal,
final PrintDocumentAdapter.WriteResultCallback writeResultCallback) {
final PrintAttributes pdfAttributes;
if (mIsMinMarginsHandlingCorrect) {
pdfAttributes = attributes;
} else {
// If the handling of any margin != 0 is broken, strip the margins and add them to the
// bitmap later
pdfAttributes = copyAttributes(attributes)
.setMinMargins(new PrintAttributes.Margins(0,0,0,0)).build();
}
(new AsyncTask<Void, Void, Throwable>() {
@Override
protected Throwable doInBackground(Void... params) {
try {
if (cancellationSignal.isCanceled()) {
return null;
}
PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext,
pdfAttributes);
Bitmap maybeGrayscale = convertBitmapForColorMode(bitmap,
pdfAttributes.getColorMode());
if (cancellationSignal.isCanceled()) {
return null;
}
try {
Page page = pdfDocument.startPage(1);
RectF contentRect;
if (mIsMinMarginsHandlingCorrect) {
contentRect = new RectF(page.getInfo().getContentRect());
} else {
// Create dummy doc that has the margins to compute correctly sized
// content rectangle
PrintedPdfDocument dummyDocument = new PrintedPdfDocument(mContext,
attributes);
Page dummyPage = dummyDocument.startPage(1);
contentRect = new RectF(dummyPage.getInfo().getContentRect());
dummyDocument.finishPage(dummyPage);
dummyDocument.close();
}
// Resize bitmap
Matrix matrix = getMatrix(
maybeGrayscale.getWidth(), maybeGrayscale.getHeight(),
contentRect, fittingMode);
if (mIsMinMarginsHandlingCorrect) {
// The pdfDocument takes care of the positioning and margins
} else {
// Move it to the correct position.
matrix.postTranslate(contentRect.left, contentRect.top);
// Cut off margins
page.getCanvas().clipRect(contentRect);
}
// Draw the bitmap.
page.getCanvas().drawBitmap(maybeGrayscale, matrix, null);
// Finish the page.
pdfDocument.finishPage(page);
if (cancellationSignal.isCanceled()) {
return null;
}
// Write the document.
pdfDocument
.writeTo(new FileOutputStream(fileDescriptor.getFileDescriptor()));
return null;
} finally {
pdfDocument.close();
if (fileDescriptor != null) {
try {
fileDescriptor.close();
} catch (IOException ioe) {
// ignore
}
}
// If we created a new instance for grayscaling, then recycle it here.
if (maybeGrayscale != bitmap) {
maybeGrayscale.recycle();
}
}
} catch (Throwable t) {
return t;
}
}
@Override
protected void onPostExecute(Throwable throwable) {
if (cancellationSignal.isCanceled()) {
// Cancelled.
writeResultCallback.onWriteCancelled();
} else if (throwable == null) {
// Done.
writeResultCallback.onWriteFinished(new PageRange[] { PageRange.ALL_PAGES });
} else {
// Failed.
Log.e(LOG_TAG, "Error writing printed content", throwable);
writeResultCallback.onWriteFailed(null);
}
}
}).execute();
}
/**
* 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.
* @param callback Optional callback to observe when printing is finished.
* @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image.
*/
public void printBitmap(final String jobName, final Uri imageFile,
final OnPrintFinishCallback callback) throws FileNotFoundException {
final int fittingMode = mScaleMode;
PrintDocumentAdapter printDocumentAdapter = new PrintDocumentAdapter() {
private PrintAttributes mAttributes;
AsyncTask<Uri, Boolean, Bitmap> mLoadBitmap;
Bitmap mBitmap = null;
@Override
public void onLayout(final PrintAttributes oldPrintAttributes,
final PrintAttributes newPrintAttributes,
final CancellationSignal cancellationSignal,
final LayoutResultCallback layoutResultCallback,
Bundle bundle) {
synchronized (this) {
mAttributes = newPrintAttributes;
}
if (cancellationSignal.isCanceled()) {
layoutResultCallback.onLayoutCancelled();
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;
}
mLoadBitmap = 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);
// If orientation was not set by the caller, try to fit the bitmap on
// the current paper by potentially rotating the bitmap by 90 degrees.
if (bitmap != null
&& (!mPrintActivityRespectsOrientation || mOrientation == 0)) {
MediaSize mediaSize;
synchronized (this) {
mediaSize = mAttributes.getMediaSize();
}
if (mediaSize != null) {
if (mediaSize.isPortrait() != isPortrait(bitmap)) {
Matrix rotation = new Matrix();
rotation.postRotate(90);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), rotation, true);
}
}
}
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);
}
mLoadBitmap = null;
}
@Override
protected void onCancelled(Bitmap result) {
// Task was cancelled, report that.
layoutResultCallback.onLayoutCancelled();
mLoadBitmap = null;
}
}.execute();
}
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();
if (mLoadBitmap != null) {
mLoadBitmap.cancel(true);
}
if (callback != null) {
callback.onFinish();
}
if (mBitmap != null) {
mBitmap.recycle();
mBitmap = null;
}
}
@Override
public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor,
CancellationSignal cancellationSignal,
WriteResultCallback writeResultCallback) {
writeBitmap(mAttributes, fittingMode, mBitmap, fileDescriptor, cancellationSignal,
writeResultCallback);
}
};
PrintManager printManager = (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
PrintAttributes.Builder builder = new PrintAttributes.Builder();
builder.setColorMode(mColorMode);
if (mOrientation == ORIENTATION_LANDSCAPE || mOrientation == 0) {
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);
}
}
}
}
private Bitmap convertBitmapForColorMode(Bitmap original, int colorMode) {
if (colorMode != COLOR_MODE_MONOCHROME) {
return original;
}
// Create a grayscale bitmap
Bitmap grayscale = Bitmap.createBitmap(original.getWidth(), original.getHeight(),
Config.ARGB_8888);
Canvas c = new Canvas(grayscale);
Paint p = new Paint();
ColorMatrix cm = new ColorMatrix();
cm.setSaturation(0);
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
p.setColorFilter(f);
c.drawBitmap(original, 0, 0, p);
c.setBitmap(null);
return grayscale;
}
}