PdfRenderer.java revision 366262dc7854ba54f64905df8d275358be41edf5
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            mPageCount = nativeGetPageCount(mNativeDocument);
167        }
168
169        mCloseGuard.open("close");
170    }
171
172    /**
173     * Closes this renderer. You should not use this instance
174     * after this method is called.
175     */
176    public void close() {
177        throwIfClosed();
178        throwIfPageOpened();
179        doClose();
180    }
181
182    /**
183     * Gets the number of pages in the document.
184     *
185     * @return The page count.
186     */
187    public int getPageCount() {
188        throwIfClosed();
189        return mPageCount;
190    }
191
192    /**
193     * Gets whether the document prefers to be scaled for printing.
194     * You should take this info account if the document is rendered
195     * for printing and the target media size differs from the page
196     * size.
197     *
198     * @return If to scale the document.
199     */
200    public boolean shouldScaleForPrinting() {
201        throwIfClosed();
202
203        synchronized (sPdfiumLock) {
204            return nativeScaleForPrinting(mNativeDocument);
205        }
206    }
207
208    /**
209     * Opens a page for rendering.
210     *
211     * @param index The page index.
212     * @return A page that can be rendered.
213     *
214     * @see android.graphics.pdf.PdfRenderer.Page#close() PdfRenderer.Page.close()
215     */
216    public Page openPage(int index) {
217        throwIfClosed();
218        throwIfPageOpened();
219        throwIfPageNotInDocument(index);
220        mCurrentPage = new Page(index);
221        return mCurrentPage;
222    }
223
224    @Override
225    protected void finalize() throws Throwable {
226        try {
227            mCloseGuard.warnIfOpen();
228            if (mInput != null) {
229                doClose();
230            }
231        } finally {
232            super.finalize();
233        }
234    }
235
236    private void doClose() {
237        if (mCurrentPage != null) {
238            mCurrentPage.close();
239        }
240        synchronized (sPdfiumLock) {
241            nativeClose(mNativeDocument);
242        }
243        try {
244            mInput.close();
245        } catch (IOException ioe) {
246            /* ignore - best effort */
247        }
248        mInput = null;
249        mCloseGuard.close();
250    }
251
252    private void throwIfClosed() {
253        if (mInput == null) {
254            throw new IllegalStateException("Already closed");
255        }
256    }
257
258    private void throwIfPageOpened() {
259        if (mCurrentPage != null) {
260            throw new IllegalStateException("Current page not closed");
261        }
262    }
263
264    private void throwIfPageNotInDocument(int pageIndex) {
265        if (pageIndex < 0 || pageIndex >= mPageCount) {
266            throw new IllegalArgumentException("Invalid page index");
267        }
268    }
269
270    /**
271     * This class represents a PDF document page for rendering.
272     */
273    public final class Page implements AutoCloseable {
274
275        private final CloseGuard mCloseGuard = CloseGuard.get();
276
277        /**
278         * Mode to render the content for display on a screen.
279         */
280        public static final int RENDER_MODE_FOR_DISPLAY = 1;
281
282        /**
283         * Mode to render the content for printing.
284         */
285        public static final int RENDER_MODE_FOR_PRINT = 2;
286
287        private final int mIndex;
288        private final int mWidth;
289        private final int mHeight;
290
291        private long mNativePage;
292
293        private Page(int index) {
294            Point size = mTempPoint;
295            synchronized (sPdfiumLock) {
296                mNativePage = nativeOpenPageAndGetSize(mNativeDocument, index, size);
297            }
298            mIndex = index;
299            mWidth = size.x;
300            mHeight = size.y;
301            mCloseGuard.open("close");
302        }
303
304        /**
305         * Gets the page index.
306         *
307         * @return The index.
308         */
309        public int getIndex() {
310            return  mIndex;
311        }
312
313        /**
314         * Gets the page width in points (1/72").
315         *
316         * @return The width in points.
317         */
318        public int getWidth() {
319            return mWidth;
320        }
321
322        /**
323         * Gets the page height in points (1/72").
324         *
325         * @return The height in points.
326         */
327        public int getHeight() {
328            return mHeight;
329        }
330
331        /**
332         * Renders a page to a bitmap.
333         * <p>
334         * You may optionally specify a rectangular clip in the bitmap bounds. No rendering
335         * outside the clip will be performed, hence it is your responsibility to initialize
336         * the bitmap outside the clip.
337         * </p>
338         * <p>
339         * You may optionally specify a matrix to transform the content from page coordinates
340         * which are in points (1/72") to bitmap coordinates which are in pixels. If this
341         * matrix is not provided this method will apply a transformation that will fit the
342         * whole page to the destination clip if provided or the destination bitmap if no
343         * clip is provided.
344         * </p>
345         * <p>
346         * The clip and transformation are useful for implementing tile rendering where the
347         * destination bitmap contains a portion of the image, for example when zooming.
348         * Another useful application is for printing where the size of the bitmap holding
349         * the page is too large and a client can render the page in stripes.
350         * </p>
351         * <p>
352         * <strong>Note: </strong> The destination bitmap format must be
353         * {@link Config#ARGB_8888 ARGB}.
354         * </p>
355         * <p>
356         * <strong>Note: </strong> The optional transformation matrix must be affine as per
357         * {@link android.graphics.Matrix#isAffine() Matrix.isAffine()}. Hence, you can specify
358         * rotation, scaling, translation but not a perspective transformation.
359         * </p>
360         *
361         * @param destination Destination bitmap to which to render.
362         * @param destClip Optional clip in the bitmap bounds.
363         * @param transform Optional transformation to apply when rendering.
364         * @param renderMode The render mode.
365         *
366         * @see #RENDER_MODE_FOR_DISPLAY
367         * @see #RENDER_MODE_FOR_PRINT
368         */
369        public void render(@NonNull Bitmap destination, @Nullable Rect destClip,
370                           @Nullable Matrix transform, @RenderMode int renderMode) {
371            if (destination.getConfig() != Config.ARGB_8888) {
372                throw new IllegalArgumentException("Unsupported pixel format");
373            }
374
375            if (destClip != null) {
376                if (destClip.left < 0 || destClip.top < 0
377                        || destClip.right > destination.getWidth()
378                        || destClip.bottom > destination.getHeight()) {
379                    throw new IllegalArgumentException("destBounds not in destination");
380                }
381            }
382
383            if (transform != null && !transform.isAffine()) {
384                 throw new IllegalArgumentException("transform not affine");
385            }
386
387            if (renderMode != RENDER_MODE_FOR_PRINT && renderMode != RENDER_MODE_FOR_DISPLAY) {
388                throw new IllegalArgumentException("Unsupported render mode");
389            }
390
391            if (renderMode == RENDER_MODE_FOR_PRINT && renderMode == RENDER_MODE_FOR_DISPLAY) {
392                throw new IllegalArgumentException("Only single render mode supported");
393            }
394
395            final int contentLeft = (destClip != null) ? destClip.left : 0;
396            final int contentTop = (destClip != null) ? destClip.top : 0;
397            final int contentRight = (destClip != null) ? destClip.right
398                    : destination.getWidth();
399            final int contentBottom = (destClip != null) ? destClip.bottom
400                    : destination.getHeight();
401
402            final long transformPtr = (transform != null) ? transform.native_instance : 0;
403
404            synchronized (sPdfiumLock) {
405                nativeRenderPage(mNativeDocument, mNativePage, destination, contentLeft,
406                        contentTop, contentRight, contentBottom, transformPtr, renderMode);
407            }
408        }
409
410        /**
411         * Closes this page.
412         *
413         * @see android.graphics.pdf.PdfRenderer#openPage(int)
414         */
415        @Override
416        public void close() {
417            throwIfClosed();
418            doClose();
419        }
420
421        @Override
422        protected void finalize() throws Throwable {
423            try {
424                mCloseGuard.warnIfOpen();
425                if (mNativePage != 0) {
426                    doClose();
427                }
428            } finally {
429                super.finalize();
430            }
431        }
432
433        private void doClose() {
434            synchronized (sPdfiumLock) {
435                nativeClosePage(mNativePage);
436            }
437            mNativePage = 0;
438            mCloseGuard.close();
439            mCurrentPage = null;
440        }
441
442        private void throwIfClosed() {
443            if (mNativePage == 0) {
444                throw new IllegalStateException("Already closed");
445            }
446        }
447    }
448
449    private static native long nativeCreate(int fd, long size);
450    private static native void nativeClose(long documentPtr);
451    private static native int nativeGetPageCount(long documentPtr);
452    private static native boolean nativeScaleForPrinting(long documentPtr);
453    private static native void nativeRenderPage(long documentPtr, long pagePtr, Bitmap dest,
454            int destLeft, int destTop, int destRight, int destBottom, long matrixPtr, int renderMode);
455    private static native long nativeOpenPageAndGetSize(long documentPtr, int pageIndex,
456            Point outSize);
457    private static native void nativeClosePage(long pagePtr);
458}
459