blob: 54710e58687c0d3fb8ba4f1c972149943152035e [file] [log] [blame]
/*
* 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.compat.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.getNativeInstance(),
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, long bitmapHandle,
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);
}