1 /*
2  * Copyright (C) 2020 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 
17 package com.android.bips;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.Canvas;
22 import android.graphics.Matrix;
23 import android.graphics.Paint;
24 import android.graphics.RectF;
25 import android.graphics.pdf.PdfDocument;
26 import android.os.AsyncTask;
27 import android.os.CancellationSignal;
28 import android.os.ParcelFileDescriptor;
29 import android.print.PrintAttributes;
30 import android.print.pdf.PrintedPdfDocument;
31 import android.util.Log;
32 
33 import java.io.FileOutputStream;
34 
35 /**
36  * A background task that optimizes a {@link Bitmap}, renders it into a PDF, and delivers the PDF
37  * to a {@link ParcelFileDescriptor}.
38  */
39 class ImageToPdfTask extends AsyncTask<ParcelFileDescriptor, Void, Throwable> {
40     private static final String TAG = ImageToPdfTask.class.getSimpleName();
41     private static final boolean DEBUG = false;
42     private static final float POINTS_PER_INCH = 72;
43 
44     private final PrintedPdfDocument mDocument;
45     private final Bitmap mBitmap;
46     private final PrintAttributes mAttributes;
47     private final int mDpi;
48     private final CancellationSignal mCancellationSignal;
49 
ImageToPdfTask(Context context, Bitmap bitmap, PrintAttributes attributes, int dpi, CancellationSignal cancellationSignal)50     ImageToPdfTask(Context context, Bitmap bitmap, PrintAttributes attributes, int dpi,
51                    CancellationSignal cancellationSignal) {
52         mBitmap = bitmap;
53         mAttributes = attributes;
54         mCancellationSignal = cancellationSignal;
55         mDpi = dpi;
56         mDocument = new PrintedPdfDocument(context, mAttributes);
57     }
58 
59     @Override
doInBackground(ParcelFileDescriptor... outputs)60     protected Throwable doInBackground(ParcelFileDescriptor... outputs) {
61         try (ParcelFileDescriptor output = outputs[0]) {
62             if (DEBUG) Log.d(TAG, "creating document at dpi=" + mDpi);
63             writeBitmapToDocument();
64             mCancellationSignal.throwIfCanceled();
65             if (DEBUG) Log.d(TAG, "writing to output stream");
66             mDocument.writeTo(new FileOutputStream(output.getFileDescriptor()));
67             mDocument.close();
68             if (DEBUG) Log.d(TAG, "finished sending");
69             return null;
70         } catch (Throwable t) {
71             return t;
72         }
73     }
74 
75     /** Create a one-page PDF document containing the bitmap */
writeBitmapToDocument()76     private void writeBitmapToDocument() {
77         PdfDocument.Page page = mDocument.startPage(1);
78         if (mAttributes.getMediaSize().isPortrait() == mBitmap.getWidth() < mBitmap.getHeight()) {
79             writeBitmapToPage(page, true);
80         } else {
81             // If user selects the opposite orientation, fit instead of fill.
82             writeBitmapToPage(page, false);
83         }
84         mDocument.finishPage(page);
85     }
86 
writeBitmapToPage(PdfDocument.Page page, boolean fill)87     private void writeBitmapToPage(PdfDocument.Page page, boolean fill) {
88         RectF extent = new RectF(page.getInfo().getContentRect());
89         float scale;
90         boolean rotate;
91         if (fill) {
92             // Fill the entire page with image data
93             scale = Math.max(extent.height() / POINTS_PER_INCH * mDpi / mBitmap.getHeight(),
94                 extent.width() / POINTS_PER_INCH * mDpi / mBitmap.getWidth());
95             rotate = false;
96         } else {
97             // Scale and rotate the image to fit entirely on the page
98             scale = Math.min(extent.height() / POINTS_PER_INCH * mDpi / mBitmap.getWidth(),
99                 extent.width() / POINTS_PER_INCH * mDpi / mBitmap.getHeight());
100             rotate = true;
101         }
102 
103         if (scale >= 1) {
104             // Image will need to be scaled up
105             drawDirect(page, extent, fill, rotate);
106         } else {
107             // Scale image down to the size needed for printing
108             drawOptimized(page, extent, scale, rotate);
109         }
110     }
111 
112     /**
113      * Render the source bitmap directly into the PDF
114      */
drawDirect(PdfDocument.Page page, RectF extent, boolean fill, boolean rotate)115     private void drawDirect(PdfDocument.Page page, RectF extent, boolean fill, boolean rotate) {
116         float scale;
117         if (fill) {
118             scale = Math.max(extent.height() / mBitmap.getHeight(),
119                 extent.width() / mBitmap.getWidth());
120         } else {
121             scale = Math.min(extent.height() / mBitmap.getWidth(),
122                 extent.width() / mBitmap.getHeight());
123         }
124 
125         float offsetX = (extent.width() - mBitmap.getWidth() * scale) / 2;
126         float offsetY = (extent.height() - mBitmap.getHeight() * scale) / 2;
127 
128         Matrix matrix = new Matrix();
129         if (rotate) {
130             matrix.postRotate(90, mBitmap.getWidth() / 2, mBitmap.getHeight() / 2);
131         }
132         matrix.postScale(scale, scale);
133         matrix.postTranslate(offsetX, offsetY);
134         page.getCanvas().clipRect(extent);
135         page.getCanvas().drawBitmap(mBitmap, matrix, new Paint(Paint.FILTER_BITMAP_FLAG));
136     }
137 
138     /**
139      * Scale down the bitmap to specific DPI to reduce delivered PDF size
140      */
drawOptimized(PdfDocument.Page page, RectF extent, float scale, boolean rotate)141     private void drawOptimized(PdfDocument.Page page, RectF extent, float scale, boolean rotate) {
142         float targetWidth = (extent.width() / POINTS_PER_INCH * mDpi);
143         float targetHeight = (extent.height() / POINTS_PER_INCH * mDpi);
144         float offsetX = ((targetWidth / scale) - mBitmap.getWidth()) / 2;
145         float offsetY = ((targetHeight / scale) - mBitmap.getHeight()) / 2;
146 
147         Bitmap targetBitmap = Bitmap.createBitmap((int) targetWidth, (int) targetHeight,
148                 Bitmap.Config.ARGB_8888);
149         Canvas bitmapCanvas = new Canvas(targetBitmap);
150         Matrix matrix = new Matrix();
151         matrix.postScale(scale, scale);
152         if (rotate) {
153             matrix.postRotate(90, targetWidth / 2, targetHeight / 2);
154         }
155         bitmapCanvas.setMatrix(matrix);
156         bitmapCanvas.drawBitmap(mBitmap, offsetX, offsetY, new Paint(Paint.FILTER_BITMAP_FLAG));
157         page.getCanvas().drawBitmap(targetBitmap, null, extent, null);
158         targetBitmap.recycle();
159     }
160 }
161