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