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