content: /*
   * Copyright (C) 2013 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.graphics.pdf;
  
  import android.graphics.Bitmap;
  import android.graphics.Canvas;
  import android.graphics.Paint;
  import android.graphics.Rect;
  
  import dalvik.system.CloseGuard;
  
  import java.io.IOException;
  import java.io.OutputStream;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.List;
  
  /**
   * <p>
   * This class enables generating a PDF document from native Android content. You
   * create a new document and then for every page you want to add you start a page,
   * write content to the page, and finish the page. After you are done with all
   * pages, you write the document to an output stream and close the document.
   * After a document is closed you should not use it anymore. Note that pages are
   * created one by one, i.e. you can have only a single page to which you are
   * writing at any given time. This class is not thread safe.
   * </p>
   * <p>
   * A typical use of the APIs looks like this:
   * </p>
   * <pre>
   * // create a new document
   * PdfDocument document = new PdfDocument();
   *
   * // crate a page description
   * PageInfo pageInfo = new PageInfo.Builder(new Rect(0, 0, 100, 100), 1).create();
   *
   * // start a page
   * Page page = document.startPage(pageInfo);
   *
   * // draw something on the page
   * View content = getContentView();
   * content.draw(page.getCanvas());
   *
   * // finish the page
   * document.finishPage(page);
   * . . .
   * // add more pages
   * . . .
   * // write the document content
   * document.writeTo(getOutputStream());
   *
   * // close the document
   * document.close();
   * </pre>
   */
  public class PdfDocument {
  
      // TODO: We need a constructor that will take an OutputStream to
      // support online data serialization as opposed to the current
      // on demand one. The current approach is fine until Skia starts
      // to support online PDF generation at which point we need to
      // handle this.
  
      private final byte[] mChunk = new byte[4096];
  
      private final CloseGuard mCloseGuard = CloseGuard.get();
  
      private final List<PageInfo> mPages = new ArrayList<PageInfo>();
  
      private long mNativeDocument;
  
      private Page mCurrentPage;
  
      /**
       * Creates a new instance.
       */
      public PdfDocument() {
          mNativeDocument = nativeCreateDocument();
          mCloseGuard.open("close");
      }
  
      /**
       * Starts a page using the provided {@link PageInfo}. After the page
       * is created you can draw arbitrary content on the page's canvas which
       * you can get by calling {@link Page#getCanvas()}. After you are done
       * drawing the content you should finish the page by calling
       * {@link #finishPage(Page)}. After the page is finished you should
       * no longer access the page or its canvas.
       * <p>
       * <strong>Note:</strong> Do not call this method after {@link #close()}.
       * Also do not call this method if the last page returned by this method
       * is not finished by calling {@link #finishPage(Page)}.
       * </p>
       *
       * @param pageInfo The page info. Cannot be null.
       * @return A blank page.
       *
       * @see #finishPage(Page)
       */
      public Page startPage(PageInfo pageInfo) {
          throwIfClosed();
          throwIfCurrentPageNotFinished();
          if (pageInfo == null) {
              throw new IllegalArgumentException("page cannot be null");
          }
          Canvas canvas = new PdfCanvas(nativeStartPage(mNativeDocument, pageInfo.mPageWidth,
                  pageInfo.mPageHeight, pageInfo.mContentRect.left, pageInfo.mContentRect.top,
                  pageInfo.mContentRect.right, pageInfo.mContentRect.bottom));
          mCurrentPage = new Page(canvas, pageInfo);
          return mCurrentPage;
      }
  
      /**
       * Finishes a started page. You should always finish the last started page.
       * <p>
       * <strong>Note:</strong> Do not call this method after {@link #close()}.
       * You should not finish the same page more than once.
       * </p>
       *
       * @param page The page. Cannot be null.
       *
       * @see #startPage(PageInfo)
       */
      public void finishPage(Page page) {
          throwIfClosed();
          if (page == null) {
              throw new IllegalArgumentException("page cannot be null");
          }
          if (page != mCurrentPage) {
              throw new IllegalStateException("invalid page");
          }
          if (page.isFinished()) {
              throw new IllegalStateException("page already finished");
          }
          mPages.add(page.getInfo());
          mCurrentPage = null;
          nativeFinishPage(mNativeDocument);
          page.finish();
      }
  
      /**
       * Writes the document to an output stream. You can call this method
       * multiple times.
       * <p>
       * <strong>Note:</strong> Do not call this method after {@link #close()}.
       * Also do not call this method if a page returned by {@link #startPage(
       * PageInfo)} is not finished by calling {@link #finishPage(Page)}.
       * </p>
       *
       * @param out The output stream. Cannot be null.
       *
       * @throws IOException If an error occurs while writing.
       */
      public void writeTo(OutputStream out) throws IOException {
          throwIfClosed();
          throwIfCurrentPageNotFinished();
          if (out == null) {
              throw new IllegalArgumentException("out cannot be null!");
          }
          nativeWriteTo(mNativeDocument, out, mChunk);
      }
  
      /**
       * Gets the pages of the document.
       *
       * @return The pages or an empty list.
       */
      public List<PageInfo> getPages() {
          return Collections.unmodifiableList(mPages);
      }
  
      /**
       * Closes this document. This method should be called after you
       * are done working with the document. After this call the document
       * is considered closed and none of its methods should be called.
       * <p>
       * <strong>Note:</strong> Do not call this method if the page
       * returned by {@link #startPage(PageInfo)} is not finished by
       * calling {@link #finishPage(Page)}.
       * </p>
       */
      public void close() {
          throwIfCurrentPageNotFinished();
          dispose();
      }
  
      @Override
      protected void finalize() throws Throwable {
          try {
              if (mCloseGuard != null) {
                  mCloseGuard.warnIfOpen();
              }
  
              dispose();
          } finally {
              super.finalize();
          }
      }
  
      private void dispose() {
          if (mNativeDocument != 0) {
              nativeClose(mNativeDocument);
              mCloseGuard.close();
              mNativeDocument = 0;
          }
      }
  
      /**
       * Throws an exception if the document is already closed.
       */
      private void throwIfClosed() {
          if (mNativeDocument == 0) {
              throw new IllegalStateException("document is closed!");
          }
      }
  
      /**
       * Throws an exception if the last started page is not finished.
       */
      private void throwIfCurrentPageNotFinished() {
          if (mCurrentPage != null) {
              throw new IllegalStateException("Current page not finished!");
          }
      }
  
      private native long nativeCreateDocument();
  
      private native void nativeClose(long nativeDocument);
  
      private native void nativeFinishPage(long nativeDocument);
  
      private native void nativeWriteTo(long nativeDocument, OutputStream out, byte[] chunk);
  
      private static native long nativeStartPage(long nativeDocument, int pageWidth, int pageHeight,
              int contentLeft, int contentTop, int contentRight, int contentBottom);
  
      private final class PdfCanvas extends Canvas {
  
          public PdfCanvas(long nativeCanvas) {
              super(nativeCanvas);
          }
  
          @Override
          public void setBitmap(Bitmap bitmap) {
              throw new UnsupportedOperationException();
          }
      }
  
      /**
       * This class represents meta-data that describes a PDF {@link Page}.
       */
      public static final class PageInfo {
          private int mPageWidth;
          private int mPageHeight;
          private Rect mContentRect;
          private int mPageNumber;
  
          /**
           * Creates a new instance.
           */
          private PageInfo() {
              /* do nothing */
          }
  
          /**
           * Gets the page width in PostScript points (1/72th of an inch).
           *
           * @return The page width.
           */
          public int getPageWidth() {
              return mPageWidth;
          }
  
          /**
           * Gets the page height in PostScript points (1/72th of an inch).
           *
           * @return The page height.
           */
          public int getPageHeight() {
              return mPageHeight;
          }
  
          /**
           * Get the content rectangle in PostScript points (1/72th of an inch).
           * This is the area that contains the page content and is relative to
           * the page top left.
           *
           * @return The content rectangle.
           */
          public Rect getContentRect() {
              return mContentRect;
          }
  
          /**
           * Gets the page number.
           *
           * @return The page number.
           */
          public int getPageNumber() {
              return mPageNumber;
          }
  
          /**
           * Builder for creating a {@link PageInfo}.
           */
          public static final class Builder {
              private final PageInfo mPageInfo = new PageInfo();
  
              /**
               * Creates a new builder with the mandatory page info attributes.
               *
               * @param pageWidth The page width in PostScript (1/72th of an inch).
               * @param pageHeight The page height in PostScript (1/72th of an inch).
               * @param pageNumber The page number.
               */
              public Builder(int pageWidth, int pageHeight, int pageNumber) {
                  if (pageWidth <= 0) {
                      throw new IllegalArgumentException("page width must be positive");
                  }
                  if (pageHeight <= 0) {
                      throw new IllegalArgumentException("page width must be positive");
                  }
                  if (pageNumber < 0) {
                      throw new IllegalArgumentException("pageNumber must be non negative");
                  }
                  mPageInfo.mPageWidth = pageWidth;
                  mPageInfo.mPageHeight = pageHeight;
                  mPageInfo.mPageNumber = pageNumber;
              }
  
              /**
               * Sets the content rectangle in PostScript point (1/72th of an inch).
               * This is the area that contains the page content and is relative to
               * the page top left.
               *
               * @param contentRect The content rectangle. Must fit in the page.
               */
              public Builder setContentRect(Rect contentRect) {
                  if (contentRect != null && (contentRect.left < 0
                          || contentRect.top < 0
                          || contentRect.right > mPageInfo.mPageWidth
                          || contentRect.bottom > mPageInfo.mPageHeight)) {
                      throw new IllegalArgumentException("contentRect does not fit the page");
                  }
                  mPageInfo.mContentRect = contentRect;
                  return this;
              }
  
              /**
               * Creates a new {@link PageInfo}.
               *
               * @return The new instance.
               */
              public PageInfo create() {
                  if (mPageInfo.mContentRect == null) {
                      mPageInfo.mContentRect = new Rect(0, 0,
                              mPageInfo.mPageWidth, mPageInfo.mPageHeight);
                  }
                  return mPageInfo;
              }
          }
      }
  
      /**
       * This class represents a PDF document page. It has associated
       * a canvas on which you can draw content and is acquired by a
       * call to {@link #getCanvas()}. It also has associated a
       * {@link PageInfo} instance that describes its attributes. Also
       * a page has 
       */
      public static final class Page {
          private final PageInfo mPageInfo;
          private Canvas mCanvas;
  
          /**
           * Creates a new instance.
           *
           * @param canvas The canvas of the page.
           * @param pageInfo The info with meta-data.
           */
          private Page(Canvas canvas, PageInfo pageInfo) {
              mCanvas = canvas;
              mPageInfo = pageInfo;
          }
  
          /**
           * Gets the {@link Canvas} of the page.
           *
           * <p>
           * <strong>Note: </strong> There are some draw operations that are not yet
           * supported by the canvas returned by this method. More specifically:
           * <ul>
           * <li>Inverse path clipping performed via {@link Canvas#clipPath(android.graphics.Path,
           *     android.graphics.Region.Op) Canvas.clipPath(android.graphics.Path,
           *     android.graphics.Region.Op)} for {@link
           *     android.graphics.Region.Op#REVERSE_DIFFERENCE
           *     Region.Op#REVERSE_DIFFERENCE} operations.</li>
           * <li>{@link Canvas#drawVertices(android.graphics.Canvas.VertexMode, int,
           *     float[], int, float[], int, int[], int, short[], int, int,
           *     android.graphics.Paint) Canvas.drawVertices(
           *     android.graphics.Canvas.VertexMode, int, float[], int, float[],
           *     int, int[], int, short[], int, int, android.graphics.Paint)}</li>
           * <li>Color filters set via {@link Paint#setColorFilter(
           *     android.graphics.ColorFilter)}</li>
           * <li>Mask filters set via {@link Paint#setMaskFilter(
           *     android.graphics.MaskFilter)}</li>
           * <li>Some XFER modes such as
           *     {@link android.graphics.PorterDuff.Mode#SRC_ATOP PorterDuff.Mode SRC},
           *     {@link android.graphics.PorterDuff.Mode#DST_ATOP PorterDuff.DST_ATOP},
           *     {@link android.graphics.PorterDuff.Mode#XOR PorterDuff.XOR},
           *     {@link android.graphics.PorterDuff.Mode#ADD PorterDuff.ADD}</li>
           * </ul>
           *
           * @return The canvas if the page is not finished, null otherwise.
           *
           * @see PdfDocument#finishPage(Page)
           */
          public Canvas getCanvas() {
              return mCanvas;
          }
  
          /**
           * Gets the {@link PageInfo} with meta-data for the page.
           *
           * @return The page info.
           *
           * @see PdfDocument#finishPage(Page)
           */
          public PageInfo getInfo() {
              return mPageInfo;
          }
  
          boolean isFinished() {
              return mCanvas == null;
          }
  
          private void finish() {
              if (mCanvas != null) {
                  mCanvas.release();
                  mCanvas = null;
              }
          }
      }
  }
  