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 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