PdfRenderer.java revision 3d0d141710d23b0f08c865100b4d55b4c2976486
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    private final CloseGuard mCloseGuard = CloseGuard.get();
103
104    private final Point mTempPoint = new Point();
105
106    private final long mNativeDocument;
107
108    private final int mPageCount;
109
110    private ParcelFileDescriptor mInput;
111
112    private Page mCurrentPage;
113
114    /** @hide */
115    @IntDef({
116        Page.RENDER_MODE_FOR_DISPLAY,
117        Page.RENDER_MODE_FOR_PRINT
118    })
119    @Retention(RetentionPolicy.SOURCE)
120    public @interface RenderMode {}
121
122    /**
123     * Creates a new instance.
124     * <p>
125     * <strong>Note:</strong> The provided file descriptor must be <strong>seekable</strong>,
126     * i.e. its data being randomly accessed, e.g. pointing to a file.
127     * </p>
128     * <p>
129     * <strong>Note:</strong> This class takes ownership of the passed in file descriptor
130     * and is responsible for closing it when the renderer is closed.
131     * </p>
132     * <p>
133     * If the file is from an untrusted source it is recommended to run the renderer in a separate,
134     * isolated process with minimal permissions to limit the impact of security exploits.
135     * </p>
136     *
137     * @param input Seekable file descriptor to read from.
138     *
139     * @throws java.io.IOException If an error occurs while reading the file.
140     * @throws java.lang.SecurityException If the file requires a password or
141     *         the security scheme is not supported.
142     */
143    public PdfRenderer(@NonNull ParcelFileDescriptor input) throws IOException {
144        if (input == null) {
145            throw new NullPointerException("input cannot be null");
146        }
147
148        final long size;
149        try {
150            Libcore.os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET);
151            size = Libcore.os.fstat(input.getFileDescriptor()).st_size;
152        } catch (ErrnoException ee) {
153            throw new IllegalArgumentException("file descriptor not seekable");
154        }
155
156        mInput = input;
157        mNativeDocument = nativeCreate(mInput.getFd(), size);
158        mPageCount = nativeGetPageCount(mNativeDocument);
159        mCloseGuard.open("close");
160    }
161
162    /**
163     * Closes this renderer. You should not use this instance
164     * after this method is called.
165     */
166    public void close() {
167        throwIfClosed();
168        throwIfPageOpened();
169        doClose();
170    }
171
172    /**
173     * Gets the number of pages in the document.
174     *
175     * @return The page count.
176     */
177    public int getPageCount() {
178        throwIfClosed();
179        return mPageCount;
180    }
181
182    /**
183     * Gets whether the document prefers to be scaled for printing.
184     * You should take this info account if the document is rendered
185     * for printing and the target media size differs from the page
186     * size.
187     *
188     * @return If to scale the document.
189     */
190    public boolean shouldScaleForPrinting() {
191        throwIfClosed();
192        return nativeScaleForPrinting(mNativeDocument);
193    }
194
195    /**
196     * Opens a page for rendering.
197     *
198     * @param index The page index.
199     * @return A page that can be rendered.
200     *
201     * @see android.graphics.pdf.PdfRenderer.Page#close() PdfRenderer.Page.close()
202     */
203    public Page openPage(int index) {
204        throwIfClosed();
205        throwIfPageOpened();
206        throwIfPageNotInDocument(index);
207        mCurrentPage = new Page(index);
208        return mCurrentPage;
209    }
210
211    @Override
212    protected void finalize() throws Throwable {
213        try {
214            mCloseGuard.warnIfOpen();
215            if (mInput != null) {
216                doClose();
217            }
218        } finally {
219            super.finalize();
220        }
221    }
222
223    private void doClose() {
224        if (mCurrentPage != null) {
225            mCurrentPage.close();
226        }
227        nativeClose(mNativeDocument);
228        try {
229            mInput.close();
230        } catch (IOException ioe) {
231            /* ignore - best effort */
232        }
233        mInput = null;
234        mCloseGuard.close();
235    }
236
237    private void throwIfClosed() {
238        if (mInput == null) {
239            throw new IllegalStateException("Already closed");
240        }
241    }
242
243    private void throwIfPageOpened() {
244        if (mCurrentPage != null) {
245            throw new IllegalStateException("Current page not closed");
246        }
247    }
248
249    private void throwIfPageNotInDocument(int pageIndex) {
250        if (pageIndex < 0 || pageIndex >= mPageCount) {
251            throw new IllegalArgumentException("Invalid page index");
252        }
253    }
254
255    /**
256     * This class represents a PDF document page for rendering.
257     */
258    public final class Page implements AutoCloseable {
259
260        private final CloseGuard mCloseGuard = CloseGuard.get();
261
262        /**
263         * Mode to render the content for display on a screen.
264         */
265        public static final int RENDER_MODE_FOR_DISPLAY = 1;
266
267        /**
268         * Mode to render the content for printing.
269         */
270        public static final int RENDER_MODE_FOR_PRINT = 2;
271
272        private final int mIndex;
273        private final int mWidth;
274        private final int mHeight;
275
276        private long mNativePage;
277
278        private Page(int index) {
279            Point size = mTempPoint;
280            mNativePage = nativeOpenPageAndGetSize(mNativeDocument, index, size);
281            mIndex = index;
282            mWidth = size.x;
283            mHeight = size.y;
284            mCloseGuard.open("close");
285        }
286
287        /**
288         * Gets the page index.
289         *
290         * @return The index.
291         */
292        public int getIndex() {
293            return  mIndex;
294        }
295
296        /**
297         * Gets the page width in points (1/72").
298         *
299         * @return The width in points.
300         */
301        public int getWidth() {
302            return mWidth;
303        }
304
305        /**
306         * Gets the page height in points (1/72").
307         *
308         * @return The height in points.
309         */
310        public int getHeight() {
311            return mHeight;
312        }
313
314        /**
315         * Renders a page to a bitmap.
316         * <p>
317         * You may optionally specify a rectangular clip in the bitmap bounds. No rendering
318         * outside the clip will be performed, hence it is your responsibility to initialize
319         * the bitmap outside the clip.
320         * </p>
321         * <p>
322         * You may optionally specify a matrix to transform the content from page coordinates
323         * which are in points (1/72") to bitmap coordinates which are in pixels. If this
324         * matrix is not provided this method will apply a transformation that will fit the
325         * whole page to the destination clip if provided or the destination bitmap if no
326         * clip is provided.
327         * </p>
328         * <p>
329         * The clip and transformation are useful for implementing tile rendering where the
330         * destination bitmap contains a portion of the image, for example when zooming.
331         * Another useful application is for printing where the size of the bitmap holding
332         * the page is too large and a client can render the page in stripes.
333         * </p>
334         * <p>
335         * <strong>Note: </strong> The destination bitmap format must be
336         * {@link Config#ARGB_8888 ARGB}.
337         * </p>
338         * <p>
339         * <strong>Note: </strong> The optional transformation matrix must be affine as per
340         * {@link android.graphics.Matrix#isAffine() Matrix.isAffine()}. Hence, you can specify
341         * rotation, scaling, translation but not a perspective transformation.
342         * </p>
343         *
344         * @param destination Destination bitmap to which to render.
345         * @param destClip Optional clip in the bitmap bounds.
346         * @param transform Optional transformation to apply when rendering.
347         * @param renderMode The render mode.
348         *
349         * @see #RENDER_MODE_FOR_DISPLAY
350         * @see #RENDER_MODE_FOR_PRINT
351         */
352        public void render(@NonNull Bitmap destination, @Nullable Rect destClip,
353                           @Nullable Matrix transform, @RenderMode int renderMode) {
354            if (destination.getConfig() != Config.ARGB_8888) {
355                throw new IllegalArgumentException("Unsupported pixel format");
356            }
357
358            if (destClip != null) {
359                if (destClip.left < 0 || destClip.top < 0
360                        || destClip.right > destination.getWidth()
361                        || destClip.bottom > destination.getHeight()) {
362                    throw new IllegalArgumentException("destBounds not in destination");
363                }
364            }
365
366            if (transform != null && !transform.isAffine()) {
367                 throw new IllegalArgumentException("transform not affine");
368            }
369
370            if (renderMode != RENDER_MODE_FOR_PRINT && renderMode != RENDER_MODE_FOR_DISPLAY) {
371                throw new IllegalArgumentException("Unsupported render mode");
372            }
373
374            if (renderMode == RENDER_MODE_FOR_PRINT && renderMode == RENDER_MODE_FOR_DISPLAY) {
375                throw new IllegalArgumentException("Only single render mode supported");
376            }
377
378            final int contentLeft = (destClip != null) ? destClip.left : 0;
379            final int contentTop = (destClip != null) ? destClip.top : 0;
380            final int contentRight = (destClip != null) ? destClip.right
381                    : destination.getWidth();
382            final int contentBottom = (destClip != null) ? destClip.bottom
383                    : destination.getHeight();
384
385            final long transformPtr = (transform != null) ? transform.native_instance : 0;
386
387            nativeRenderPage(mNativeDocument, mNativePage, destination, contentLeft,
388                    contentTop, contentRight, contentBottom, transformPtr, renderMode);
389        }
390
391        /**
392         * Closes this page.
393         *
394         * @see android.graphics.pdf.PdfRenderer#openPage(int)
395         */
396        @Override
397        public void close() {
398            throwIfClosed();
399            doClose();
400        }
401
402        @Override
403        protected void finalize() throws Throwable {
404            try {
405                mCloseGuard.warnIfOpen();
406                if (mNativePage != 0) {
407                    doClose();
408                }
409            } finally {
410                super.finalize();
411            }
412        }
413
414        private void doClose() {
415            nativeClosePage(mNativePage);
416            mNativePage = 0;
417            mCloseGuard.close();
418            mCurrentPage = null;
419        }
420
421        private void throwIfClosed() {
422            if (mNativePage == 0) {
423                throw new IllegalStateException("Already closed");
424            }
425        }
426    }
427
428    private static native long nativeCreate(int fd, long size);
429    private static native void nativeClose(long documentPtr);
430    private static native int nativeGetPageCount(long documentPtr);
431    private static native boolean nativeScaleForPrinting(long documentPtr);
432    private static native void nativeRenderPage(long documentPtr, long pagePtr, Bitmap dest,
433            int destLeft, int destTop, int destRight, int destBottom, long matrixPtr, int renderMode);
434    private static native long nativeOpenPageAndGetSize(long documentPtr, int pageIndex,
435            Point outSize);
436    private static native void nativeClosePage(long pagePtr);
437}
438