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