1 /* 2 * Copyright (C) 2014 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.printspooler.renderer; 18 19 import android.app.Service; 20 import android.content.Intent; 21 import android.content.res.Configuration; 22 import android.graphics.Bitmap; 23 import android.graphics.Color; 24 import android.graphics.Matrix; 25 import android.graphics.Rect; 26 import android.graphics.pdf.PdfEditor; 27 import android.graphics.pdf.PdfRenderer; 28 import android.os.IBinder; 29 import android.os.ParcelFileDescriptor; 30 import android.os.RemoteException; 31 import android.print.PageRange; 32 import android.print.PrintAttributes; 33 import android.print.PrintAttributes.Margins; 34 import android.util.Log; 35 import android.view.View; 36 import com.android.printspooler.util.PageRangeUtils; 37 import libcore.io.IoUtils; 38 import com.android.printspooler.util.BitmapSerializeUtils; 39 import java.io.IOException; 40 41 /** 42 * Service for manipulation of PDF documents in an isolated process. 43 */ 44 public final class PdfManipulationService extends Service { 45 public static final String ACTION_GET_RENDERER = 46 "com.android.printspooler.renderer.ACTION_GET_RENDERER"; 47 public static final String ACTION_GET_EDITOR = 48 "com.android.printspooler.renderer.ACTION_GET_EDITOR"; 49 50 public static final int ERROR_MALFORMED_PDF_FILE = -2; 51 52 public static final int ERROR_SECURE_PDF_FILE = -3; 53 54 private static final String LOG_TAG = "PdfManipulationService"; 55 private static final boolean DEBUG = false; 56 57 private static final int MILS_PER_INCH = 1000; 58 private static final int POINTS_IN_INCH = 72; 59 60 @Override onBind(Intent intent)61 public IBinder onBind(Intent intent) { 62 String action = intent.getAction(); 63 switch (action) { 64 case ACTION_GET_RENDERER: { 65 return new PdfRendererImpl(); 66 } 67 case ACTION_GET_EDITOR: { 68 return new PdfEditorImpl(); 69 } 70 default: { 71 throw new IllegalArgumentException("Invalid intent action:" + action); 72 } 73 } 74 } 75 76 private final class PdfRendererImpl extends IPdfRenderer.Stub { 77 private final Object mLock = new Object(); 78 79 private Bitmap mBitmap; 80 private PdfRenderer mRenderer; 81 82 @Override openDocument(ParcelFileDescriptor source)83 public int openDocument(ParcelFileDescriptor source) throws RemoteException { 84 synchronized (mLock) { 85 try { 86 throwIfOpened(); 87 if (DEBUG) { 88 Log.i(LOG_TAG, "openDocument()"); 89 } 90 mRenderer = new PdfRenderer(source); 91 return mRenderer.getPageCount(); 92 } catch (IOException | IllegalStateException e) { 93 IoUtils.closeQuietly(source); 94 Log.e(LOG_TAG, "Cannot open file", e); 95 return ERROR_MALFORMED_PDF_FILE; 96 } catch (SecurityException e) { 97 IoUtils.closeQuietly(source); 98 Log.e(LOG_TAG, "Cannot open file", e); 99 return ERROR_SECURE_PDF_FILE; 100 } 101 } 102 } 103 104 @Override renderPage(int pageIndex, int bitmapWidth, int bitmapHeight, PrintAttributes attributes, ParcelFileDescriptor destination)105 public void renderPage(int pageIndex, int bitmapWidth, int bitmapHeight, 106 PrintAttributes attributes, ParcelFileDescriptor destination) { 107 synchronized (mLock) { 108 try { 109 throwIfNotOpened(); 110 111 try (PdfRenderer.Page page = mRenderer.openPage(pageIndex)) { 112 final int srcWidthPts = page.getWidth(); 113 final int srcHeightPts = page.getHeight(); 114 115 final int dstWidthPts = pointsFromMils( 116 attributes.getMediaSize().getWidthMils()); 117 final int dstHeightPts = pointsFromMils( 118 attributes.getMediaSize().getHeightMils()); 119 120 final boolean scaleContent = mRenderer.shouldScaleForPrinting(); 121 final boolean contentLandscape = !attributes.getMediaSize().isPortrait(); 122 123 final float displayScale; 124 Matrix matrix = new Matrix(); 125 126 if (scaleContent) { 127 displayScale = Math.min((float) bitmapWidth / srcWidthPts, 128 (float) bitmapHeight / srcHeightPts); 129 } else { 130 if (contentLandscape) { 131 displayScale = (float) bitmapHeight / dstHeightPts; 132 } else { 133 displayScale = (float) bitmapWidth / dstWidthPts; 134 } 135 } 136 matrix.postScale(displayScale, displayScale); 137 138 Configuration configuration = PdfManipulationService.this.getResources() 139 .getConfiguration(); 140 if (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { 141 matrix.postTranslate(bitmapWidth - srcWidthPts * displayScale, 0); 142 } 143 144 Margins minMargins = attributes.getMinMargins(); 145 final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils()); 146 final int paddingTopPts = pointsFromMils(minMargins.getTopMils()); 147 final int paddingRightPts = pointsFromMils(minMargins.getRightMils()); 148 final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils()); 149 150 Rect clip = new Rect(); 151 clip.left = (int) (paddingLeftPts * displayScale); 152 clip.top = (int) (paddingTopPts * displayScale); 153 clip.right = (int) (bitmapWidth - paddingRightPts * displayScale); 154 clip.bottom = (int) (bitmapHeight - paddingBottomPts * displayScale); 155 156 if (DEBUG) { 157 Log.i(LOG_TAG, "Rendering page:" + pageIndex); 158 } 159 160 Bitmap bitmap = getBitmapForSize(bitmapWidth, bitmapHeight); 161 page.render(bitmap, clip, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); 162 163 BitmapSerializeUtils.writeBitmapPixels(bitmap, destination); 164 } 165 } catch (Throwable e) { 166 Log.e(LOG_TAG, "Cannot render page", e); 167 168 // The error is propagated to the caller when it tries to read the bitmap and 169 // the pipe is closed prematurely 170 } finally { 171 IoUtils.closeQuietly(destination); 172 } 173 } 174 } 175 176 @Override closeDocument()177 public void closeDocument() { 178 synchronized (mLock) { 179 throwIfNotOpened(); 180 if (DEBUG) { 181 Log.i(LOG_TAG, "closeDocument()"); 182 } 183 mRenderer.close(); 184 mRenderer = null; 185 } 186 } 187 getBitmapForSize(int width, int height)188 private Bitmap getBitmapForSize(int width, int height) { 189 if (mBitmap != null) { 190 if (mBitmap.getWidth() == width && mBitmap.getHeight() == height) { 191 mBitmap.eraseColor(Color.WHITE); 192 return mBitmap; 193 } 194 mBitmap.recycle(); 195 } 196 mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 197 mBitmap.eraseColor(Color.WHITE); 198 return mBitmap; 199 } 200 throwIfOpened()201 private void throwIfOpened() { 202 if (mRenderer != null) { 203 throw new IllegalStateException("Already opened"); 204 } 205 } 206 throwIfNotOpened()207 private void throwIfNotOpened() { 208 if (mRenderer == null) { 209 throw new IllegalStateException("Not opened"); 210 } 211 } 212 } 213 214 private final class PdfEditorImpl extends IPdfEditor.Stub { 215 private final Object mLock = new Object(); 216 217 private PdfEditor mEditor; 218 219 @Override openDocument(ParcelFileDescriptor source)220 public int openDocument(ParcelFileDescriptor source) throws RemoteException { 221 synchronized (mLock) { 222 try { 223 throwIfOpened(); 224 if (DEBUG) { 225 Log.i(LOG_TAG, "openDocument()"); 226 } 227 mEditor = new PdfEditor(source); 228 return mEditor.getPageCount(); 229 } catch (IOException | IllegalStateException e) { 230 IoUtils.closeQuietly(source); 231 Log.e(LOG_TAG, "Cannot open file", e); 232 throw new RemoteException(e.toString()); 233 } 234 } 235 } 236 237 @Override removePages(PageRange[] ranges)238 public void removePages(PageRange[] ranges) { 239 synchronized (mLock) { 240 throwIfNotOpened(); 241 if (DEBUG) { 242 Log.i(LOG_TAG, "removePages()"); 243 } 244 245 ranges = PageRangeUtils.normalize(ranges); 246 247 int lastPageIdx = mEditor.getPageCount() - 1; 248 249 final int rangeCount = ranges.length; 250 for (int i = rangeCount - 1; i >= 0; i--) { 251 PageRange range = ranges[i]; 252 253 // Ignore removal of pages that are outside the document 254 if (range.getEnd() > lastPageIdx) { 255 if (range.getStart() > lastPageIdx) { 256 continue; 257 } 258 range = new PageRange(range.getStart(), lastPageIdx); 259 } 260 261 for (int j = range.getEnd(); j >= range.getStart(); j--) { 262 mEditor.removePage(j); 263 } 264 } 265 } 266 } 267 268 @Override applyPrintAttributes(PrintAttributes attributes)269 public void applyPrintAttributes(PrintAttributes attributes) { 270 synchronized (mLock) { 271 throwIfNotOpened(); 272 if (DEBUG) { 273 Log.i(LOG_TAG, "applyPrintAttributes()"); 274 } 275 276 Rect mediaBox = new Rect(); 277 Rect cropBox = new Rect(); 278 Matrix transform = new Matrix(); 279 280 final boolean layoutDirectionRtl = getResources().getConfiguration() 281 .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; 282 283 // We do not want to rotate the media box, so take into account orientation. 284 final int dstWidthPts = pointsFromMils(attributes.getMediaSize().getWidthMils()); 285 final int dstHeightPts = pointsFromMils(attributes.getMediaSize().getHeightMils()); 286 287 final boolean scaleForPrinting = mEditor.shouldScaleForPrinting(); 288 289 final int pageCount = mEditor.getPageCount(); 290 for (int i = 0; i < pageCount; i++) { 291 if (!mEditor.getPageMediaBox(i, mediaBox)) { 292 Log.e(LOG_TAG, "Malformed PDF file"); 293 return; 294 } 295 296 final int srcWidthPts = mediaBox.width(); 297 final int srcHeightPts = mediaBox.height(); 298 299 // Update the media box with the desired size. 300 mediaBox.right = dstWidthPts; 301 mediaBox.bottom = dstHeightPts; 302 mEditor.setPageMediaBox(i, mediaBox); 303 304 // Make sure content is top-left after media box resize. 305 transform.setTranslate(0, srcHeightPts - dstHeightPts); 306 307 // Scale the content if document allows it. 308 final float scale; 309 if (scaleForPrinting) { 310 scale = Math.min((float) dstWidthPts / srcWidthPts, 311 (float) dstHeightPts / srcHeightPts); 312 transform.postScale(scale, scale); 313 } else { 314 scale = 1.0f; 315 } 316 317 // Update the crop box relatively to the media box change, if needed. 318 if (mEditor.getPageCropBox(i, cropBox)) { 319 cropBox.left = (int) (cropBox.left * scale + 0.5f); 320 cropBox.top = (int) (cropBox.top * scale + 0.5f); 321 cropBox.right = (int) (cropBox.right * scale + 0.5f); 322 cropBox.bottom = (int) (cropBox.bottom * scale + 0.5f); 323 cropBox.intersect(mediaBox); 324 mEditor.setPageCropBox(i, cropBox); 325 } 326 327 // If in RTL mode put the content in the logical top-right corner. 328 if (layoutDirectionRtl) { 329 final float dx = dstWidthPts - (int) (srcWidthPts * scale + 0.5f); 330 final float dy = 0; 331 transform.postTranslate(dx, dy); 332 } 333 334 // Adjust the physical margins if needed. 335 Margins minMargins = attributes.getMinMargins(); 336 final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils()); 337 final int paddingTopPts = pointsFromMils(minMargins.getTopMils()); 338 final int paddingRightPts = pointsFromMils(minMargins.getRightMils()); 339 final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils()); 340 341 Rect clip = new Rect(mediaBox); 342 clip.left += paddingLeftPts; 343 clip.top += paddingTopPts; 344 clip.right -= paddingRightPts; 345 clip.bottom -= paddingBottomPts; 346 347 // Apply the accumulated transforms. 348 mEditor.setTransformAndClip(i, transform, clip); 349 } 350 } 351 } 352 353 @Override write(ParcelFileDescriptor destination)354 public void write(ParcelFileDescriptor destination) throws RemoteException { 355 synchronized (mLock) { 356 try { 357 throwIfNotOpened(); 358 if (DEBUG) { 359 Log.i(LOG_TAG, "write()"); 360 } 361 mEditor.write(destination); 362 } catch (IOException | IllegalStateException e) { 363 IoUtils.closeQuietly(destination); 364 Log.e(LOG_TAG, "Error writing PDF to file.", e); 365 throw new RemoteException(e.toString()); 366 } 367 } 368 } 369 370 @Override closeDocument()371 public void closeDocument() { 372 synchronized (mLock) { 373 throwIfNotOpened(); 374 if (DEBUG) { 375 Log.i(LOG_TAG, "closeDocument()"); 376 } 377 mEditor.close(); 378 mEditor = null; 379 } 380 } 381 throwIfOpened()382 private void throwIfOpened() { 383 if (mEditor != null) { 384 throw new IllegalStateException("Already opened"); 385 } 386 } 387 throwIfNotOpened()388 private void throwIfNotOpened() { 389 if (mEditor == null) { 390 throw new IllegalStateException("Not opened"); 391 } 392 } 393 } 394 pointsFromMils(int mils)395 private static int pointsFromMils(int mils) { 396 return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH); 397 } 398 } 399