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             mCloseGuard.warnIfOpen();
206             dispose();
207         } finally {
208             super.finalize();
209         }
210     }
211 
dispose()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      */
throwIfClosed()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      */
throwIfCurrentPageNotFinished()232     private void throwIfCurrentPageNotFinished() {
233         if (mCurrentPage != null) {
234             throw new IllegalStateException("Current page not finished!");
235         }
236     }
237 
nativeCreateDocument()238     private native long nativeCreateDocument();
239 
nativeClose(long nativeDocument)240     private native void nativeClose(long nativeDocument);
241 
nativeFinishPage(long nativeDocument)242     private native void nativeFinishPage(long nativeDocument);
243 
nativeWriteTo(long nativeDocument, OutputStream out, byte[] chunk)244     private native void nativeWriteTo(long nativeDocument, OutputStream out, byte[] chunk);
245 
nativeStartPage(long nativeDocument, int pageWidth, int pageHeight, int contentLeft, int contentTop, int contentRight, int contentBottom)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 
PdfCanvas(long nativeCanvas)251         public PdfCanvas(long nativeCanvas) {
252             super(nativeCanvas);
253         }
254 
255         @Override
setBitmap(Bitmap bitmap)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          */
PageInfo()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          */
getPageWidth()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          */
getPageHeight()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          */
getContentRect()302         public Rect getContentRect() {
303             return mContentRect;
304         }
305 
306         /**
307          * Gets the page number.
308          *
309          * @return The page number.
310          */
getPageNumber()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              */
Builder(int pageWidth, int pageHeight, int pageNumber)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              */
setContentRect(Rect contentRect)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              */
create()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          */
Page(Canvas canvas, PageInfo pageInfo)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          */
getCanvas()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          */
getInfo()441         public PageInfo getInfo() {
442             return mPageInfo;
443         }
444 
isFinished()445         boolean isFinished() {
446             return mCanvas == null;
447         }
448 
finish()449         private void finish() {
450             if (mCanvas != null) {
451                 mCanvas.release();
452                 mCanvas = null;
453             }
454         }
455     }
456 }
457