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