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