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