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