• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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