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 android.graphics.pdf; 18 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.UnsupportedAppUsage; 23 import android.graphics.Bitmap; 24 import android.graphics.Bitmap.Config; 25 import android.graphics.Matrix; 26 import android.graphics.Point; 27 import android.graphics.Rect; 28 import android.os.ParcelFileDescriptor; 29 import android.system.ErrnoException; 30 import android.system.Os; 31 import android.system.OsConstants; 32 import com.android.internal.util.Preconditions; 33 import dalvik.system.CloseGuard; 34 35 import libcore.io.IoUtils; 36 37 import java.io.IOException; 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 41 /** 42 * <p> 43 * This class enables rendering a PDF document. This class is not thread safe. 44 * </p> 45 * <p> 46 * If you want to render a PDF, you create a renderer and for every page you want 47 * to render, you open the page, render it, and close the page. After you are done 48 * with rendering, you close the renderer. After the renderer is closed it should not 49 * be used anymore. Note that the pages are rendered one by one, i.e. you can have 50 * only a single page opened at any given time. 51 * </p> 52 * <p> 53 * A typical use of the APIs to render a PDF looks like this: 54 * </p> 55 * <pre> 56 * // create a new renderer 57 * PdfRenderer renderer = new PdfRenderer(getSeekableFileDescriptor()); 58 * 59 * // let us just render all pages 60 * final int pageCount = renderer.getPageCount(); 61 * for (int i = 0; i < pageCount; i++) { 62 * Page page = renderer.openPage(i); 63 * 64 * // say we render for showing on the screen 65 * page.render(mBitmap, null, null, Page.RENDER_MODE_FOR_DISPLAY); 66 * 67 * // do stuff with the bitmap 68 * 69 * // close the page 70 * page.close(); 71 * } 72 * 73 * // close the renderer 74 * renderer.close(); 75 * </pre> 76 * 77 * <h3>Print preview and print output</h3> 78 * <p> 79 * If you are using this class to rasterize a PDF for printing or show a print 80 * preview, it is recommended that you respect the following contract in order 81 * to provide a consistent user experience when seeing a preview and printing, 82 * i.e. the user sees a preview that is the same as the printout. 83 * </p> 84 * <ul> 85 * <li> 86 * Respect the property whether the document would like to be scaled for printing 87 * as per {@link #shouldScaleForPrinting()}. 88 * </li> 89 * <li> 90 * When scaling a document for printing the aspect ratio should be preserved. 91 * </li> 92 * <li> 93 * Do not inset the content with any margins from the {@link android.print.PrintAttributes} 94 * as the application is responsible to render it such that the margins are respected. 95 * </li> 96 * <li> 97 * If document page size is greater than the printed media size the content should 98 * be anchored to the upper left corner of the page for left-to-right locales and 99 * top right corner for right-to-left locales. 100 * </li> 101 * </ul> 102 * 103 * @see #close() 104 */ 105 public final class PdfRenderer implements AutoCloseable { 106 /** 107 * Any call the native pdfium code has to be single threaded as the library does not support 108 * parallel use. 109 */ 110 final static Object sPdfiumLock = new Object(); 111 112 private final CloseGuard mCloseGuard = CloseGuard.get(); 113 114 private final Point mTempPoint = new Point(); 115 116 private long mNativeDocument; 117 118 private final int mPageCount; 119 120 private ParcelFileDescriptor mInput; 121 122 @UnsupportedAppUsage 123 private Page mCurrentPage; 124 125 /** @hide */ 126 @IntDef({ 127 Page.RENDER_MODE_FOR_DISPLAY, 128 Page.RENDER_MODE_FOR_PRINT 129 }) 130 @Retention(RetentionPolicy.SOURCE) 131 public @interface RenderMode {} 132 133 /** 134 * Creates a new instance. 135 * <p> 136 * <strong>Note:</strong> The provided file descriptor must be <strong>seekable</strong>, 137 * i.e. its data being randomly accessed, e.g. pointing to a file. 138 * </p> 139 * <p> 140 * <strong>Note:</strong> This class takes ownership of the passed in file descriptor 141 * and is responsible for closing it when the renderer is closed. 142 * </p> 143 * <p> 144 * If the file is from an untrusted source it is recommended to run the renderer in a separate, 145 * isolated process with minimal permissions to limit the impact of security exploits. 146 * </p> 147 * 148 * @param input Seekable file descriptor to read from. 149 * 150 * @throws java.io.IOException If an error occurs while reading the file. 151 * @throws java.lang.SecurityException If the file requires a password or 152 * the security scheme is not supported. 153 */ PdfRenderer(@onNull ParcelFileDescriptor input)154 public PdfRenderer(@NonNull ParcelFileDescriptor input) throws IOException { 155 if (input == null) { 156 throw new NullPointerException("input cannot be null"); 157 } 158 159 final long size; 160 try { 161 Os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET); 162 size = Os.fstat(input.getFileDescriptor()).st_size; 163 } catch (ErrnoException ee) { 164 throw new IllegalArgumentException("file descriptor not seekable"); 165 } 166 mInput = input; 167 168 synchronized (sPdfiumLock) { 169 mNativeDocument = nativeCreate(mInput.getFd(), size); 170 try { 171 mPageCount = nativeGetPageCount(mNativeDocument); 172 } catch (Throwable t) { 173 nativeClose(mNativeDocument); 174 mNativeDocument = 0; 175 throw t; 176 } 177 } 178 179 mCloseGuard.open("close"); 180 } 181 182 /** 183 * Closes this renderer. You should not use this instance 184 * after this method is called. 185 */ close()186 public void close() { 187 throwIfClosed(); 188 throwIfPageOpened(); 189 doClose(); 190 } 191 192 /** 193 * Gets the number of pages in the document. 194 * 195 * @return The page count. 196 */ getPageCount()197 public int getPageCount() { 198 throwIfClosed(); 199 return mPageCount; 200 } 201 202 /** 203 * Gets whether the document prefers to be scaled for printing. 204 * You should take this info account if the document is rendered 205 * for printing and the target media size differs from the page 206 * size. 207 * 208 * @return If to scale the document. 209 */ shouldScaleForPrinting()210 public boolean shouldScaleForPrinting() { 211 throwIfClosed(); 212 213 synchronized (sPdfiumLock) { 214 return nativeScaleForPrinting(mNativeDocument); 215 } 216 } 217 218 /** 219 * Opens a page for rendering. 220 * 221 * @param index The page index. 222 * @return A page that can be rendered. 223 * 224 * @see android.graphics.pdf.PdfRenderer.Page#close() PdfRenderer.Page.close() 225 */ openPage(int index)226 public Page openPage(int index) { 227 throwIfClosed(); 228 throwIfPageOpened(); 229 throwIfPageNotInDocument(index); 230 mCurrentPage = new Page(index); 231 return mCurrentPage; 232 } 233 234 @Override finalize()235 protected void finalize() throws Throwable { 236 try { 237 if (mCloseGuard != null) { 238 mCloseGuard.warnIfOpen(); 239 } 240 241 doClose(); 242 } finally { 243 super.finalize(); 244 } 245 } 246 247 @UnsupportedAppUsage doClose()248 private void doClose() { 249 if (mCurrentPage != null) { 250 mCurrentPage.close(); 251 mCurrentPage = null; 252 } 253 254 if (mNativeDocument != 0) { 255 synchronized (sPdfiumLock) { 256 nativeClose(mNativeDocument); 257 } 258 mNativeDocument = 0; 259 } 260 261 if (mInput != null) { 262 IoUtils.closeQuietly(mInput); 263 mInput = null; 264 } 265 mCloseGuard.close(); 266 } 267 throwIfClosed()268 private void throwIfClosed() { 269 if (mInput == null) { 270 throw new IllegalStateException("Already closed"); 271 } 272 } 273 throwIfPageOpened()274 private void throwIfPageOpened() { 275 if (mCurrentPage != null) { 276 throw new IllegalStateException("Current page not closed"); 277 } 278 } 279 throwIfPageNotInDocument(int pageIndex)280 private void throwIfPageNotInDocument(int pageIndex) { 281 if (pageIndex < 0 || pageIndex >= mPageCount) { 282 throw new IllegalArgumentException("Invalid page index"); 283 } 284 } 285 286 /** 287 * This class represents a PDF document page for rendering. 288 */ 289 public final class Page implements AutoCloseable { 290 291 private final CloseGuard mCloseGuard = CloseGuard.get(); 292 293 /** 294 * Mode to render the content for display on a screen. 295 */ 296 public static final int RENDER_MODE_FOR_DISPLAY = 1; 297 298 /** 299 * Mode to render the content for printing. 300 */ 301 public static final int RENDER_MODE_FOR_PRINT = 2; 302 303 private final int mIndex; 304 private final int mWidth; 305 private final int mHeight; 306 307 private long mNativePage; 308 Page(int index)309 private Page(int index) { 310 Point size = mTempPoint; 311 synchronized (sPdfiumLock) { 312 mNativePage = nativeOpenPageAndGetSize(mNativeDocument, index, size); 313 } 314 mIndex = index; 315 mWidth = size.x; 316 mHeight = size.y; 317 mCloseGuard.open("close"); 318 } 319 320 /** 321 * Gets the page index. 322 * 323 * @return The index. 324 */ getIndex()325 public int getIndex() { 326 return mIndex; 327 } 328 329 /** 330 * Gets the page width in points (1/72"). 331 * 332 * @return The width in points. 333 */ getWidth()334 public int getWidth() { 335 return mWidth; 336 } 337 338 /** 339 * Gets the page height in points (1/72"). 340 * 341 * @return The height in points. 342 */ getHeight()343 public int getHeight() { 344 return mHeight; 345 } 346 347 /** 348 * Renders a page to a bitmap. 349 * <p> 350 * You may optionally specify a rectangular clip in the bitmap bounds. No rendering 351 * outside the clip will be performed, hence it is your responsibility to initialize 352 * the bitmap outside the clip. 353 * </p> 354 * <p> 355 * You may optionally specify a matrix to transform the content from page coordinates 356 * which are in points (1/72") to bitmap coordinates which are in pixels. If this 357 * matrix is not provided this method will apply a transformation that will fit the 358 * whole page to the destination clip if provided or the destination bitmap if no 359 * clip is provided. 360 * </p> 361 * <p> 362 * The clip and transformation are useful for implementing tile rendering where the 363 * destination bitmap contains a portion of the image, for example when zooming. 364 * Another useful application is for printing where the size of the bitmap holding 365 * the page is too large and a client can render the page in stripes. 366 * </p> 367 * <p> 368 * <strong>Note: </strong> The destination bitmap format must be 369 * {@link Config#ARGB_8888 ARGB}. 370 * </p> 371 * <p> 372 * <strong>Note: </strong> The optional transformation matrix must be affine as per 373 * {@link android.graphics.Matrix#isAffine() Matrix.isAffine()}. Hence, you can specify 374 * rotation, scaling, translation but not a perspective transformation. 375 * </p> 376 * 377 * @param destination Destination bitmap to which to render. 378 * @param destClip Optional clip in the bitmap bounds. 379 * @param transform Optional transformation to apply when rendering. 380 * @param renderMode The render mode. 381 * 382 * @see #RENDER_MODE_FOR_DISPLAY 383 * @see #RENDER_MODE_FOR_PRINT 384 */ render(@onNull Bitmap destination, @Nullable Rect destClip, @Nullable Matrix transform, @RenderMode int renderMode)385 public void render(@NonNull Bitmap destination, @Nullable Rect destClip, 386 @Nullable Matrix transform, @RenderMode int renderMode) { 387 if (mNativePage == 0) { 388 throw new NullPointerException(); 389 } 390 391 destination = Preconditions.checkNotNull(destination, "bitmap null"); 392 393 if (destination.getConfig() != Config.ARGB_8888) { 394 throw new IllegalArgumentException("Unsupported pixel format"); 395 } 396 397 if (destClip != null) { 398 if (destClip.left < 0 || destClip.top < 0 399 || destClip.right > destination.getWidth() 400 || destClip.bottom > destination.getHeight()) { 401 throw new IllegalArgumentException("destBounds not in destination"); 402 } 403 } 404 405 if (transform != null && !transform.isAffine()) { 406 throw new IllegalArgumentException("transform not affine"); 407 } 408 409 if (renderMode != RENDER_MODE_FOR_PRINT && renderMode != RENDER_MODE_FOR_DISPLAY) { 410 throw new IllegalArgumentException("Unsupported render mode"); 411 } 412 413 if (renderMode == RENDER_MODE_FOR_PRINT && renderMode == RENDER_MODE_FOR_DISPLAY) { 414 throw new IllegalArgumentException("Only single render mode supported"); 415 } 416 417 final int contentLeft = (destClip != null) ? destClip.left : 0; 418 final int contentTop = (destClip != null) ? destClip.top : 0; 419 final int contentRight = (destClip != null) ? destClip.right 420 : destination.getWidth(); 421 final int contentBottom = (destClip != null) ? destClip.bottom 422 : destination.getHeight(); 423 424 // If transform is not set, stretch page to whole clipped area 425 if (transform == null) { 426 int clipWidth = contentRight - contentLeft; 427 int clipHeight = contentBottom - contentTop; 428 429 transform = new Matrix(); 430 transform.postScale((float)clipWidth / getWidth(), 431 (float)clipHeight / getHeight()); 432 transform.postTranslate(contentLeft, contentTop); 433 } 434 435 final long transformPtr = transform.native_instance; 436 437 synchronized (sPdfiumLock) { 438 nativeRenderPage(mNativeDocument, mNativePage, destination.getNativeInstance(), 439 contentLeft, contentTop, contentRight, contentBottom, transformPtr, 440 renderMode); 441 } 442 } 443 444 /** 445 * Closes this page. 446 * 447 * @see android.graphics.pdf.PdfRenderer#openPage(int) 448 */ 449 @Override close()450 public void close() { 451 throwIfClosed(); 452 doClose(); 453 } 454 455 @Override finalize()456 protected void finalize() throws Throwable { 457 try { 458 if (mCloseGuard != null) { 459 mCloseGuard.warnIfOpen(); 460 } 461 462 doClose(); 463 } finally { 464 super.finalize(); 465 } 466 } 467 doClose()468 private void doClose() { 469 if (mNativePage != 0) { 470 synchronized (sPdfiumLock) { 471 nativeClosePage(mNativePage); 472 } 473 mNativePage = 0; 474 } 475 476 mCloseGuard.close(); 477 mCurrentPage = null; 478 } 479 throwIfClosed()480 private void throwIfClosed() { 481 if (mNativePage == 0) { 482 throw new IllegalStateException("Already closed"); 483 } 484 } 485 } 486 nativeCreate(int fd, long size)487 private static native long nativeCreate(int fd, long size); nativeClose(long documentPtr)488 private static native void nativeClose(long documentPtr); nativeGetPageCount(long documentPtr)489 private static native int nativeGetPageCount(long documentPtr); nativeScaleForPrinting(long documentPtr)490 private static native boolean nativeScaleForPrinting(long documentPtr); nativeRenderPage(long documentPtr, long pagePtr, long bitmapHandle, int clipLeft, int clipTop, int clipRight, int clipBottom, long transformPtr, int renderMode)491 private static native void nativeRenderPage(long documentPtr, long pagePtr, long bitmapHandle, 492 int clipLeft, int clipTop, int clipRight, int clipBottom, long transformPtr, 493 int renderMode); nativeOpenPageAndGetSize(long documentPtr, int pageIndex, Point outSize)494 private static native long nativeOpenPageAndGetSize(long documentPtr, int pageIndex, 495 Point outSize); nativeClosePage(long pagePtr)496 private static native void nativeClosePage(long pagePtr); 497 } 498