Files
frameworks_base/graphics/java/android/graphics/pdf/PdfDocument.java
Svetoslav 2961769ea9 Adding APIs to render PDF documents.
We need to render PDF documents for two main use cases. First,
for print preview. Second, for resterizing the PDF document by
a print service before passing it to a printer which does not
natively support PDF (most consumer ones).

Adding PDF rendering APIs improves guarantees for print quality
as the same library is used for preview and rasterization. Also
print vendors do not have to license third-party rendering engines.
Last but not least as the platform uses PDF as its main print
format it should also be able to natively render it.

Change-Id: I57004a435db147663cafea40cf3296465aba7f99
2014-05-13 18:32:42 -07:00

457 lines
15 KiB
Java

/*
* Copyright (C) 2013 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.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import dalvik.system.CloseGuard;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* <p>
* This class enables generating a PDF document from native Android content. You
* create a new document and then for every page you want to add you start a page,
* write content to the page, and finish the page. After you are done with all
* pages, you write the document to an output stream and close the document.
* After a document is closed you should not use it anymore. Note that pages are
* created one by one, i.e. you can have only a single page to which you are
* writing at any given time. This class is not thread safe.
* </p>
* <p>
* A typical use of the APIs looks like this:
* </p>
* <pre>
* // create a new document
* PdfDocument document = new PdfDocument();
*
* // crate a page description
* PageInfo pageInfo = new PageInfo.Builder(new Rect(0, 0, 100, 100), 1).create();
*
* // start a page
* Page page = document.startPage(pageInfo);
*
* // draw something on the page
* View content = getContentView();
* content.draw(page.getCanvas());
*
* // finish the page
* document.finishPage(page);
* . . .
* // add more pages
* . . .
* // write the document content
* document.writeTo(getOutputStream());
*
* // close the document
* document.close();
* </pre>
*/
public class PdfDocument {
// TODO: We need a constructor that will take an OutputStream to
// support online data serialization as opposed to the current
// on demand one. The current approach is fine until Skia starts
// to support online PDF generation at which point we need to
// handle this.
private final byte[] mChunk = new byte[4096];
private final CloseGuard mCloseGuard = CloseGuard.get();
private final List<PageInfo> mPages = new ArrayList<PageInfo>();
private long mNativeDocument;
private Page mCurrentPage;
/**
* Creates a new instance.
*/
public PdfDocument() {
mNativeDocument = nativeCreateDocument();
mCloseGuard.open("close");
}
/**
* Starts a page using the provided {@link PageInfo}. After the page
* is created you can draw arbitrary content on the page's canvas which
* you can get by calling {@link Page#getCanvas()}. After you are done
* drawing the content you should finish the page by calling
* {@link #finishPage(Page)}. After the page is finished you should
* no longer access the page or its canvas.
* <p>
* <strong>Note:</strong> Do not call this method after {@link #close()}.
* Also do not call this method if the last page returned by this method
* is not finished by calling {@link #finishPage(Page)}.
* </p>
*
* @param pageInfo The page info. Cannot be null.
* @return A blank page.
*
* @see #finishPage(Page)
*/
public Page startPage(PageInfo pageInfo) {
throwIfClosed();
throwIfCurrentPageNotFinished();
if (pageInfo == null) {
throw new IllegalArgumentException("page cannot be null");
}
Canvas canvas = new PdfCanvas(nativeStartPage(mNativeDocument, pageInfo.mPageWidth,
pageInfo.mPageHeight, pageInfo.mContentRect.left, pageInfo.mContentRect.top,
pageInfo.mContentRect.right, pageInfo.mContentRect.bottom));
mCurrentPage = new Page(canvas, pageInfo);
return mCurrentPage;
}
/**
* Finishes a started page. You should always finish the last started page.
* <p>
* <strong>Note:</strong> Do not call this method after {@link #close()}.
* You should not finish the same page more than once.
* </p>
*
* @param page The page. Cannot be null.
*
* @see #startPage(PageInfo)
*/
public void finishPage(Page page) {
throwIfClosed();
if (page == null) {
throw new IllegalArgumentException("page cannot be null");
}
if (page != mCurrentPage) {
throw new IllegalStateException("invalid page");
}
if (page.isFinished()) {
throw new IllegalStateException("page already finished");
}
mPages.add(page.getInfo());
mCurrentPage = null;
nativeFinishPage(mNativeDocument);
page.finish();
}
/**
* Writes the document to an output stream. You can call this method
* multiple times.
* <p>
* <strong>Note:</strong> Do not call this method after {@link #close()}.
* Also do not call this method if a page returned by {@link #startPage(
* PageInfo)} is not finished by calling {@link #finishPage(Page)}.
* </p>
*
* @param out The output stream. Cannot be null.
*
* @throws IOException If an error occurs while writing.
*/
public void writeTo(OutputStream out) throws IOException {
throwIfClosed();
throwIfCurrentPageNotFinished();
if (out == null) {
throw new IllegalArgumentException("out cannot be null!");
}
nativeWriteTo(mNativeDocument, out, mChunk);
}
/**
* Gets the pages of the document.
*
* @return The pages or an empty list.
*/
public List<PageInfo> getPages() {
return Collections.unmodifiableList(mPages);
}
/**
* Closes this document. This method should be called after you
* are done working with the document. After this call the document
* is considered closed and none of its methods should be called.
* <p>
* <strong>Note:</strong> Do not call this method if the page
* returned by {@link #startPage(PageInfo)} is not finished by
* calling {@link #finishPage(Page)}.
* </p>
*/
public void close() {
throwIfCurrentPageNotFinished();
dispose();
}
@Override
protected void finalize() throws Throwable {
try {
mCloseGuard.warnIfOpen();
dispose();
} finally {
super.finalize();
}
}
private void dispose() {
if (mNativeDocument != 0) {
nativeClose(mNativeDocument);
mCloseGuard.close();
mNativeDocument = 0;
}
}
/**
* Throws an exception if the document is already closed.
*/
private void throwIfClosed() {
if (mNativeDocument == 0) {
throw new IllegalStateException("document is closed!");
}
}
/**
* Throws an exception if the last started page is not finished.
*/
private void throwIfCurrentPageNotFinished() {
if (mCurrentPage != null) {
throw new IllegalStateException("Current page not finished!");
}
}
private native long nativeCreateDocument();
private native void nativeClose(long nativeDocument);
private native void nativeFinishPage(long nativeDocument);
private native void nativeWriteTo(long nativeDocument, OutputStream out, byte[] chunk);
private static native long nativeStartPage(long nativeDocument, int pageWidth, int pageHeight,
int contentLeft, int contentTop, int contentRight, int contentBottom);
private final class PdfCanvas extends Canvas {
public PdfCanvas(long nativeCanvas) {
super(nativeCanvas);
}
@Override
public void setBitmap(Bitmap bitmap) {
throw new UnsupportedOperationException();
}
}
/**
* This class represents meta-data that describes a PDF {@link Page}.
*/
public static final class PageInfo {
private int mPageWidth;
private int mPageHeight;
private Rect mContentRect;
private int mPageNumber;
/**
* Creates a new instance.
*/
private PageInfo() {
/* do nothing */
}
/**
* Gets the page width in PostScript points (1/72th of an inch).
*
* @return The page width.
*/
public int getPageWidth() {
return mPageWidth;
}
/**
* Gets the page height in PostScript points (1/72th of an inch).
*
* @return The page height.
*/
public int getPageHeight() {
return mPageHeight;
}
/**
* Get the content rectangle in PostScript points (1/72th of an inch).
* This is the area that contains the page content and is relative to
* the page top left.
*
* @return The content rectangle.
*/
public Rect getContentRect() {
return mContentRect;
}
/**
* Gets the page number.
*
* @return The page number.
*/
public int getPageNumber() {
return mPageNumber;
}
/**
* Builder for creating a {@link PageInfo}.
*/
public static final class Builder {
private final PageInfo mPageInfo = new PageInfo();
/**
* Creates a new builder with the mandatory page info attributes.
*
* @param pageWidth The page width in PostScript (1/72th of an inch).
* @param pageHeight The page height in PostScript (1/72th of an inch).
* @param pageNumber The page number.
*/
public Builder(int pageWidth, int pageHeight, int pageNumber) {
if (pageWidth <= 0) {
throw new IllegalArgumentException("page width must be positive");
}
if (pageHeight <= 0) {
throw new IllegalArgumentException("page width must be positive");
}
if (pageNumber < 0) {
throw new IllegalArgumentException("pageNumber must be non negative");
}
mPageInfo.mPageWidth = pageWidth;
mPageInfo.mPageHeight = pageHeight;
mPageInfo.mPageNumber = pageNumber;
}
/**
* Sets the content rectangle in PostScript point (1/72th of an inch).
* This is the area that contains the page content and is relative to
* the page top left.
*
* @param contentRect The content rectangle. Must fit in the page.
*/
public Builder setContentRect(Rect contentRect) {
if (contentRect != null && (contentRect.left < 0
|| contentRect.top < 0
|| contentRect.right > mPageInfo.mPageWidth
|| contentRect.bottom > mPageInfo.mPageHeight)) {
throw new IllegalArgumentException("contentRect does not fit the page");
}
mPageInfo.mContentRect = contentRect;
return this;
}
/**
* Creates a new {@link PageInfo}.
*
* @return The new instance.
*/
public PageInfo create() {
if (mPageInfo.mContentRect == null) {
mPageInfo.mContentRect = new Rect(0, 0,
mPageInfo.mPageWidth, mPageInfo.mPageHeight);
}
return mPageInfo;
}
}
}
/**
* This class represents a PDF document page. It has associated
* a canvas on which you can draw content and is acquired by a
* call to {@link #getCanvas()}. It also has associated a
* {@link PageInfo} instance that describes its attributes. Also
* a page has
*/
public static final class Page {
private final PageInfo mPageInfo;
private Canvas mCanvas;
/**
* Creates a new instance.
*
* @param canvas The canvas of the page.
* @param pageInfo The info with meta-data.
*/
private Page(Canvas canvas, PageInfo pageInfo) {
mCanvas = canvas;
mPageInfo = pageInfo;
}
/**
* Gets the {@link Canvas} of the page.
*
* <p>
* <strong>Note: </strong> There are some draw operations that are not yet
* supported by the canvas returned by this method. More specifically:
* <ul>
* <li>Inverse path clipping performed via {@link Canvas#clipPath(android.graphics.Path,
* android.graphics.Region.Op) Canvas.clipPath(android.graphics.Path,
* android.graphics.Region.Op)} for {@link
* android.graphics.Region.Op#REVERSE_DIFFERENCE
* Region.Op#REVERSE_DIFFERENCE} operations.</li>
* <li>{@link Canvas#drawVertices(android.graphics.Canvas.VertexMode, int,
* float[], int, float[], int, int[], int, short[], int, int,
* android.graphics.Paint) Canvas.drawVertices(
* android.graphics.Canvas.VertexMode, int, float[], int, float[],
* int, int[], int, short[], int, int, android.graphics.Paint)}</li>
* <li>Color filters set via {@link Paint#setColorFilter(
* android.graphics.ColorFilter)}</li>
* <li>Mask filters set via {@link Paint#setMaskFilter(
* android.graphics.MaskFilter)}</li>
* <li>Some XFER modes such as
* {@link android.graphics.PorterDuff.Mode#SRC_ATOP PorterDuff.Mode SRC},
* {@link android.graphics.PorterDuff.Mode#DST_ATOP PorterDuff.DST_ATOP},
* {@link android.graphics.PorterDuff.Mode#XOR PorterDuff.XOR},
* {@link android.graphics.PorterDuff.Mode#ADD PorterDuff.ADD}</li>
* </ul>
*
* @return The canvas if the page is not finished, null otherwise.
*
* @see PdfDocument#finishPage(Page)
*/
public Canvas getCanvas() {
return mCanvas;
}
/**
* Gets the {@link PageInfo} with meta-data for the page.
*
* @return The page info.
*
* @see PdfDocument#finishPage(Page)
*/
public PageInfo getInfo() {
return mPageInfo;
}
boolean isFinished() {
return mCanvas == null;
}
private void finish() {
if (mCanvas != null) {
mCanvas.release();
mCanvas = null;
}
}
}
}