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