| /* |
| * Copyright (C) 2014 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.graphics.pdf; |
| |
| import android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.UnsupportedAppUsage; |
| import android.graphics.Bitmap; |
| import android.graphics.Bitmap.Config; |
| import android.graphics.Matrix; |
| import android.graphics.Point; |
| import android.graphics.Rect; |
| import android.os.ParcelFileDescriptor; |
| import android.system.ErrnoException; |
| import android.system.Os; |
| import android.system.OsConstants; |
| import com.android.internal.util.Preconditions; |
| import dalvik.system.CloseGuard; |
| |
| import libcore.io.IoUtils; |
| |
| import java.io.IOException; |
| import java.lang.annotation.Retention; |
| import java.lang.annotation.RetentionPolicy; |
| |
| /** |
| * <p> |
| * This class enables rendering a PDF document. This class is not thread safe. |
| * </p> |
| * <p> |
| * If you want to render a PDF, you create a renderer and for every page you want |
| * to render, you open the page, render it, and close the page. After you are done |
| * with rendering, you close the renderer. After the renderer is closed it should not |
| * be used anymore. Note that the pages are rendered one by one, i.e. you can have |
| * only a single page opened at any given time. |
| * </p> |
| * <p> |
| * A typical use of the APIs to render a PDF looks like this: |
| * </p> |
| * <pre> |
| * // create a new renderer |
| * PdfRenderer renderer = new PdfRenderer(getSeekableFileDescriptor()); |
| * |
| * // let us just render all pages |
| * final int pageCount = renderer.getPageCount(); |
| * for (int i = 0; i < pageCount; i++) { |
| * Page page = renderer.openPage(i); |
| * |
| * // say we render for showing on the screen |
| * page.render(mBitmap, null, null, Page.RENDER_MODE_FOR_DISPLAY); |
| * |
| * // do stuff with the bitmap |
| * |
| * // close the page |
| * page.close(); |
| * } |
| * |
| * // close the renderer |
| * renderer.close(); |
| * </pre> |
| * |
| * <h3>Print preview and print output</h3> |
| * <p> |
| * If you are using this class to rasterize a PDF for printing or show a print |
| * preview, it is recommended that you respect the following contract in order |
| * to provide a consistent user experience when seeing a preview and printing, |
| * i.e. the user sees a preview that is the same as the printout. |
| * </p> |
| * <ul> |
| * <li> |
| * Respect the property whether the document would like to be scaled for printing |
| * as per {@link #shouldScaleForPrinting()}. |
| * </li> |
| * <li> |
| * When scaling a document for printing the aspect ratio should be preserved. |
| * </li> |
| * <li> |
| * Do not inset the content with any margins from the {@link android.print.PrintAttributes} |
| * as the application is responsible to render it such that the margins are respected. |
| * </li> |
| * <li> |
| * If document page size is greater than the printed media size the content should |
| * be anchored to the upper left corner of the page for left-to-right locales and |
| * top right corner for right-to-left locales. |
| * </li> |
| * </ul> |
| * |
| * @see #close() |
| */ |
| public final class PdfRenderer implements AutoCloseable { |
| /** |
| * Any call the native pdfium code has to be single threaded as the library does not support |
| * parallel use. |
| */ |
| final static Object sPdfiumLock = new Object(); |
| |
| private final CloseGuard mCloseGuard = CloseGuard.get(); |
| |
| private final Point mTempPoint = new Point(); |
| |
| private long mNativeDocument; |
| |
| private final int mPageCount; |
| |
| private ParcelFileDescriptor mInput; |
| |
| @UnsupportedAppUsage |
| private Page mCurrentPage; |
| |
| /** @hide */ |
| @IntDef({ |
| Page.RENDER_MODE_FOR_DISPLAY, |
| Page.RENDER_MODE_FOR_PRINT |
| }) |
| @Retention(RetentionPolicy.SOURCE) |
| public @interface RenderMode {} |
| |
| /** |
| * Creates a new instance. |
| * <p> |
| * <strong>Note:</strong> The provided file descriptor must be <strong>seekable</strong>, |
| * i.e. its data being randomly accessed, e.g. pointing to a file. |
| * </p> |
| * <p> |
| * <strong>Note:</strong> This class takes ownership of the passed in file descriptor |
| * and is responsible for closing it when the renderer is closed. |
| * </p> |
| * <p> |
| * If the file is from an untrusted source it is recommended to run the renderer in a separate, |
| * isolated process with minimal permissions to limit the impact of security exploits. |
| * </p> |
| * |
| * @param input Seekable file descriptor to read from. |
| * |
| * @throws java.io.IOException If an error occurs while reading the file. |
| * @throws java.lang.SecurityException If the file requires a password or |
| * the security scheme is not supported. |
| */ |
| public PdfRenderer(@NonNull ParcelFileDescriptor input) throws IOException { |
| if (input == null) { |
| throw new NullPointerException("input cannot be null"); |
| } |
| |
| final long size; |
| try { |
| Os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET); |
| size = Os.fstat(input.getFileDescriptor()).st_size; |
| } catch (ErrnoException ee) { |
| throw new IllegalArgumentException("file descriptor not seekable"); |
| } |
| mInput = input; |
| |
| synchronized (sPdfiumLock) { |
| mNativeDocument = nativeCreate(mInput.getFd(), size); |
| try { |
| mPageCount = nativeGetPageCount(mNativeDocument); |
| } catch (Throwable t) { |
| nativeClose(mNativeDocument); |
| mNativeDocument = 0; |
| throw t; |
| } |
| } |
| |
| mCloseGuard.open("close"); |
| } |
| |
| /** |
| * Closes this renderer. You should not use this instance |
| * after this method is called. |
| */ |
| public void close() { |
| throwIfClosed(); |
| throwIfPageOpened(); |
| doClose(); |
| } |
| |
| /** |
| * Gets the number of pages in the document. |
| * |
| * @return The page count. |
| */ |
| public int getPageCount() { |
| throwIfClosed(); |
| return mPageCount; |
| } |
| |
| /** |
| * Gets whether the document prefers to be scaled for printing. |
| * You should take this info account if the document is rendered |
| * for printing and the target media size differs from the page |
| * size. |
| * |
| * @return If to scale the document. |
| */ |
| public boolean shouldScaleForPrinting() { |
| throwIfClosed(); |
| |
| synchronized (sPdfiumLock) { |
| return nativeScaleForPrinting(mNativeDocument); |
| } |
| } |
| |
| /** |
| * Opens a page for rendering. |
| * |
| * @param index The page index. |
| * @return A page that can be rendered. |
| * |
| * @see android.graphics.pdf.PdfRenderer.Page#close() PdfRenderer.Page.close() |
| */ |
| public Page openPage(int index) { |
| throwIfClosed(); |
| throwIfPageOpened(); |
| throwIfPageNotInDocument(index); |
| mCurrentPage = new Page(index); |
| return mCurrentPage; |
| } |
| |
| @Override |
| protected void finalize() throws Throwable { |
| try { |
| if (mCloseGuard != null) { |
| mCloseGuard.warnIfOpen(); |
| } |
| |
| doClose(); |
| } finally { |
| super.finalize(); |
| } |
| } |
| |
| @UnsupportedAppUsage |
| private void doClose() { |
| if (mCurrentPage != null) { |
| mCurrentPage.close(); |
| mCurrentPage = null; |
| } |
| |
| if (mNativeDocument != 0) { |
| synchronized (sPdfiumLock) { |
| nativeClose(mNativeDocument); |
| } |
| mNativeDocument = 0; |
| } |
| |
| if (mInput != null) { |
| IoUtils.closeQuietly(mInput); |
| mInput = null; |
| } |
| mCloseGuard.close(); |
| } |
| |
| private void throwIfClosed() { |
| if (mInput == null) { |
| throw new IllegalStateException("Already closed"); |
| } |
| } |
| |
| private void throwIfPageOpened() { |
| if (mCurrentPage != null) { |
| throw new IllegalStateException("Current page not closed"); |
| } |
| } |
| |
| private void throwIfPageNotInDocument(int pageIndex) { |
| if (pageIndex < 0 || pageIndex >= mPageCount) { |
| throw new IllegalArgumentException("Invalid page index"); |
| } |
| } |
| |
| /** |
| * This class represents a PDF document page for rendering. |
| */ |
| public final class Page implements AutoCloseable { |
| |
| private final CloseGuard mCloseGuard = CloseGuard.get(); |
| |
| /** |
| * Mode to render the content for display on a screen. |
| */ |
| public static final int RENDER_MODE_FOR_DISPLAY = 1; |
| |
| /** |
| * Mode to render the content for printing. |
| */ |
| public static final int RENDER_MODE_FOR_PRINT = 2; |
| |
| private final int mIndex; |
| private final int mWidth; |
| private final int mHeight; |
| |
| private long mNativePage; |
| |
| private Page(int index) { |
| Point size = mTempPoint; |
| synchronized (sPdfiumLock) { |
| mNativePage = nativeOpenPageAndGetSize(mNativeDocument, index, size); |
| } |
| mIndex = index; |
| mWidth = size.x; |
| mHeight = size.y; |
| mCloseGuard.open("close"); |
| } |
| |
| /** |
| * Gets the page index. |
| * |
| * @return The index. |
| */ |
| public int getIndex() { |
| return mIndex; |
| } |
| |
| /** |
| * Gets the page width in points (1/72"). |
| * |
| * @return The width in points. |
| */ |
| public int getWidth() { |
| return mWidth; |
| } |
| |
| /** |
| * Gets the page height in points (1/72"). |
| * |
| * @return The height in points. |
| */ |
| public int getHeight() { |
| return mHeight; |
| } |
| |
| /** |
| * Renders a page to a bitmap. |
| * <p> |
| * You may optionally specify a rectangular clip in the bitmap bounds. No rendering |
| * outside the clip will be performed, hence it is your responsibility to initialize |
| * the bitmap outside the clip. |
| * </p> |
| * <p> |
| * You may optionally specify a matrix to transform the content from page coordinates |
| * which are in points (1/72") to bitmap coordinates which are in pixels. If this |
| * matrix is not provided this method will apply a transformation that will fit the |
| * whole page to the destination clip if provided or the destination bitmap if no |
| * clip is provided. |
| * </p> |
| * <p> |
| * The clip and transformation are useful for implementing tile rendering where the |
| * destination bitmap contains a portion of the image, for example when zooming. |
| * Another useful application is for printing where the size of the bitmap holding |
| * the page is too large and a client can render the page in stripes. |
| * </p> |
| * <p> |
| * <strong>Note: </strong> The destination bitmap format must be |
| * {@link Config#ARGB_8888 ARGB}. |
| * </p> |
| * <p> |
| * <strong>Note: </strong> The optional transformation matrix must be affine as per |
| * {@link android.graphics.Matrix#isAffine() Matrix.isAffine()}. Hence, you can specify |
| * rotation, scaling, translation but not a perspective transformation. |
| * </p> |
| * |
| * @param destination Destination bitmap to which to render. |
| * @param destClip Optional clip in the bitmap bounds. |
| * @param transform Optional transformation to apply when rendering. |
| * @param renderMode The render mode. |
| * |
| * @see #RENDER_MODE_FOR_DISPLAY |
| * @see #RENDER_MODE_FOR_PRINT |
| */ |
| public void render(@NonNull Bitmap destination, @Nullable Rect destClip, |
| @Nullable Matrix transform, @RenderMode int renderMode) { |
| if (mNativePage == 0) { |
| throw new NullPointerException(); |
| } |
| |
| destination = Preconditions.checkNotNull(destination, "bitmap null"); |
| |
| if (destination.getConfig() != Config.ARGB_8888) { |
| throw new IllegalArgumentException("Unsupported pixel format"); |
| } |
| |
| if (destClip != null) { |
| if (destClip.left < 0 || destClip.top < 0 |
| || destClip.right > destination.getWidth() |
| || destClip.bottom > destination.getHeight()) { |
| throw new IllegalArgumentException("destBounds not in destination"); |
| } |
| } |
| |
| if (transform != null && !transform.isAffine()) { |
| throw new IllegalArgumentException("transform not affine"); |
| } |
| |
| if (renderMode != RENDER_MODE_FOR_PRINT && renderMode != RENDER_MODE_FOR_DISPLAY) { |
| throw new IllegalArgumentException("Unsupported render mode"); |
| } |
| |
| if (renderMode == RENDER_MODE_FOR_PRINT && renderMode == RENDER_MODE_FOR_DISPLAY) { |
| throw new IllegalArgumentException("Only single render mode supported"); |
| } |
| |
| final int contentLeft = (destClip != null) ? destClip.left : 0; |
| final int contentTop = (destClip != null) ? destClip.top : 0; |
| final int contentRight = (destClip != null) ? destClip.right |
| : destination.getWidth(); |
| final int contentBottom = (destClip != null) ? destClip.bottom |
| : destination.getHeight(); |
| |
| // If transform is not set, stretch page to whole clipped area |
| if (transform == null) { |
| int clipWidth = contentRight - contentLeft; |
| int clipHeight = contentBottom - contentTop; |
| |
| transform = new Matrix(); |
| transform.postScale((float)clipWidth / getWidth(), |
| (float)clipHeight / getHeight()); |
| transform.postTranslate(contentLeft, contentTop); |
| } |
| |
| final long transformPtr = transform.native_instance; |
| |
| synchronized (sPdfiumLock) { |
| nativeRenderPage(mNativeDocument, mNativePage, destination, contentLeft, |
| contentTop, contentRight, contentBottom, transformPtr, renderMode); |
| } |
| } |
| |
| /** |
| * Closes this page. |
| * |
| * @see android.graphics.pdf.PdfRenderer#openPage(int) |
| */ |
| @Override |
| public void close() { |
| throwIfClosed(); |
| doClose(); |
| } |
| |
| @Override |
| protected void finalize() throws Throwable { |
| try { |
| if (mCloseGuard != null) { |
| mCloseGuard.warnIfOpen(); |
| } |
| |
| doClose(); |
| } finally { |
| super.finalize(); |
| } |
| } |
| |
| private void doClose() { |
| if (mNativePage != 0) { |
| synchronized (sPdfiumLock) { |
| nativeClosePage(mNativePage); |
| } |
| mNativePage = 0; |
| } |
| |
| mCloseGuard.close(); |
| mCurrentPage = null; |
| } |
| |
| private void throwIfClosed() { |
| if (mNativePage == 0) { |
| throw new IllegalStateException("Already closed"); |
| } |
| } |
| } |
| |
| private static native long nativeCreate(int fd, long size); |
| private static native void nativeClose(long documentPtr); |
| private static native int nativeGetPageCount(long documentPtr); |
| private static native boolean nativeScaleForPrinting(long documentPtr); |
| private static native void nativeRenderPage(long documentPtr, long pagePtr, Bitmap dest, |
| int clipLeft, int clipTop, int clipRight, int clipBottom, long transformPtr, |
| int renderMode); |
| private static native long nativeOpenPageAndGetSize(long documentPtr, int pageIndex, |
| Point outSize); |
| private static native void nativeClosePage(long pagePtr); |
| } |