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