Home | History | Annotate | Download | only in pdf
      1 /*
      2  * Copyright (C) 2013 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 
     17 package android.graphics.pdf;
     18 
     19 import android.graphics.Bitmap;
     20 import android.graphics.Canvas;
     21 import android.graphics.Paint;
     22 import android.graphics.Rect;
     23 
     24 import dalvik.system.CloseGuard;
     25 
     26 import java.io.IOException;
     27 import java.io.OutputStream;
     28 import java.util.ArrayList;
     29 import java.util.Collections;
     30 import java.util.List;
     31 
     32 /**
     33  * <p>
     34  * This class enables generating a PDF document from native Android content. You
     35  * create a new document and then for every page you want to add you start a page,
     36  * write content to the page, and finish the page. After you are done with all
     37  * pages, you write the document to an output stream and close the document.
     38  * After a document is closed you should not use it anymore. Note that pages are
     39  * created one by one, i.e. you can have only a single page to which you are
     40  * writing at any given time. This class is not thread safe.
     41  * </p>
     42  * <p>
     43  * A typical use of the APIs looks like this:
     44  * </p>
     45  * <pre>
     46  * // create a new document
     47  * PdfDocument document = new PdfDocument();
     48  *
     49  * // crate a page description
     50  * PageInfo pageInfo = new PageInfo.Builder(new Rect(0, 0, 100, 100), 1).create();
     51  *
     52  * // start a page
     53  * Page page = document.startPage(pageInfo);
     54  *
     55  * // draw something on the page
     56  * View content = getContentView();
     57  * content.draw(page.getCanvas());
     58  *
     59  * // finish the page
     60  * document.finishPage(page);
     61  * . . .
     62  * // add more pages
     63  * . . .
     64  * // write the document content
     65  * document.writeTo(getOutputStream());
     66  *
     67  * // close the document
     68  * document.close();
     69  * </pre>
     70  */
     71 public class PdfDocument {
     72 
     73     // TODO: We need a constructor that will take an OutputStream to
     74     // support online data serialization as opposed to the current
     75     // on demand one. The current approach is fine until Skia starts
     76     // to support online PDF generation at which point we need to
     77     // handle this.
     78 
     79     private final byte[] mChunk = new byte[4096];
     80 
     81     private final CloseGuard mCloseGuard = CloseGuard.get();
     82 
     83     private final List<PageInfo> mPages = new ArrayList<PageInfo>();
     84 
     85     private long mNativeDocument;
     86 
     87     private Page mCurrentPage;
     88 
     89     /**
     90      * Creates a new instance.
     91      */
     92     public PdfDocument() {
     93         mNativeDocument = nativeCreateDocument();
     94         mCloseGuard.open("close");
     95     }
     96 
     97     /**
     98      * Starts a page using the provided {@link PageInfo}. After the page
     99      * is created you can draw arbitrary content on the page's canvas which
    100      * you can get by calling {@link Page#getCanvas()}. After you are done
    101      * drawing the content you should finish the page by calling
    102      * {@link #finishPage(Page)}. After the page is finished you should
    103      * no longer access the page or its canvas.
    104      * <p>
    105      * <strong>Note:</strong> Do not call this method after {@link #close()}.
    106      * Also do not call this method if the last page returned by this method
    107      * is not finished by calling {@link #finishPage(Page)}.
    108      * </p>
    109      *
    110      * @param pageInfo The page info. Cannot be null.
    111      * @return A blank page.
    112      *
    113      * @see #finishPage(Page)
    114      */
    115     public Page startPage(PageInfo pageInfo) {
    116         throwIfClosed();
    117         throwIfCurrentPageNotFinished();
    118         if (pageInfo == null) {
    119             throw new IllegalArgumentException("page cannot be null");
    120         }
    121         Canvas canvas = new PdfCanvas(nativeStartPage(mNativeDocument, pageInfo.mPageWidth,
    122                 pageInfo.mPageHeight, pageInfo.mContentRect.left, pageInfo.mContentRect.top,
    123                 pageInfo.mContentRect.right, pageInfo.mContentRect.bottom));
    124         mCurrentPage = new Page(canvas, pageInfo);
    125         return mCurrentPage;
    126     }
    127 
    128     /**
    129      * Finishes a started page. You should always finish the last started page.
    130      * <p>
    131      * <strong>Note:</strong> Do not call this method after {@link #close()}.
    132      * You should not finish the same page more than once.
    133      * </p>
    134      *
    135      * @param page The page. Cannot be null.
    136      *
    137      * @see #startPage(PageInfo)
    138      */
    139     public void finishPage(Page page) {
    140         throwIfClosed();
    141         if (page == null) {
    142             throw new IllegalArgumentException("page cannot be null");
    143         }
    144         if (page != mCurrentPage) {
    145             throw new IllegalStateException("invalid page");
    146         }
    147         if (page.isFinished()) {
    148             throw new IllegalStateException("page already finished");
    149         }
    150         mPages.add(page.getInfo());
    151         mCurrentPage = null;
    152         nativeFinishPage(mNativeDocument);
    153         page.finish();
    154     }
    155 
    156     /**
    157      * Writes the document to an output stream. You can call this method
    158      * multiple times.
    159      * <p>
    160      * <strong>Note:</strong> Do not call this method after {@link #close()}.
    161      * Also do not call this method if a page returned by {@link #startPage(
    162      * PageInfo)} is not finished by calling {@link #finishPage(Page)}.
    163      * </p>
    164      *
    165      * @param out The output stream. Cannot be null.
    166      *
    167      * @throws IOException If an error occurs while writing.
    168      */
    169     public void writeTo(OutputStream out) throws IOException {
    170         throwIfClosed();
    171         throwIfCurrentPageNotFinished();
    172         if (out == null) {
    173             throw new IllegalArgumentException("out cannot be null!");
    174         }
    175         nativeWriteTo(mNativeDocument, out, mChunk);
    176     }
    177 
    178     /**
    179      * Gets the pages of the document.
    180      *
    181      * @return The pages or an empty list.
    182      */
    183     public List<PageInfo> getPages() {
    184         return Collections.unmodifiableList(mPages);
    185     }
    186 
    187     /**
    188      * Closes this document. This method should be called after you
    189      * are done working with the document. After this call the document
    190      * is considered closed and none of its methods should be called.
    191      * <p>
    192      * <strong>Note:</strong> Do not call this method if the page
    193      * returned by {@link #startPage(PageInfo)} is not finished by
    194      * calling {@link #finishPage(Page)}.
    195      * </p>
    196      */
    197     public void close() {
    198         throwIfCurrentPageNotFinished();
    199         dispose();
    200     }
    201 
    202     @Override
    203     protected void finalize() throws Throwable {
    204         try {
    205             mCloseGuard.warnIfOpen();
    206             dispose();
    207         } finally {
    208             super.finalize();
    209         }
    210     }
    211 
    212     private void dispose() {
    213         if (mNativeDocument != 0) {
    214             nativeClose(mNativeDocument);
    215             mCloseGuard.close();
    216             mNativeDocument = 0;
    217         }
    218     }
    219 
    220     /**
    221      * Throws an exception if the document is already closed.
    222      */
    223     private void throwIfClosed() {
    224         if (mNativeDocument == 0) {
    225             throw new IllegalStateException("document is closed!");
    226         }
    227     }
    228 
    229     /**
    230      * Throws an exception if the last started page is not finished.
    231      */
    232     private void throwIfCurrentPageNotFinished() {
    233         if (mCurrentPage != null) {
    234             throw new IllegalStateException("Current page not finished!");
    235         }
    236     }
    237 
    238     private native long nativeCreateDocument();
    239 
    240     private native void nativeClose(long nativeDocument);
    241 
    242     private native void nativeFinishPage(long nativeDocument);
    243 
    244     private native void nativeWriteTo(long nativeDocument, OutputStream out, byte[] chunk);
    245 
    246     private static native long nativeStartPage(long nativeDocument, int pageWidth, int pageHeight,
    247             int contentLeft, int contentTop, int contentRight, int contentBottom);
    248 
    249     private final class PdfCanvas extends Canvas {
    250 
    251         public PdfCanvas(long nativeCanvas) {
    252             super(nativeCanvas);
    253         }
    254 
    255         @Override
    256         public void setBitmap(Bitmap bitmap) {
    257             throw new UnsupportedOperationException();
    258         }
    259     }
    260 
    261     /**
    262      * This class represents meta-data that describes a PDF {@link Page}.
    263      */
    264     public static final class PageInfo {
    265         private int mPageWidth;
    266         private int mPageHeight;
    267         private Rect mContentRect;
    268         private int mPageNumber;
    269 
    270         /**
    271          * Creates a new instance.
    272          */
    273         private PageInfo() {
    274             /* do nothing */
    275         }
    276 
    277         /**
    278          * Gets the page width in PostScript points (1/72th of an inch).
    279          *
    280          * @return The page width.
    281          */
    282         public int getPageWidth() {
    283             return mPageWidth;
    284         }
    285 
    286         /**
    287          * Gets the page height in PostScript points (1/72th of an inch).
    288          *
    289          * @return The page height.
    290          */
    291         public int getPageHeight() {
    292             return mPageHeight;
    293         }
    294 
    295         /**
    296          * Get the content rectangle in PostScript points (1/72th of an inch).
    297          * This is the area that contains the page content and is relative to
    298          * the page top left.
    299          *
    300          * @return The content rectangle.
    301          */
    302         public Rect getContentRect() {
    303             return mContentRect;
    304         }
    305 
    306         /**
    307          * Gets the page number.
    308          *
    309          * @return The page number.
    310          */
    311         public int getPageNumber() {
    312             return mPageNumber;
    313         }
    314 
    315         /**
    316          * Builder for creating a {@link PageInfo}.
    317          */
    318         public static final class Builder {
    319             private final PageInfo mPageInfo = new PageInfo();
    320 
    321             /**
    322              * Creates a new builder with the mandatory page info attributes.
    323              *
    324              * @param pageWidth The page width in PostScript (1/72th of an inch).
    325              * @param pageHeight The page height in PostScript (1/72th of an inch).
    326              * @param pageNumber The page number.
    327              */
    328             public Builder(int pageWidth, int pageHeight, int pageNumber) {
    329                 if (pageWidth <= 0) {
    330                     throw new IllegalArgumentException("page width must be positive");
    331                 }
    332                 if (pageHeight <= 0) {
    333                     throw new IllegalArgumentException("page width must be positive");
    334                 }
    335                 if (pageNumber < 0) {
    336                     throw new IllegalArgumentException("pageNumber must be non negative");
    337                 }
    338                 mPageInfo.mPageWidth = pageWidth;
    339                 mPageInfo.mPageHeight = pageHeight;
    340                 mPageInfo.mPageNumber = pageNumber;
    341             }
    342 
    343             /**
    344              * Sets the content rectangle in PostScript point (1/72th of an inch).
    345              * This is the area that contains the page content and is relative to
    346              * the page top left.
    347              *
    348              * @param contentRect The content rectangle. Must fit in the page.
    349              */
    350             public Builder setContentRect(Rect contentRect) {
    351                 if (contentRect != null && (contentRect.left < 0
    352                         || contentRect.top < 0
    353                         || contentRect.right > mPageInfo.mPageWidth
    354                         || contentRect.bottom > mPageInfo.mPageHeight)) {
    355                     throw new IllegalArgumentException("contentRect does not fit the page");
    356                 }
    357                 mPageInfo.mContentRect = contentRect;
    358                 return this;
    359             }
    360 
    361             /**
    362              * Creates a new {@link PageInfo}.
    363              *
    364              * @return The new instance.
    365              */
    366             public PageInfo create() {
    367                 if (mPageInfo.mContentRect == null) {
    368                     mPageInfo.mContentRect = new Rect(0, 0,
    369                             mPageInfo.mPageWidth, mPageInfo.mPageHeight);
    370                 }
    371                 return mPageInfo;
    372             }
    373         }
    374     }
    375 
    376     /**
    377      * This class represents a PDF document page. It has associated
    378      * a canvas on which you can draw content and is acquired by a
    379      * call to {@link #getCanvas()}. It also has associated a
    380      * {@link PageInfo} instance that describes its attributes. Also
    381      * a page has
    382      */
    383     public static final class Page {
    384         private final PageInfo mPageInfo;
    385         private Canvas mCanvas;
    386 
    387         /**
    388          * Creates a new instance.
    389          *
    390          * @param canvas The canvas of the page.
    391          * @param pageInfo The info with meta-data.
    392          */
    393         private Page(Canvas canvas, PageInfo pageInfo) {
    394             mCanvas = canvas;
    395             mPageInfo = pageInfo;
    396         }
    397 
    398         /**
    399          * Gets the {@link Canvas} of the page.
    400          *
    401          * <p>
    402          * <strong>Note: </strong> There are some draw operations that are not yet
    403          * supported by the canvas returned by this method. More specifically:
    404          * <ul>
    405          * <li>Inverse path clipping performed via {@link Canvas#clipPath(android.graphics.Path,
    406          *     android.graphics.Region.Op) Canvas.clipPath(android.graphics.Path,
    407          *     android.graphics.Region.Op)} for {@link
    408          *     android.graphics.Region.Op#REVERSE_DIFFERENCE
    409          *     Region.Op#REVERSE_DIFFERENCE} operations.</li>
    410          * <li>{@link Canvas#drawVertices(android.graphics.Canvas.VertexMode, int,
    411          *     float[], int, float[], int, int[], int, short[], int, int,
    412          *     android.graphics.Paint) Canvas.drawVertices(
    413          *     android.graphics.Canvas.VertexMode, int, float[], int, float[],
    414          *     int, int[], int, short[], int, int, android.graphics.Paint)}</li>
    415          * <li>Color filters set via {@link Paint#setColorFilter(
    416          *     android.graphics.ColorFilter)}</li>
    417          * <li>Mask filters set via {@link Paint#setMaskFilter(
    418          *     android.graphics.MaskFilter)}</li>
    419          * <li>Some XFER modes such as
    420          *     {@link android.graphics.PorterDuff.Mode#SRC_ATOP PorterDuff.Mode SRC},
    421          *     {@link android.graphics.PorterDuff.Mode#DST_ATOP PorterDuff.DST_ATOP},
    422          *     {@link android.graphics.PorterDuff.Mode#XOR PorterDuff.XOR},
    423          *     {@link android.graphics.PorterDuff.Mode#ADD PorterDuff.ADD}</li>
    424          * </ul>
    425          *
    426          * @return The canvas if the page is not finished, null otherwise.
    427          *
    428          * @see PdfDocument#finishPage(Page)
    429          */
    430         public Canvas getCanvas() {
    431             return mCanvas;
    432         }
    433 
    434         /**
    435          * Gets the {@link PageInfo} with meta-data for the page.
    436          *
    437          * @return The page info.
    438          *
    439          * @see PdfDocument#finishPage(Page)
    440          */
    441         public PageInfo getInfo() {
    442             return mPageInfo;
    443         }
    444 
    445         boolean isFinished() {
    446             return mCanvas == null;
    447         }
    448 
    449         private void finish() {
    450             if (mCanvas != null) {
    451                 mCanvas.release();
    452                 mCanvas = null;
    453             }
    454         }
    455     }
    456 }
    457