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
17package android.graphics.pdf;
18
19import android.graphics.Bitmap;
20import android.graphics.Canvas;
21import android.graphics.Paint;
22import android.graphics.Rect;
23
24import dalvik.system.CloseGuard;
25
26import java.io.IOException;
27import java.io.OutputStream;
28import java.util.ArrayList;
29import java.util.Collections;
30import 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 */
71public 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            if (mCloseGuard != null) {
206                mCloseGuard.warnIfOpen();
207            }
208
209            dispose();
210        } finally {
211            super.finalize();
212        }
213    }
214
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     */
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     */
235    private void throwIfCurrentPageNotFinished() {
236        if (mCurrentPage != null) {
237            throw new IllegalStateException("Current page not finished!");
238        }
239    }
240
241    private native long nativeCreateDocument();
242
243    private native void nativeClose(long nativeDocument);
244
245    private native void nativeFinishPage(long nativeDocument);
246
247    private native void nativeWriteTo(long nativeDocument, OutputStream out, byte[] chunk);
248
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
254        public PdfCanvas(long nativeCanvas) {
255            super(nativeCanvas);
256        }
257
258        @Override
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         */
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         */
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         */
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         */
305        public Rect getContentRect() {
306            return mContentRect;
307        }
308
309        /**
310         * Gets the page number.
311         *
312         * @return The page number.
313         */
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             */
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             */
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             */
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         */
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         */
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         */
444        public PageInfo getInfo() {
445            return mPageInfo;
446        }
447
448        boolean isFinished() {
449            return mCanvas == null;
450        }
451
452        private void finish() {
453            if (mCanvas != null) {
454                mCanvas.release();
455                mCanvas = null;
456            }
457        }
458    }
459}
460