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