• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
PdfDocument()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      */
startPage(PageInfo pageInfo)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      */
finishPage(Page page)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      */
writeTo(OutputStream out)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      */
getPages()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      */
close()197     public void close() {
198         throwIfCurrentPageNotFinished();
199         dispose();
200     }
201 
202     @Override
finalize()203     protected void finalize() throws Throwable {
204         try {
205             if (mCloseGuard != null) {
206                 mCloseGuard.warnIfOpen();
207             }
208 
209             dispose();
210         } finally {
211             super.finalize();
212         }
213     }
214 
dispose()215     private void dispose() {
216         if (mNativeDocument != 0) {
217             nativeClose(mNativeDocument);
218             mCloseGuard.close();
219             mNativeDocument = 0;
220         }
221     }
222 
223     /**
224      * Throws an exception if the document is already closed.
225      */
throwIfClosed()226     private void throwIfClosed() {
227         if (mNativeDocument == 0) {
228             throw new IllegalStateException("document is closed!");
229         }
230     }
231 
232     /**
233      * Throws an exception if the last started page is not finished.
234      */
throwIfCurrentPageNotFinished()235     private void throwIfCurrentPageNotFinished() {
236         if (mCurrentPage != null) {
237             throw new IllegalStateException("Current page not finished!");
238         }
239     }
240 
nativeCreateDocument()241     private native long nativeCreateDocument();
242 
nativeClose(long nativeDocument)243     private native void nativeClose(long nativeDocument);
244 
nativeFinishPage(long nativeDocument)245     private native void nativeFinishPage(long nativeDocument);
246 
nativeWriteTo(long nativeDocument, OutputStream out, byte[] chunk)247     private native void nativeWriteTo(long nativeDocument, OutputStream out, byte[] chunk);
248 
nativeStartPage(long nativeDocument, int pageWidth, int pageHeight, int contentLeft, int contentTop, int contentRight, int contentBottom)249     private static native long nativeStartPage(long nativeDocument, int pageWidth, int pageHeight,
250             int contentLeft, int contentTop, int contentRight, int contentBottom);
251 
252     private final class PdfCanvas extends Canvas {
253 
PdfCanvas(long nativeCanvas)254         public PdfCanvas(long nativeCanvas) {
255             super(nativeCanvas);
256         }
257 
258         @Override
setBitmap(Bitmap bitmap)259         public void setBitmap(Bitmap bitmap) {
260             throw new UnsupportedOperationException();
261         }
262     }
263 
264     /**
265      * This class represents meta-data that describes a PDF {@link Page}.
266      */
267     public static final class PageInfo {
268         private int mPageWidth;
269         private int mPageHeight;
270         private Rect mContentRect;
271         private int mPageNumber;
272 
273         /**
274          * Creates a new instance.
275          */
PageInfo()276         private PageInfo() {
277             /* do nothing */
278         }
279 
280         /**
281          * Gets the page width in PostScript points (1/72th of an inch).
282          *
283          * @return The page width.
284          */
getPageWidth()285         public int getPageWidth() {
286             return mPageWidth;
287         }
288 
289         /**
290          * Gets the page height in PostScript points (1/72th of an inch).
291          *
292          * @return The page height.
293          */
getPageHeight()294         public int getPageHeight() {
295             return mPageHeight;
296         }
297 
298         /**
299          * Get the content rectangle in PostScript points (1/72th of an inch).
300          * This is the area that contains the page content and is relative to
301          * the page top left.
302          *
303          * @return The content rectangle.
304          */
getContentRect()305         public Rect getContentRect() {
306             return mContentRect;
307         }
308 
309         /**
310          * Gets the page number.
311          *
312          * @return The page number.
313          */
getPageNumber()314         public int getPageNumber() {
315             return mPageNumber;
316         }
317 
318         /**
319          * Builder for creating a {@link PageInfo}.
320          */
321         public static final class Builder {
322             private final PageInfo mPageInfo = new PageInfo();
323 
324             /**
325              * Creates a new builder with the mandatory page info attributes.
326              *
327              * @param pageWidth The page width in PostScript (1/72th of an inch).
328              * @param pageHeight The page height in PostScript (1/72th of an inch).
329              * @param pageNumber The page number.
330              */
Builder(int pageWidth, int pageHeight, int pageNumber)331             public Builder(int pageWidth, int pageHeight, int pageNumber) {
332                 if (pageWidth <= 0) {
333                     throw new IllegalArgumentException("page width must be positive");
334                 }
335                 if (pageHeight <= 0) {
336                     throw new IllegalArgumentException("page width must be positive");
337                 }
338                 if (pageNumber < 0) {
339                     throw new IllegalArgumentException("pageNumber must be non negative");
340                 }
341                 mPageInfo.mPageWidth = pageWidth;
342                 mPageInfo.mPageHeight = pageHeight;
343                 mPageInfo.mPageNumber = pageNumber;
344             }
345 
346             /**
347              * Sets the content rectangle in PostScript point (1/72th of an inch).
348              * This is the area that contains the page content and is relative to
349              * the page top left.
350              *
351              * @param contentRect The content rectangle. Must fit in the page.
352              */
setContentRect(Rect contentRect)353             public Builder setContentRect(Rect contentRect) {
354                 if (contentRect != null && (contentRect.left < 0
355                         || contentRect.top < 0
356                         || contentRect.right > mPageInfo.mPageWidth
357                         || contentRect.bottom > mPageInfo.mPageHeight)) {
358                     throw new IllegalArgumentException("contentRect does not fit the page");
359                 }
360                 mPageInfo.mContentRect = contentRect;
361                 return this;
362             }
363 
364             /**
365              * Creates a new {@link PageInfo}.
366              *
367              * @return The new instance.
368              */
create()369             public PageInfo create() {
370                 if (mPageInfo.mContentRect == null) {
371                     mPageInfo.mContentRect = new Rect(0, 0,
372                             mPageInfo.mPageWidth, mPageInfo.mPageHeight);
373                 }
374                 return mPageInfo;
375             }
376         }
377     }
378 
379     /**
380      * This class represents a PDF document page. It has associated
381      * a canvas on which you can draw content and is acquired by a
382      * call to {@link #getCanvas()}. It also has associated a
383      * {@link PageInfo} instance that describes its attributes. Also
384      * a page has
385      */
386     public static final class Page {
387         private final PageInfo mPageInfo;
388         private Canvas mCanvas;
389 
390         /**
391          * Creates a new instance.
392          *
393          * @param canvas The canvas of the page.
394          * @param pageInfo The info with meta-data.
395          */
Page(Canvas canvas, PageInfo pageInfo)396         private Page(Canvas canvas, PageInfo pageInfo) {
397             mCanvas = canvas;
398             mPageInfo = pageInfo;
399         }
400 
401         /**
402          * Gets the {@link Canvas} of the page.
403          *
404          * <p>
405          * <strong>Note: </strong> There are some draw operations that are not yet
406          * supported by the canvas returned by this method. More specifically:
407          * <ul>
408          * <li>Inverse path clipping performed via {@link Canvas#clipPath(android.graphics.Path,
409          *     android.graphics.Region.Op) Canvas.clipPath(android.graphics.Path,
410          *     android.graphics.Region.Op)} for {@link
411          *     android.graphics.Region.Op#REVERSE_DIFFERENCE
412          *     Region.Op#REVERSE_DIFFERENCE} operations.</li>
413          * <li>{@link Canvas#drawVertices(android.graphics.Canvas.VertexMode, int,
414          *     float[], int, float[], int, int[], int, short[], int, int,
415          *     android.graphics.Paint) Canvas.drawVertices(
416          *     android.graphics.Canvas.VertexMode, int, float[], int, float[],
417          *     int, int[], int, short[], int, int, android.graphics.Paint)}</li>
418          * <li>Color filters set via {@link Paint#setColorFilter(
419          *     android.graphics.ColorFilter)}</li>
420          * <li>Mask filters set via {@link Paint#setMaskFilter(
421          *     android.graphics.MaskFilter)}</li>
422          * <li>Some XFER modes such as
423          *     {@link android.graphics.PorterDuff.Mode#SRC_ATOP PorterDuff.Mode SRC},
424          *     {@link android.graphics.PorterDuff.Mode#DST_ATOP PorterDuff.DST_ATOP},
425          *     {@link android.graphics.PorterDuff.Mode#XOR PorterDuff.XOR},
426          *     {@link android.graphics.PorterDuff.Mode#ADD PorterDuff.ADD}</li>
427          * </ul>
428          *
429          * @return The canvas if the page is not finished, null otherwise.
430          *
431          * @see PdfDocument#finishPage(Page)
432          */
getCanvas()433         public Canvas getCanvas() {
434             return mCanvas;
435         }
436 
437         /**
438          * Gets the {@link PageInfo} with meta-data for the page.
439          *
440          * @return The page info.
441          *
442          * @see PdfDocument#finishPage(Page)
443          */
getInfo()444         public PageInfo getInfo() {
445             return mPageInfo;
446         }
447 
isFinished()448         boolean isFinished() {
449             return mCanvas == null;
450         }
451 
finish()452         private void finish() {
453             if (mCanvas != null) {
454                 mCanvas.release();
455                 mCanvas = null;
456             }
457         }
458     }
459 }
460