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