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