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