1 /* 2 * Copyright (C) 2024 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 static android.graphics.pdf.PdfLinearizationTypes.PDF_DOCUMENT_TYPE_LINEARIZED; 20 import static android.graphics.pdf.PdfLinearizationTypes.PDF_DOCUMENT_TYPE_NON_LINEARIZED; 21 22 import android.annotation.FlaggedApi; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.graphics.Bitmap; 26 import android.graphics.Matrix; 27 import android.graphics.Point; 28 import android.graphics.Rect; 29 import android.graphics.pdf.content.PdfPageGotoLinkContent; 30 import android.graphics.pdf.content.PdfPageImageContent; 31 import android.graphics.pdf.content.PdfPageLinkContent; 32 import android.graphics.pdf.content.PdfPageTextContent; 33 import android.graphics.pdf.flags.Flags; 34 import android.graphics.pdf.logging.PdfEventLogger; 35 import android.graphics.pdf.models.FormEditRecord; 36 import android.graphics.pdf.models.FormWidgetInfo; 37 import android.graphics.pdf.models.PageMatchBounds; 38 import android.graphics.pdf.models.jni.LoadPdfResult; 39 import android.graphics.pdf.models.selection.PageSelection; 40 import android.graphics.pdf.models.selection.SelectionBoundary; 41 import android.graphics.pdf.utils.Preconditions; 42 import android.os.Binder; 43 import android.os.ParcelFileDescriptor; 44 import android.system.ErrnoException; 45 import android.system.Os; 46 import android.system.OsConstants; 47 import android.util.Log; 48 49 import java.io.IOException; 50 import java.security.SecureRandom; 51 import java.util.Collections; 52 import java.util.List; 53 import java.util.stream.Collectors; 54 55 /** 56 * Represents a PDF document processing class. 57 * 58 * @hide 59 */ 60 public class PdfProcessor { 61 /** Represents a PDF without form fields */ 62 public static final int PDF_FORM_TYPE_NONE = 0; 63 64 /** Represents a PDF with form fields specified using the AcroForm spec */ 65 public static final int PDF_FORM_TYPE_ACRO_FORM = 1; 66 67 /** Represents a PDF with form fields specified using the entire XFA spec */ 68 public static final int PDF_FORM_TYPE_XFA_FULL = 2; 69 70 /** Represents a PDF with form fields specified using the XFAF subset of the XFA spec */ 71 public static final int PDF_FORM_TYPE_XFA_FOREGROUND = 3; 72 73 private static final String TAG = "PdfProcessor"; 74 private static final Object sPdfiumLock = new Object(); 75 private final PdfEventLogger mPdfEventLogger; 76 private PdfDocumentProxy mPdfDocument; 77 PdfProcessor()78 public PdfProcessor() { 79 PdfDocumentProxy.loadLibPdf(); 80 81 mPdfEventLogger = new PdfEventLogger( 82 /* processId = */ Binder.getCallingUid(), 83 /* docId = */ new SecureRandom().nextLong()); 84 } 85 86 /** 87 * Creates an instance of {@link PdfDocumentProxy} on successful loading of the PDF document. 88 * This method ensures that an older {@link PdfDocumentProxy} instance is closed and then loads 89 * the new document. This method should be run on a {@link android.annotation.WorkerThread} as 90 * it is long-running task. 91 * 92 * @param fileDescriptor {@link ParcelFileDescriptor} for the input PDF document. 93 * @param params instance of {@link LoadParams} which includes the password as well. 94 * @throws IOException if an error occurred during the processing of the PDF document. 95 * @throws SecurityException if the password is incorrect. 96 */ 97 @FlaggedApi(Flags.FLAG_ENABLE_PDF_VIEWER) create(ParcelFileDescriptor fileDescriptor, @Nullable LoadParams params)98 public void create(ParcelFileDescriptor fileDescriptor, @Nullable LoadParams params) 99 throws IOException { 100 long loadingStartTime = System.currentTimeMillis(); 101 102 Preconditions.checkNotNull(fileDescriptor, "Input FD cannot be null"); 103 ensurePdfDestroyed(); 104 try { 105 Os.lseek(fileDescriptor.getFileDescriptor(), 0, OsConstants.SEEK_SET); 106 } catch (ErrnoException ee) { 107 throw new IllegalArgumentException("File descriptor not seekable"); 108 } 109 110 String password = (params != null) ? params.getPassword() : null; 111 synchronized (sPdfiumLock) { 112 LoadPdfResult result = PdfDocumentProxy.createFromFd(fileDescriptor.detachFd(), 113 password); 114 switch (result.status) { 115 case NEED_MORE_DATA, PDF_ERROR, FILE_ERROR -> { 116 117 mPdfEventLogger.logPdfLoadReportedEvent( 118 /* loadDurationMillis= */ System.currentTimeMillis() - loadingStartTime, 119 /* pdfSizeInKb = */ result.pdfSizeInKb, 120 /* pdfLoadResult = */ PdfEventLogger.PdfLoadResults.ERROR, 121 /* linearizationType = */ PdfEventLogger.LinearizationTypes.UNKNOWN, 122 /* numPages = */ -1); 123 throw new IOException("Unable to load the document!"); 124 } 125 case REQUIRES_PASSWORD -> { 126 127 mPdfEventLogger.logPdfLoadReportedEvent( 128 /* loadDurationMillis= */ System.currentTimeMillis() - loadingStartTime, 129 /* pdfSizeInKb = */ result.pdfSizeInKb, 130 /* pdfLoadResult = */ PdfEventLogger.PdfLoadResults.WRONG_PASSWORD, 131 /* linearizationType = */ PdfEventLogger.LinearizationTypes.UNKNOWN, 132 /* numPages = */ -1); 133 throw new SecurityException("Password required to access document"); 134 } 135 case LOADED -> { 136 137 this.mPdfDocument = result.pdfDocument; 138 139 @PdfEventLogger.LinearizationTypes.LinearizationType int linearizationType = 140 mPdfDocument.isPdfLinearized() 141 ? PdfEventLogger.LinearizationTypes.LINEARIZED 142 : PdfEventLogger.LinearizationTypes.NON_LINEARIZED; 143 144 // Log pdf loaded successfully. 145 mPdfEventLogger.logPdfLoadReportedEvent( 146 /* loadDurationMillis= */ System.currentTimeMillis() - loadingStartTime, 147 /* pdfSizeInKb = */ result.pdfSizeInKb, 148 /* pdfLoadResult = */ PdfEventLogger.PdfLoadResults.LOADED, 149 /* linearizationType = */ linearizationType, 150 /* numPages = */ mPdfDocument.getNumPages()); 151 } 152 default -> { 153 154 mPdfEventLogger.logPdfLoadReportedEvent( 155 /* loadDurationMillis= */ System.currentTimeMillis() - loadingStartTime, 156 /* pdfSizeInKb = */ result.pdfSizeInKb, 157 /* pdfLoadResult = */ PdfEventLogger.PdfLoadResults.UNKNOWN, 158 /* linearizationType = */ PdfEventLogger.LinearizationTypes.UNKNOWN, 159 /* numPages = */ -1); 160 throw new RuntimeException("Unexpected error has occurred!"); 161 } 162 } 163 } 164 } 165 166 /** Returns the number of pages in the PDF document */ getNumPages()167 public int getNumPages() { 168 synchronized (sPdfiumLock) { 169 assertPdfDocumentNotNull(); 170 return mPdfDocument.getNumPages(); 171 } 172 } 173 174 /** 175 * Returns the {@link List} of {@link PdfPageTextContent} for the page number specified. In case 176 * of the multiple column textual content, the order is not guaranteed and the text is returned 177 * as it is seen by the processing library. 178 * 179 * @param pageNum page number of the document 180 * @return list of the textual content encountered on the page. 181 */ 182 @FlaggedApi(Flags.FLAG_ENABLE_PDF_VIEWER) getPageTextContents(int pageNum)183 public List<PdfPageTextContent> getPageTextContents(int pageNum) { 184 synchronized (sPdfiumLock) { 185 assertPdfDocumentNotNull(); 186 PdfPageTextContent content = new PdfPageTextContent(mPdfDocument.getPageText(pageNum)); 187 return List.of(content); 188 } 189 } 190 191 /** 192 * Returns the alternate text for each image encountered on the specified page as a 193 * {@link List} of {@link PdfPageImageContent}. The primary use case of this method is for 194 * accessibility. 195 * 196 * @param pageNum page number of the document 197 * @return list of the alt text for each image on the page. 198 */ 199 @FlaggedApi(Flags.FLAG_ENABLE_PDF_VIEWER) getPageImageContents(int pageNum)200 public List<PdfPageImageContent> getPageImageContents(int pageNum) { 201 synchronized (sPdfiumLock) { 202 assertPdfDocumentNotNull(); 203 return mPdfDocument.getPageAltText(pageNum).stream().map( 204 PdfPageImageContent::new).collect(Collectors.toList()); 205 } 206 } 207 208 /** 209 * Returns the width of the given page of the PDF document. It is not guaranteed that all the 210 * pages of the document will have the same dimensions 211 */ getPageWidth(int pageNum)212 public int getPageWidth(int pageNum) { 213 synchronized (sPdfiumLock) { 214 assertPdfDocumentNotNull(); 215 return mPdfDocument.getPageWidth(pageNum); 216 } 217 } 218 219 /** 220 * Returns the height of the given page of the PDF document. It is not guaranteed that all the 221 * pages of the document will have the same dimensions 222 */ getPageHeight(int pageNum)223 public int getPageHeight(int pageNum) { 224 synchronized (sPdfiumLock) { 225 assertPdfDocumentNotNull(); 226 return mPdfDocument.getPageHeight(pageNum); 227 } 228 } 229 230 /** 231 * Renders a page to a bitmap for the specified page number. 232 * 233 * <p>Should be invoked on the {@link android.annotation.WorkerThread} as it is a long-running 234 * task. 235 */ 236 @FlaggedApi(Flags.FLAG_ENABLE_PDF_VIEWER) renderPage(int pageNum, Bitmap bitmap, Rect destClip, Matrix transform, RenderParams params, boolean renderFormFields)237 public void renderPage(int pageNum, Bitmap bitmap, Rect destClip, Matrix transform, 238 RenderParams params, boolean renderFormFields) { 239 Preconditions.checkNotNull(bitmap, "Destination bitmap cannot be null"); 240 Preconditions.checkNotNull(params, "RenderParams cannot be null"); 241 Preconditions.checkArgument(bitmap.getConfig() == Bitmap.Config.ARGB_8888, 242 "Unsupported pixel format"); 243 Preconditions.checkArgument(transform == null || transform.isAffine(), 244 "Transform not affine"); 245 int renderMode = params.getRenderMode(); 246 Preconditions.checkArgument(renderMode == RenderParams.RENDER_MODE_FOR_DISPLAY 247 || renderMode == RenderParams.RENDER_MODE_FOR_PRINT, "Unsupported render mode"); 248 Preconditions.checkArgument(clipInBitmap(destClip, bitmap), "destClip not in bounds"); 249 final int contentLeft = (destClip != null) ? destClip.left : 0; 250 final int contentTop = (destClip != null) ? destClip.top : 0; 251 final int contentRight = (destClip != null) ? destClip.right : bitmap.getWidth(); 252 final int contentBottom = (destClip != null) ? destClip.bottom : bitmap.getHeight(); 253 254 // If transform is not set, stretch page to whole clipped area 255 if (transform == null) { 256 int clipWidth = contentRight - contentLeft; 257 int clipHeight = contentBottom - contentTop; 258 transform = new Matrix(); 259 transform.postScale((float) clipWidth / getPageWidth(pageNum), 260 (float) clipHeight / getPageHeight(pageNum)); 261 transform.postTranslate(contentLeft, contentTop); 262 } 263 264 float[] transformArr = new float[9]; 265 transform.getValues(transformArr); 266 267 synchronized (sPdfiumLock) { 268 assertPdfDocumentNotNull(); 269 mPdfDocument.render( 270 pageNum, 271 bitmap, 272 contentLeft, 273 contentTop, 274 contentRight, 275 contentBottom, 276 transformArr, 277 renderMode, 278 params.getRenderAnnotations(), 279 renderFormFields); 280 } 281 } 282 283 /** 284 * Searches the specified page with the specified query. Should be run on the 285 * {@link android.annotation.WorkerThread} as it is a long-running task. 286 * 287 * @param pageNum page number of the document 288 * @param query the search query 289 * @return list of {@link PageMatchBounds} that represents the highlighters which can span 290 * multiple 291 * lines as well. 292 */ 293 @FlaggedApi(Flags.FLAG_ENABLE_PDF_VIEWER) searchPageText(int pageNum, String query)294 public List<PageMatchBounds> searchPageText(int pageNum, String query) { 295 Preconditions.checkNotNull(query, "Search query cannot be null"); 296 synchronized (sPdfiumLock) { 297 assertPdfDocumentNotNull(); 298 long searchStartTime = System.currentTimeMillis(); 299 List<PageMatchBounds> searchPageTextResult = 300 mPdfDocument.searchPageText(pageNum, query).unflattenToList(); 301 302 // In the current version we are only interested in the results where query length is 303 // 1 as there is no much change in latency after words. 304 if (pageNum == 0 && query.length() == 1) { 305 mPdfEventLogger.logSearchReportedEvent( 306 /* loadDurationMillis = */ System.currentTimeMillis() - searchStartTime, 307 /* queryLength = */ query.length(), 308 /* queryPageNumber = */ pageNum, 309 /* apiResponse = */ PdfEventLogger.ApiResponseTypes.SUCCESS, 310 /* numPages = */ mPdfDocument.getNumPages(), 311 /* matchCount = */ searchPageTextResult.size()); 312 } 313 314 return searchPageTextResult; 315 } 316 } 317 318 /** 319 * Return a PageSelection which represents the selected content that spans between the 320 * two boundaries, both of which can be either exactly defined with text indexes, or 321 * approximately defined with points on the page.The resulting Selection will also be 322 * exactly defined with both indexes and points.If the start and stop boundary are both 323 * the same point, selects the word at that point. 324 */ 325 @FlaggedApi(Flags.FLAG_ENABLE_PDF_VIEWER) selectPageText(int pageNum, SelectionBoundary start, SelectionBoundary stop)326 public PageSelection selectPageText(int pageNum, SelectionBoundary start, 327 SelectionBoundary stop) { 328 Preconditions.checkNotNull(start, "Start selection boundary cannot be null"); 329 Preconditions.checkNotNull(stop, "Stop selection boundary cannot be null"); 330 synchronized (sPdfiumLock) { 331 assertPdfDocumentNotNull(); 332 android.graphics.pdf.models.jni.PageSelection legacyPageSelection = 333 mPdfDocument.selectPageText(pageNum, 334 android.graphics.pdf.models.jni.SelectionBoundary.convert(start), 335 android.graphics.pdf.models.jni.SelectionBoundary.convert(stop)); 336 if (legacyPageSelection != null) { 337 338 mPdfEventLogger.logPdfApiUsageReportedEvent( 339 /* apiType = */ PdfEventLogger.ApiTypes.SELECT_CONTENT, 340 /* apiResponse = */ PdfEventLogger.ApiResponseTypes.SUCCESS); 341 342 return legacyPageSelection.convert(); 343 } 344 return null; 345 } 346 } 347 348 /** Get the bounds and URLs of all the links on the given page. */ 349 @FlaggedApi(Flags.FLAG_ENABLE_PDF_VIEWER) getPageLinkContents(int pageNum)350 public List<PdfPageLinkContent> getPageLinkContents(int pageNum) { 351 synchronized (sPdfiumLock) { 352 assertPdfDocumentNotNull(); 353 return mPdfDocument.getPageLinks(pageNum).unflattenToList(); 354 } 355 } 356 357 /** Returns bookmarks and other goto links (within the current document) on a page */ getPageGotoLinks(int pageNum)358 public List<PdfPageGotoLinkContent> getPageGotoLinks(int pageNum) { 359 synchronized (sPdfiumLock) { 360 assertPdfDocumentNotNull(); 361 return mPdfDocument.getPageGotoLinks(pageNum); 362 } 363 } 364 365 /** Retains object in memory related to a page when that page becomes visible. */ retainPage(int pageNum)366 public void retainPage(int pageNum) { 367 synchronized (sPdfiumLock) { 368 assertPdfDocumentNotNull(); 369 mPdfDocument.retainPage(pageNum); 370 } 371 } 372 373 /** Releases object in memory related to a page when that page is no longer visible. */ releasePage(int pageNum)374 public void releasePage(int pageNum) { 375 synchronized (sPdfiumLock) { 376 assertPdfDocumentNotNull(); 377 mPdfDocument.releasePage(pageNum); 378 } 379 } 380 381 /** 382 * Returns the linearization flag on the PDF document. 383 */ 384 @PdfLinearizationTypes.PdfLinearizationType getDocumentLinearizationType()385 public int getDocumentLinearizationType() { 386 synchronized (sPdfiumLock) { 387 assertPdfDocumentNotNull(); 388 return mPdfDocument.isPdfLinearized() ? PDF_DOCUMENT_TYPE_LINEARIZED 389 : PDF_DOCUMENT_TYPE_NON_LINEARIZED; 390 } 391 } 392 393 /** 394 * Returns the form type of the loaded PDF 395 * 396 * @throws IllegalArgumentException if an unrecognized PDF form type is returned 397 */ getPdfFormType()398 public int getPdfFormType() { 399 synchronized (sPdfiumLock) { 400 assertPdfDocumentNotNull(); 401 int pdfFormType = mPdfDocument.getFormType(); 402 return switch (pdfFormType) { 403 case PDF_FORM_TYPE_ACRO_FORM, PDF_FORM_TYPE_XFA_FULL, 404 PDF_FORM_TYPE_XFA_FOREGROUND -> 405 pdfFormType; 406 default -> PDF_FORM_TYPE_NONE; 407 }; 408 } 409 } 410 411 /** Returns true if this PDF prefers to be scaled for printing. */ scaleForPrinting()412 public boolean scaleForPrinting() { 413 synchronized (sPdfiumLock) { 414 assertPdfDocumentNotNull(); 415 return mPdfDocument.scaleForPrinting(); 416 } 417 } 418 419 /** 420 * Returns information about all form widgets on the page, or an empty list if there are no form 421 * widgets on the page. 422 * 423 * <p>Optionally restricted by {@code types}. If {@code types} is empty, all form widgets on the 424 * page will be returned. 425 */ 426 @NonNull getFormWidgetInfos(int pageNum, @NonNull @FormWidgetInfo.WidgetType int[] types)427 public List<FormWidgetInfo> getFormWidgetInfos(int pageNum, 428 @NonNull @FormWidgetInfo.WidgetType int[] types) { 429 synchronized (sPdfiumLock) { 430 assertPdfDocumentNotNull(); 431 return mPdfDocument.getFormWidgetInfos(pageNum, types); 432 } 433 } 434 435 /** 436 * Returns information about the widget with {@code annotationIndex}. 437 * 438 * <p>{@code annotationIndex} refers to the index of the annotation within the page's "Annot" 439 * array in the PDF document. This info is available on results of previous calls via {@link 440 * FormWidgetInfo#getWidgetIndex()}. 441 */ 442 @NonNull getFormWidgetInfoAtIndex(int pageNum, int annotationIndex)443 FormWidgetInfo getFormWidgetInfoAtIndex(int pageNum, int annotationIndex) { 444 synchronized (sPdfiumLock) { 445 assertPdfDocumentNotNull(); 446 FormWidgetInfo result = mPdfDocument.getFormWidgetInfo(pageNum, annotationIndex); 447 if (result == null) { 448 throw new IllegalArgumentException("No widget found at this index."); 449 } 450 return result; 451 } 452 } 453 454 /** Returns information about the widget at the given point. */ 455 @NonNull getFormWidgetInfoAtPosition(int pageNum, int x, int y)456 public FormWidgetInfo getFormWidgetInfoAtPosition(int pageNum, int x, int y) { 457 synchronized (sPdfiumLock) { 458 assertPdfDocumentNotNull(); 459 FormWidgetInfo result = mPdfDocument.getFormWidgetInfo(pageNum, x, y); 460 if (result == null) { 461 throw new IllegalArgumentException("No widget found at point."); 462 } 463 return result; 464 } 465 466 } 467 468 /** 469 * Applies a {@link FormEditRecord} to the PDF. 470 * 471 * @return a list of rectangular areas invalidated by form widget operation 472 * <p>For click type {@link FormEditRecord}s, performs a click on {@link 473 * FormEditRecord#getClickPoint()} 474 * <p>For set text type {@link FormEditRecord}s, sets the text value of the form widget. 475 * <p>For set indices type {@link FormEditRecord}s, sets the {@link 476 * FormEditRecord#getSelectedIndices()} as selected and all others as unselected for the 477 * form widget indicated by the record. 478 */ 479 @NonNull applyEdit(int pageNum, @NonNull FormEditRecord editRecord)480 public List<Rect> applyEdit(int pageNum, @NonNull FormEditRecord editRecord) { 481 Preconditions.checkNotNull(editRecord, "Edit record cannot be null"); 482 Preconditions.checkArgument(pageNum >= 0, "Invalid page number"); 483 if (editRecord.getType() == FormEditRecord.EDIT_TYPE_CLICK) { 484 return applyEditTypeClick(pageNum, editRecord); 485 } else if (editRecord.getType() == FormEditRecord.EDIT_TYPE_SET_INDICES) { 486 return applyEditTypeSetIndices(pageNum, editRecord); 487 } else if (editRecord.getType() == FormEditRecord.EDIT_TYPE_SET_TEXT) { 488 return applyEditSetText(pageNum, editRecord); 489 } 490 return Collections.emptyList(); 491 } 492 493 @FlaggedApi(Flags.FLAG_ENABLE_FORM_FILLING) applyEditTypeClick(int pageNum, @NonNull FormEditRecord editRecord)494 private List<Rect> applyEditTypeClick(int pageNum, @NonNull FormEditRecord editRecord) { 495 Preconditions.checkNotNull(editRecord.getClickPoint(), 496 "Can't apply click edit record without point"); 497 Point clickPoint = editRecord.getClickPoint(); 498 synchronized (sPdfiumLock) { 499 assertPdfDocumentNotNull(); 500 List<Rect> results = mPdfDocument.clickOnPage(pageNum, clickPoint.x, clickPoint.y); 501 if (results == null) { 502 throw new IllegalArgumentException("Cannot click on this widget."); 503 } 504 return results; 505 } 506 } 507 508 @FlaggedApi(Flags.FLAG_ENABLE_FORM_FILLING) applyEditTypeSetIndices(int pageNum, @NonNull FormEditRecord editRecord)509 private List<Rect> applyEditTypeSetIndices(int pageNum, @NonNull FormEditRecord editRecord) { 510 synchronized (sPdfiumLock) { 511 assertPdfDocumentNotNull(); 512 int[] selectedIndices = editRecord.getSelectedIndices(); 513 List<Rect> results = mPdfDocument.setFormFieldSelectedIndices(pageNum, 514 editRecord.getWidgetIndex(), selectedIndices); 515 if (results == null) { 516 throw new IllegalArgumentException("Cannot set selected indices on this widget."); 517 } 518 return results; 519 } 520 } 521 522 @FlaggedApi(Flags.FLAG_ENABLE_FORM_FILLING) applyEditSetText(int pageNum, @NonNull FormEditRecord editRecord)523 private List<Rect> applyEditSetText(int pageNum, @NonNull FormEditRecord editRecord) { 524 Preconditions.checkNotNull(editRecord.getText(), 525 "Can't apply set text record without text"); 526 String text = editRecord.getText(); 527 synchronized (sPdfiumLock) { 528 assertPdfDocumentNotNull(); 529 List<Rect> results = mPdfDocument.setFormFieldText(pageNum, editRecord.getWidgetIndex(), 530 text); 531 if (results == null) { 532 throw new IllegalArgumentException("Cannot set form field text on this widget."); 533 } 534 return results; 535 } 536 } 537 538 /** Ensures that any previous {@link PdfDocumentProxy} instance is closed. */ ensurePdfDestroyed()539 public void ensurePdfDestroyed() { 540 synchronized (sPdfiumLock) { 541 if (mPdfDocument != null) { 542 try { 543 mPdfDocument.destroy(); 544 } catch (Throwable t) { 545 Log.e(TAG, "Error closing PdfDocumentProxy", t); 546 } finally { 547 mPdfDocument = null; 548 } 549 } 550 } 551 } 552 553 /** 554 * Saves the current state of the loaded PDF document to the given writable 555 * ParcelFileDescriptor. 556 */ write(ParcelFileDescriptor destination, boolean removePasswordProtection)557 public void write(ParcelFileDescriptor destination, boolean removePasswordProtection) { 558 Preconditions.checkNotNull(destination, "Destination FD cannot be null"); 559 if (removePasswordProtection) { 560 cloneWithoutSecurity(destination); 561 } else { 562 saveAs(destination); 563 } 564 } 565 566 /** 567 * Creates a copy of the current document without security, if it is password protected. This 568 * may be necessary for the PrintManager which can't handle password-protected files. 569 * 570 * @param destination points to where pdfclient should make a copy of the pdf without security. 571 */ cloneWithoutSecurity(ParcelFileDescriptor destination)572 private void cloneWithoutSecurity(ParcelFileDescriptor destination) { 573 synchronized (sPdfiumLock) { 574 assertPdfDocumentNotNull(); 575 mPdfDocument.cloneWithoutSecurity(destination); 576 } 577 } 578 579 /** 580 * Saves the current document to the given {@link ParcelFileDescriptor}. 581 * 582 * @param destination where the currently open PDF should be written. 583 */ saveAs(ParcelFileDescriptor destination)584 private void saveAs(ParcelFileDescriptor destination) { 585 synchronized (sPdfiumLock) { 586 assertPdfDocumentNotNull(); 587 mPdfDocument.saveAs(destination); 588 } 589 } 590 clipInBitmap(@ullable Rect clip, Bitmap destination)591 private boolean clipInBitmap(@Nullable Rect clip, Bitmap destination) { 592 if (clip == null) { 593 return true; 594 } 595 return clip.left >= 0 && clip.top >= 0 && clip.right <= destination.getWidth() 596 && clip.bottom <= destination.getHeight(); 597 } 598 assertPdfDocumentNotNull()599 private void assertPdfDocumentNotNull() { 600 Preconditions.checkNotNull(mPdfDocument, "PdfDocumentProxy cannot be null"); 601 } 602 603 } 604