/* * 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; /** *

* This class enables generating a PDF document from native Android content. You * open 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. *

*

* A typical use of the APIs looks like this: *

*
 * // 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();
 * 
*/ 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 mPages = new ArrayList(); private int 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. *

* Note: 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)}. *

* * @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. *

* Note: Do not call this method after {@link #close()}. * You should not finish the same page more than once. *

* * @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. *

* Note: 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)}. *

* * @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 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. *

* Note: Do not call this method if the page * returned by {@link #startPage(PageInfo)} is not finished by * calling {@link #finishPage(Page)}. *

*/ 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 int nativeCreateDocument(); private native void nativeClose(int document); private native void nativeFinishPage(int document); private native void nativeWriteTo(int document, OutputStream out, byte[] chunk); private static native int nativeStartPage(int documentPtr, int pageWidth, int pageHeight, int contentLeft, int contentTop, int contentRight, int contentBottom); private final class PdfCanvas extends Canvas { public PdfCanvas(int 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. * *

* Note: There are some draw operations that are not yet * supported by the canvas returned by this method. More specifically: *

    *
  • 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.
  • *
  • {@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)}
  • *
  • Color filters set via {@link Paint#setColorFilter( * android.graphics.ColorFilter)}
  • *
  • Mask filters set via {@link Paint#setMaskFilter( * android.graphics.MaskFilter)}
  • *
  • 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}
  • *
* * @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; } } } }