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