content: /*
   * Copyright (C) 2014 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.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UnsupportedAppUsage;
  import android.graphics.Bitmap;
  import android.graphics.Bitmap.Config;
  import android.graphics.Matrix;
  import android.graphics.Point;
  import android.graphics.Rect;
  import android.os.ParcelFileDescriptor;
  import android.system.ErrnoException;
  import android.system.Os;
  import android.system.OsConstants;
  import com.android.internal.util.Preconditions;
  import dalvik.system.CloseGuard;
  
  import libcore.io.IoUtils;
  
  import java.io.IOException;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  
  /**
   * <p>
   * This class enables rendering a PDF document. This class is not thread safe.
   * </p>
   * <p>
   * If you want to render a PDF, you create a renderer and for every page you want
   * to render, you open the page, render it, and close the page. After you are done
   * with rendering, you close the renderer. After the renderer is closed it should not
   * be used anymore. Note that the pages are rendered one by one, i.e. you can have
   * only a single page opened at any given time.
   * </p>
   * <p>
   * A typical use of the APIs to render a PDF looks like this:
   * </p>
   * <pre>
   * // create a new renderer
   * PdfRenderer renderer = new PdfRenderer(getSeekableFileDescriptor());
   *
   * // let us just render all pages
   * final int pageCount = renderer.getPageCount();
   * for (int i = 0; i < pageCount; i++) {
   *     Page page = renderer.openPage(i);
   *
   *     // say we render for showing on the screen
   *     page.render(mBitmap, null, null, Page.RENDER_MODE_FOR_DISPLAY);
   *
   *     // do stuff with the bitmap
   *
   *     // close the page
   *     page.close();
   * }
   *
   * // close the renderer
   * renderer.close();
   * </pre>
   *
   * <h3>Print preview and print output</h3>
   * <p>
   * If you are using this class to rasterize a PDF for printing or show a print
   * preview, it is recommended that you respect the following contract in order
   * to provide a consistent user experience when seeing a preview and printing,
   * i.e. the user sees a preview that is the same as the printout.
   * </p>
   * <ul>
   * <li>
   * Respect the property whether the document would like to be scaled for printing
   * as per {@link #shouldScaleForPrinting()}.
   * </li>
   * <li>
   * When scaling a document for printing the aspect ratio should be preserved.
   * </li>
   * <li>
   * Do not inset the content with any margins from the {@link android.print.PrintAttributes}
   * as the application is responsible to render it such that the margins are respected.
   * </li>
   * <li>
   * If document page size is greater than the printed media size the content should
   * be anchored to the upper left corner of the page for left-to-right locales and
   * top right corner for right-to-left locales.
   * </li>
   * </ul>
   *
   * @see #close()
   */
  public final class PdfRenderer implements AutoCloseable {
      /**
       * Any call the native pdfium code has to be single threaded as the library does not support
       * parallel use.
       */
      final static Object sPdfiumLock = new Object();
  
      private final CloseGuard mCloseGuard = CloseGuard.get();
  
      private final Point mTempPoint = new Point();
  
      private long mNativeDocument;
  
      private final int mPageCount;
  
      private ParcelFileDescriptor mInput;
  
      @UnsupportedAppUsage
      private Page mCurrentPage;
  
      /** @hide */
      @IntDef({
          Page.RENDER_MODE_FOR_DISPLAY,
          Page.RENDER_MODE_FOR_PRINT
      })
      @Retention(RetentionPolicy.SOURCE)
      public @interface RenderMode {}
  
      /**
       * Creates a new instance.
       * <p>
       * <strong>Note:</strong> The provided file descriptor must be <strong>seekable</strong>,
       * i.e. its data being randomly accessed, e.g. pointing to a file.
       * </p>
       * <p>
       * <strong>Note:</strong> This class takes ownership of the passed in file descriptor
       * and is responsible for closing it when the renderer is closed.
       * </p>
       * <p>
       * If the file is from an untrusted source it is recommended to run the renderer in a separate,
       * isolated process with minimal permissions to limit the impact of security exploits.
       * </p>
       *
       * @param input Seekable file descriptor to read from.
       *
       * @throws java.io.IOException If an error occurs while reading the file.
       * @throws java.lang.SecurityException If the file requires a password or
       *         the security scheme is not supported.
       */
      public PdfRenderer(@NonNull ParcelFileDescriptor input) throws IOException {
          if (input == null) {
              throw new NullPointerException("input cannot be null");
          }
  
          final long size;
          try {
              Os.lseek(input.getFileDescriptor(), 0, OsConstants.SEEK_SET);
              size = Os.fstat(input.getFileDescriptor()).st_size;
          } catch (ErrnoException ee) {
              throw new IllegalArgumentException("file descriptor not seekable");
          }
          mInput = input;
  
          synchronized (sPdfiumLock) {
              mNativeDocument = nativeCreate(mInput.getFd(), size);
              try {
                  mPageCount = nativeGetPageCount(mNativeDocument);
              } catch (Throwable t) {
                  nativeClose(mNativeDocument);
                  mNativeDocument = 0;
                  throw t;
              }
          }
  
          mCloseGuard.open("close");
      }
  
      /**
       * Closes this renderer. You should not use this instance
       * after this method is called.
       */
      public void close() {
          throwIfClosed();
          throwIfPageOpened();
          doClose();
      }
  
      /**
       * Gets the number of pages in the document.
       *
       * @return The page count.
       */
      public int getPageCount() {
          throwIfClosed();
          return mPageCount;
      }
  
      /**
       * Gets whether the document prefers to be scaled for printing.
       * You should take this info account if the document is rendered
       * for printing and the target media size differs from the page
       * size.
       *
       * @return If to scale the document.
       */
      public boolean shouldScaleForPrinting() {
          throwIfClosed();
  
          synchronized (sPdfiumLock) {
              return nativeScaleForPrinting(mNativeDocument);
          }
      }
  
      /**
       * Opens a page for rendering.
       *
       * @param index The page index.
       * @return A page that can be rendered.
       *
       * @see android.graphics.pdf.PdfRenderer.Page#close() PdfRenderer.Page.close()
       */
      public Page openPage(int index) {
          throwIfClosed();
          throwIfPageOpened();
          throwIfPageNotInDocument(index);
          mCurrentPage = new Page(index);
          return mCurrentPage;
      }
  
      @Override
      protected void finalize() throws Throwable {
          try {
              if (mCloseGuard != null) {
                  mCloseGuard.warnIfOpen();
              }
  
              doClose();
          } finally {
              super.finalize();
          }
      }
  
      @UnsupportedAppUsage
      private void doClose() {
          if (mCurrentPage != null) {
              mCurrentPage.close();
              mCurrentPage = null;
          }
  
          if (mNativeDocument != 0) {
              synchronized (sPdfiumLock) {
                  nativeClose(mNativeDocument);
              }
              mNativeDocument = 0;
          }
  
          if (mInput != null) {
              IoUtils.closeQuietly(mInput);
              mInput = null;
          }
          mCloseGuard.close();
      }
  
      private void throwIfClosed() {
          if (mInput == null) {
              throw new IllegalStateException("Already closed");
          }
      }
  
      private void throwIfPageOpened() {
          if (mCurrentPage != null) {
              throw new IllegalStateException("Current page not closed");
          }
      }
  
      private void throwIfPageNotInDocument(int pageIndex) {
          if (pageIndex < 0 || pageIndex >= mPageCount) {
              throw new IllegalArgumentException("Invalid page index");
          }
      }
  
      /**
       * This class represents a PDF document page for rendering.
       */
      public final class Page implements AutoCloseable {
  
          private final CloseGuard mCloseGuard = CloseGuard.get();
  
          /**
           * Mode to render the content for display on a screen.
           */
          public static final int RENDER_MODE_FOR_DISPLAY = 1;
  
          /**
           * Mode to render the content for printing.
           */
          public static final int RENDER_MODE_FOR_PRINT = 2;
  
          private final int mIndex;
          private final int mWidth;
          private final int mHeight;
  
          private long mNativePage;
  
          private Page(int index) {
              Point size = mTempPoint;
              synchronized (sPdfiumLock) {
                  mNativePage = nativeOpenPageAndGetSize(mNativeDocument, index, size);
              }
              mIndex = index;
              mWidth = size.x;
              mHeight = size.y;
              mCloseGuard.open("close");
          }
  
          /**
           * Gets the page index.
           *
           * @return The index.
           */
          public int getIndex() {
              return  mIndex;
          }
  
          /**
           * Gets the page width in points (1/72").
           *
           * @return The width in points.
           */
          public int getWidth() {
              return mWidth;
          }
  
          /**
           * Gets the page height in points (1/72").
           *
           * @return The height in points.
           */
          public int getHeight() {
              return mHeight;
          }
  
          /**
           * Renders a page to a bitmap.
           * <p>
           * You may optionally specify a rectangular clip in the bitmap bounds. No rendering
           * outside the clip will be performed, hence it is your responsibility to initialize
           * the bitmap outside the clip.
           * </p>
           * <p>
           * You may optionally specify a matrix to transform the content from page coordinates
           * which are in points (1/72") to bitmap coordinates which are in pixels. If this
           * matrix is not provided this method will apply a transformation that will fit the
           * whole page to the destination clip if provided or the destination bitmap if no
           * clip is provided.
           * </p>
           * <p>
           * The clip and transformation are useful for implementing tile rendering where the
           * destination bitmap contains a portion of the image, for example when zooming.
           * Another useful application is for printing where the size of the bitmap holding
           * the page is too large and a client can render the page in stripes.
           * </p>
           * <p>
           * <strong>Note: </strong> The destination bitmap format must be
           * {@link Config#ARGB_8888 ARGB}.
           * </p>
           * <p>
           * <strong>Note: </strong> The optional transformation matrix must be affine as per
           * {@link android.graphics.Matrix#isAffine() Matrix.isAffine()}. Hence, you can specify
           * rotation, scaling, translation but not a perspective transformation.
           * </p>
           *
           * @param destination Destination bitmap to which to render.
           * @param destClip Optional clip in the bitmap bounds.
           * @param transform Optional transformation to apply when rendering.
           * @param renderMode The render mode.
           *
           * @see #RENDER_MODE_FOR_DISPLAY
           * @see #RENDER_MODE_FOR_PRINT
           */
          public void render(@NonNull Bitmap destination, @Nullable Rect destClip,
                             @Nullable Matrix transform, @RenderMode int renderMode) {
              if (mNativePage == 0) {
                  throw new NullPointerException();
              }
  
              destination = Preconditions.checkNotNull(destination, "bitmap null");
  
              if (destination.getConfig() != Config.ARGB_8888) {
                  throw new IllegalArgumentException("Unsupported pixel format");
              }
  
              if (destClip != null) {
                  if (destClip.left < 0 || destClip.top < 0
                          || destClip.right > destination.getWidth()
                          || destClip.bottom > destination.getHeight()) {
                      throw new IllegalArgumentException("destBounds not in destination");
                  }
              }
  
              if (transform != null && !transform.isAffine()) {
                   throw new IllegalArgumentException("transform not affine");
              }
  
              if (renderMode != RENDER_MODE_FOR_PRINT && renderMode != RENDER_MODE_FOR_DISPLAY) {
                  throw new IllegalArgumentException("Unsupported render mode");
              }
  
              if (renderMode == RENDER_MODE_FOR_PRINT && renderMode == RENDER_MODE_FOR_DISPLAY) {
                  throw new IllegalArgumentException("Only single render mode supported");
              }
  
              final int contentLeft = (destClip != null) ? destClip.left : 0;
              final int contentTop = (destClip != null) ? destClip.top : 0;
              final int contentRight = (destClip != null) ? destClip.right
                      : destination.getWidth();
              final int contentBottom = (destClip != null) ? destClip.bottom
                      : destination.getHeight();
  
              // If transform is not set, stretch page to whole clipped area
              if (transform == null) {
                  int clipWidth = contentRight - contentLeft;
                  int clipHeight = contentBottom - contentTop;
  
                  transform = new Matrix();
                  transform.postScale((float)clipWidth / getWidth(),
                          (float)clipHeight / getHeight());
                  transform.postTranslate(contentLeft, contentTop);
              }
  
              final long transformPtr = transform.native_instance;
  
              synchronized (sPdfiumLock) {
                  nativeRenderPage(mNativeDocument, mNativePage, destination.getNativeInstance(),
                          contentLeft, contentTop, contentRight, contentBottom, transformPtr,
                          renderMode);
              }
          }
  
          /**
           * Closes this page.
           *
           * @see android.graphics.pdf.PdfRenderer#openPage(int)
           */
          @Override
          public void close() {
              throwIfClosed();
              doClose();
          }
  
          @Override
          protected void finalize() throws Throwable {
              try {
                  if (mCloseGuard != null) {
                      mCloseGuard.warnIfOpen();
                  }
  
                  doClose();
              } finally {
                  super.finalize();
              }
          }
  
          private void doClose() {
              if (mNativePage != 0) {
                  synchronized (sPdfiumLock) {
                      nativeClosePage(mNativePage);
                  }
                  mNativePage = 0;
              }
  
              mCloseGuard.close();
              mCurrentPage = null;
          }
  
          private void throwIfClosed() {
              if (mNativePage == 0) {
                  throw new IllegalStateException("Already closed");
              }
          }
      }
  
      private static native long nativeCreate(int fd, long size);
      private static native void nativeClose(long documentPtr);
      private static native int nativeGetPageCount(long documentPtr);
      private static native boolean nativeScaleForPrinting(long documentPtr);
      private static native void nativeRenderPage(long documentPtr, long pagePtr, long bitmapHandle,
              int clipLeft, int clipTop, int clipRight, int clipBottom, long transformPtr,
              int renderMode);
      private static native long nativeOpenPageAndGetSize(long documentPtr, int pageIndex,
              Point outSize);
      private static native void nativeClosePage(long pagePtr);
  }
  