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