public final class

PrintHelper

extends java.lang.Object

 java.lang.Object

↳androidx.print.PrintHelper

Gradle dependencies

compile group: 'androidx.print', name: 'print', version: '1.1.0-beta01'

  • groupId: androidx.print
  • artifactId: print
  • version: 1.1.0-beta01

Artifact androidx.print:print:1.1.0-beta01 it located at Google repository (https://maven.google.com/)

Androidx artifact mapping:

androidx.print:print com.android.support:print

Androidx class mapping:

androidx.print.PrintHelper android.support.v4.print.PrintHelper

Overview

Helper for printing bitmaps.

Summary

Fields
public static final intCOLOR_MODE_COLOR

this is a color image (default)

public static final intCOLOR_MODE_MONOCHROME

this is a black and white image

public static final intORIENTATION_LANDSCAPE

Print the image in landscape orientation (default).

public static final intORIENTATION_PORTRAIT

Print the image in portrait orientation.

public static final intSCALE_MODE_FILL

image will fill the paper and be cropped (default)

public static final intSCALE_MODE_FIT

image will be scaled but leave white space

Constructors
publicPrintHelper(Context context)

Constructs the PrintHelper that can be used to print images.

Methods
public intgetColorMode()

Gets the color mode with which the image will be printed.

public intgetOrientation()

Gets whether the image will be printed in landscape or portrait.

public intgetScaleMode()

Returns the scale mode with which the image will fill the paper.

public voidprintBitmap(java.lang.String jobName, Bitmap bitmap)

Prints a bitmap.

public voidprintBitmap(java.lang.String jobName, Bitmap bitmap, PrintHelper.OnPrintFinishCallback callback)

Prints a bitmap.

public voidprintBitmap(java.lang.String jobName, Uri imageFile)

Prints an image located at the Uri.

public voidprintBitmap(java.lang.String jobName, Uri imageFile, PrintHelper.OnPrintFinishCallback callback)

Prints an image located at the Uri.

public voidsetColorMode(int colorMode)

Sets whether the image will be printed in color (default) PrintHelper.COLOR_MODE_COLOR or in back and white PrintHelper.COLOR_MODE_MONOCHROME.

public voidsetOrientation(int orientation)

Sets whether the image will be printed in landscape PrintHelper.ORIENTATION_LANDSCAPE (default) or portrait PrintHelper.ORIENTATION_PORTRAIT.

public voidsetScaleMode(int scaleMode)

Selects whether the image will fill the paper and be cropped PrintHelper.SCALE_MODE_FIT or whether the image will be scaled but leave white space PrintHelper.SCALE_MODE_FILL.

public static booleansystemSupportsPrint()

Gets whether the system supports printing.

from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final int SCALE_MODE_FIT

image will be scaled but leave white space

public static final int SCALE_MODE_FILL

image will fill the paper and be cropped (default)

public static final int COLOR_MODE_MONOCHROME

this is a black and white image

public static final int COLOR_MODE_COLOR

this is a color image (default)

public static final int ORIENTATION_LANDSCAPE

Print the image in landscape orientation (default).

public static final int ORIENTATION_PORTRAIT

Print the image in portrait orientation.

Constructors

public PrintHelper(Context context)

Constructs the PrintHelper that can be used to print images.

Parameters:

context: A context for accessing system resources.

Methods

public static boolean systemSupportsPrint()

Gets whether the system supports printing.

Returns:

True if printing is supported.

public void setScaleMode(int scaleMode)

Selects whether the image will fill the paper and be cropped PrintHelper.SCALE_MODE_FIT or whether the image will be scaled but leave white space PrintHelper.SCALE_MODE_FILL.

Parameters:

scaleMode: PrintHelper.SCALE_MODE_FIT or PrintHelper.SCALE_MODE_FILL

public int getScaleMode()

Returns the scale mode with which the image will fill the paper.

Returns:

The scale Mode: PrintHelper.SCALE_MODE_FIT or PrintHelper.SCALE_MODE_FILL

public void setColorMode(int colorMode)

Sets whether the image will be printed in color (default) PrintHelper.COLOR_MODE_COLOR or in back and white PrintHelper.COLOR_MODE_MONOCHROME.

Parameters:

colorMode: The color mode which is one of PrintHelper.COLOR_MODE_COLOR and PrintHelper.COLOR_MODE_MONOCHROME.

public int getColorMode()

Gets the color mode with which the image will be printed.

Returns:

The color mode which is one of PrintHelper.COLOR_MODE_COLOR and PrintHelper.COLOR_MODE_MONOCHROME.

public void setOrientation(int orientation)

Sets whether the image will be printed in landscape PrintHelper.ORIENTATION_LANDSCAPE (default) or portrait PrintHelper.ORIENTATION_PORTRAIT.

Parameters:

orientation: The page orientation which is one of PrintHelper.ORIENTATION_LANDSCAPE or PrintHelper.ORIENTATION_PORTRAIT.

public int getOrientation()

Gets whether the image will be printed in landscape or portrait.

Returns:

The page orientation which is one of PrintHelper.ORIENTATION_LANDSCAPE or PrintHelper.ORIENTATION_PORTRAIT.

public void printBitmap(java.lang.String jobName, Bitmap bitmap)

Prints a bitmap.

Parameters:

jobName: The print job name.
bitmap: The bitmap to print.

public void printBitmap(java.lang.String jobName, Bitmap bitmap, PrintHelper.OnPrintFinishCallback callback)

Prints a bitmap.

Parameters:

jobName: The print job name.
bitmap: The bitmap to print.
callback: Optional callback to observe when printing is finished.

public void printBitmap(java.lang.String jobName, Uri imageFile)

Prints an image located at the Uri. Image types supported are those of

Parameters:

jobName: The print job name.
imageFile: The Uri pointing to an image to print.

public void printBitmap(java.lang.String jobName, Uri imageFile, PrintHelper.OnPrintFinishCallback callback)

Prints an image located at the Uri. Image types supported are those of

Parameters:

jobName: The print job name.
imageFile: The Uri pointing to an image to print.
callback: Optional callback to observe when printing is finished.

Source

/*
 * Copyright 2018 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 androidx.print;

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.ColorSpace;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.pdf.PdfDocument;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.print.PageRange;
import android.print.PrintAttributes;
import android.print.PrintDocumentAdapter;
import android.print.PrintDocumentInfo;
import android.print.PrintManager;
import android.print.pdf.PrintedPdfDocument;
import android.util.Log;

import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Helper for printing bitmaps.
 */
@SuppressWarnings("deprecation")
public final class PrintHelper {
    private static final String LOG_TAG = "PrintHelper";
    // will be <= 300 dpi on A4 (8.3×11.7) paper (worst case of 150 dpi)
    private static final int MAX_PRINT_SIZE = 3500;

    /**
     * Whether the PrintActivity respects the suggested orientation.
     *
     * There is a bug in the PrintActivity that causes it to ignore the orientation
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static final boolean PRINT_ACTIVITY_RESPECTS_ORIENTATION =
            Build.VERSION.SDK_INT < 20 || Build.VERSION.SDK_INT > 23;

    /**
     * Whether the print subsystem handles min margins correctly. If not the print helper needs
     * to fake this.
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static final boolean IS_MIN_MARGINS_HANDLING_CORRECT = Build.VERSION.SDK_INT != 23;

    /**
     * image will be scaled but leave white space
     */
    public static final int SCALE_MODE_FIT = 1;

    /**
     * image will fill the paper and be cropped (default)
     */
    public static final int SCALE_MODE_FILL = 2;

    /**
     * this is a black and white image
     */
    @SuppressLint("InlinedApi")
    public static final int COLOR_MODE_MONOCHROME = PrintAttributes.COLOR_MODE_MONOCHROME;

    /**
     * this is a color image (default)
     */
    @SuppressLint("InlinedApi")
    public static final int COLOR_MODE_COLOR = PrintAttributes.COLOR_MODE_COLOR;

    /**
     * Print the image in landscape orientation (default).
     */
    public static final int ORIENTATION_LANDSCAPE = 1;

    /**
     * Print the image in  portrait orientation.
     */
    public static final int ORIENTATION_PORTRAIT = 2;

    /**
     * Callback for observing when a print operation is completed.
     * When print is finished either the system acquired the
     * document to print or printing was cancelled.
     */
    public interface OnPrintFinishCallback {
        /**
         * Called when a print operation is finished.
         */
        void onFinish();
    }

    @IntDef({SCALE_MODE_FIT, SCALE_MODE_FILL})
    @Retention(RetentionPolicy.SOURCE)
    private @interface ScaleMode {}

    @IntDef({COLOR_MODE_MONOCHROME, COLOR_MODE_COLOR})
    @Retention(RetentionPolicy.SOURCE)
    private @interface ColorMode {}

    @IntDef({ORIENTATION_LANDSCAPE, ORIENTATION_PORTRAIT})
    @Retention(RetentionPolicy.SOURCE)
    private @interface Orientation {}

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Context mContext;

    BitmapFactory.Options mDecodeOptions = null;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Object mLock = new Object();

    @ScaleMode int mScaleMode = SCALE_MODE_FILL;
    @ColorMode int mColorMode = COLOR_MODE_COLOR;
    @Orientation int mOrientation = ORIENTATION_LANDSCAPE;

    /**
     * Gets whether the system supports printing.
     *
     * @return True if printing is supported.
     */
    public static boolean systemSupportsPrint() {
        // Supported on Android 4.4 or later.
        return Build.VERSION.SDK_INT >= 19;
    }

    /**
     * Constructs the PrintHelper that can be used to print images.
     *
     * @param context A context for accessing system resources.
     */
    public PrintHelper(@NonNull Context context) {
        mContext = context;
    }

    /**
     * Selects whether the image will fill the paper and be cropped
     * {@link #SCALE_MODE_FIT}
     * or whether the image will be scaled but leave white space
     * {@link #SCALE_MODE_FILL}.
     *
     * @param scaleMode {@link #SCALE_MODE_FIT} or
     *                  {@link #SCALE_MODE_FILL}
     */
    public void setScaleMode(@ScaleMode int scaleMode) {
        mScaleMode = scaleMode;
    }

    /**
     * Returns the scale mode with which the image will fill the paper.
     *
     * @return The scale Mode: {@link #SCALE_MODE_FIT} or
     * {@link #SCALE_MODE_FILL}
     */
    @ScaleMode
    public int getScaleMode() {
        return mScaleMode;
    }

    /**
     * Sets whether the image will be printed in color (default)
     * {@link #COLOR_MODE_COLOR} or in back and white
     * {@link #COLOR_MODE_MONOCHROME}.
     *
     * @param colorMode The color mode which is one of
     * {@link #COLOR_MODE_COLOR} and {@link #COLOR_MODE_MONOCHROME}.
     */
    public void setColorMode(@ColorMode int colorMode) {
        mColorMode = colorMode;
    }

    /**
     * Gets the color mode with which the image will be printed.
     *
     * @return The color mode which is one of {@link #COLOR_MODE_COLOR}
     * and {@link #COLOR_MODE_MONOCHROME}.
     */
    @ColorMode
    public int getColorMode() {
        return mColorMode;
    }

    /**
     * Sets whether the image will be printed in landscape {@link #ORIENTATION_LANDSCAPE} (default)
     * or portrait {@link #ORIENTATION_PORTRAIT}.
     *
     * @param orientation The page orientation which is one of
     *                    {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}.
     */
    public void setOrientation(int orientation) {
        mOrientation = orientation;
    }

    /**
     * Gets whether the image will be printed in landscape or portrait.
     *
     * @return The page orientation which is one of
     * {@link #ORIENTATION_LANDSCAPE} or {@link #ORIENTATION_PORTRAIT}.
     */
    public int getOrientation() {
        // Unset defaults to landscape but might turn image
        if (Build.VERSION.SDK_INT >= 19 && mOrientation == 0) {
            return ORIENTATION_LANDSCAPE;
        }
        return mOrientation;
    }


    /**
     * Prints a bitmap.
     *
     * @param jobName The print job name.
     * @param bitmap  The bitmap to print.
     */
    public void printBitmap(@NonNull String jobName, @NonNull Bitmap bitmap) {
        printBitmap(jobName, bitmap, null);
    }

    /**
     * Prints a bitmap.
     *
     * @param jobName The print job name.
     * @param bitmap  The bitmap to print.
     * @param callback Optional callback to observe when printing is finished.
     */
    public void printBitmap(@NonNull final String jobName, @NonNull final Bitmap bitmap,
            @Nullable final OnPrintFinishCallback callback) {
        if (Build.VERSION.SDK_INT < 19 || bitmap == null) {
            return;
        }

        PrintManager printManager =
                (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
        PrintAttributes.MediaSize mediaSize;
        if (isPortrait(bitmap)) {
            mediaSize = PrintAttributes.MediaSize.UNKNOWN_PORTRAIT;
        } else {
            mediaSize = PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE;
        }
        PrintAttributes attr = new PrintAttributes.Builder()
                .setMediaSize(mediaSize)
                .setColorMode(mColorMode)
                .build();

        printManager.print(jobName,
                new PrintBitmapAdapter(jobName, mScaleMode, bitmap, callback), attr);
    }

    @RequiresApi(19)
    private class PrintBitmapAdapter extends PrintDocumentAdapter {
        private final String mJobName;
        private final int mFittingMode;
        private final Bitmap mBitmap;
        private final OnPrintFinishCallback mCallback;
        private PrintAttributes mAttributes;

        PrintBitmapAdapter(String jobName, int fittingMode, Bitmap bitmap,
                OnPrintFinishCallback callback) {
            mJobName = jobName;
            mFittingMode = fittingMode;
            mBitmap = bitmap;
            mCallback = callback;
        }

        @Override
        public void onLayout(PrintAttributes oldPrintAttributes,
                PrintAttributes newPrintAttributes,
                CancellationSignal cancellationSignal,
                LayoutResultCallback layoutResultCallback,
                Bundle bundle) {

            mAttributes = newPrintAttributes;

            PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName)
                    .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
                    .setPageCount(1)
                    .build();
            boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
            layoutResultCallback.onLayoutFinished(info, changed);
        }

        @Override
        public void onWrite(PageRange[] pageRanges,
                ParcelFileDescriptor fileDescriptor,
                CancellationSignal cancellationSignal,
                WriteResultCallback writeResultCallback) {
            writeBitmap(mAttributes, mFittingMode, mBitmap, fileDescriptor,
                    cancellationSignal, writeResultCallback);
        }

        @Override
        public void onFinish() {
            if (mCallback != null) {
                mCallback.onFinish();
            }
        }
    }

    /**
     * Prints an image located at the Uri. Image types supported are those of
     * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream)
     * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)}
     *
     * @param jobName   The print job name.
     * @param imageFile The <code>Uri</code> pointing to an image to print.
     * @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image.
     */
    public void printBitmap(@NonNull String jobName, @NonNull Uri imageFile)
            throws FileNotFoundException {
        printBitmap(jobName, imageFile, null);
    }

    /**
     * Prints an image located at the Uri. Image types supported are those of
     * {@link android.graphics.BitmapFactory#decodeStream(java.io.InputStream)
     * android.graphics.BitmapFactory.decodeStream(java.io.InputStream)}
     *
     * @param jobName   The print job name.
     * @param imageFile The <code>Uri</code> pointing to an image to print.
     * @throws FileNotFoundException if <code>Uri</code> is not pointing to a valid image.
     * @param callback Optional callback to observe when printing is finished.
     */
    public void printBitmap(@NonNull final String jobName, @NonNull final Uri imageFile,
            @Nullable final OnPrintFinishCallback callback)
            throws FileNotFoundException {
        if (Build.VERSION.SDK_INT < 19) {
            return;
        }

        PrintDocumentAdapter printDocumentAdapter = new PrintUriAdapter(jobName, imageFile,
                callback, mScaleMode);

        PrintManager printManager =
                (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE);
        PrintAttributes.Builder builder = new PrintAttributes.Builder();
        builder.setColorMode(mColorMode);

        if (mOrientation == ORIENTATION_LANDSCAPE || mOrientation == 0) {
            builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_LANDSCAPE);
        } else if (mOrientation == ORIENTATION_PORTRAIT) {
            builder.setMediaSize(PrintAttributes.MediaSize.UNKNOWN_PORTRAIT);
        }
        PrintAttributes attr = builder.build();

        printManager.print(jobName, printDocumentAdapter, attr);
    }

    @SuppressWarnings("deprecation")
    @RequiresApi(19)
    private class PrintUriAdapter extends PrintDocumentAdapter {
        final String mJobName;
        final Uri mImageFile;
        final OnPrintFinishCallback mCallback;
        final int mFittingMode;
        PrintAttributes mAttributes;
        android.os.AsyncTask<Uri, Boolean, Bitmap> mLoadBitmap;
        Bitmap mBitmap;

        PrintUriAdapter(String jobName, Uri imageFile, OnPrintFinishCallback callback,
                int fittingMode) {
            mJobName = jobName;
            mImageFile = imageFile;
            mCallback = callback;
            mFittingMode = fittingMode;
            mBitmap = null;
        }

        @Override
        public void onLayout(final PrintAttributes oldPrintAttributes,
                final PrintAttributes newPrintAttributes,
                final CancellationSignal cancellationSignal,
                final LayoutResultCallback layoutResultCallback,
                Bundle bundle) {

            synchronized (this) {
                mAttributes = newPrintAttributes;
            }


            if (cancellationSignal.isCanceled()) {
                layoutResultCallback.onLayoutCancelled();
                return;
            }
            // we finished the load
            if (mBitmap != null) {
                PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName)
                        .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
                        .setPageCount(1)
                        .build();
                boolean changed = !newPrintAttributes.equals(oldPrintAttributes);
                layoutResultCallback.onLayoutFinished(info, changed);
                return;
            }

            mLoadBitmap = new android.os.AsyncTask<Uri, Boolean, Bitmap>() {
                @Override
                protected void onPreExecute() {
                    // First register for cancellation requests.
                    cancellationSignal.setOnCancelListener(
                            new CancellationSignal.OnCancelListener() {
                                @Override
                                public void onCancel() { // on different thread
                                    cancelLoad();
                                    cancel(false);
                                }
                            });
                }

                @Override
                protected Bitmap doInBackground(Uri... uris) {
                    try {
                        return loadConstrainedBitmap(mImageFile);
                    } catch (FileNotFoundException e) {
                        /* ignore */
                    }
                    return null;
                }

                @Override
                protected void onPostExecute(Bitmap bitmap) {
                    super.onPostExecute(bitmap);

                    // If orientation was not set by the caller, try to fit the bitmap on
                    // the current paper by potentially rotating the bitmap by 90 degrees.
                    if (bitmap != null
                            && (!PRINT_ACTIVITY_RESPECTS_ORIENTATION || mOrientation == 0)) {
                        PrintAttributes.MediaSize mediaSize;

                        synchronized (this) {
                            mediaSize = mAttributes.getMediaSize();
                        }

                        if (mediaSize != null) {
                            if (mediaSize.isPortrait() != isPortrait(bitmap)) {
                                Matrix rotation = new Matrix();

                                rotation.postRotate(90);
                                bitmap = Bitmap.createBitmap(bitmap, 0, 0,
                                        bitmap.getWidth(), bitmap.getHeight(), rotation,
                                        true);
                            }
                        }
                    }

                    mBitmap = bitmap;
                    if (bitmap != null) {
                        PrintDocumentInfo info = new PrintDocumentInfo.Builder(mJobName)
                                .setContentType(PrintDocumentInfo.CONTENT_TYPE_PHOTO)
                                .setPageCount(1)
                                .build();

                        boolean changed = !newPrintAttributes.equals(oldPrintAttributes);

                        layoutResultCallback.onLayoutFinished(info, changed);

                    } else {
                        layoutResultCallback.onLayoutFailed(null);
                    }
                    mLoadBitmap = null;
                }

                @Override
                protected void onCancelled(Bitmap result) {
                    // Task was cancelled, report that.
                    layoutResultCallback.onLayoutCancelled();
                    mLoadBitmap = null;
                }
            }.execute();
        }

        @SuppressWarnings("deprecation") // Call to requestCancelDecode()
        void cancelLoad() {
            synchronized (mLock) { // prevent race with set null below
                if (mDecodeOptions != null) {
                    if (Build.VERSION.SDK_INT < 24) {
                        // This only affects API 23 and older.
                        mDecodeOptions.requestCancelDecode();
                    }
                    mDecodeOptions = null;
                }
            }
        }

        @Override
        public void onFinish() {
            super.onFinish();
            cancelLoad();
            if (mLoadBitmap != null) {
                mLoadBitmap.cancel(true);
            }
            if (mCallback != null) {
                mCallback.onFinish();
            }
            if (mBitmap != null) {
                mBitmap.recycle();
                mBitmap = null;
            }
        }

        @Override
        public void onWrite(PageRange[] pageRanges, ParcelFileDescriptor fileDescriptor,
                CancellationSignal cancellationSignal,
                WriteResultCallback writeResultCallback) {
            writeBitmap(mAttributes, mFittingMode, mBitmap, fileDescriptor,
                    cancellationSignal, writeResultCallback);
        }
    }

    /**
     * Check if the supplied bitmap should best be printed on a portrait orientation paper.
     *
     * @param bitmap The bitmap to be printed.
     * @return true iff the picture should best be printed on a portrait orientation paper.
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static boolean isPortrait(Bitmap bitmap) {
        return bitmap.getWidth() <= bitmap.getHeight();
    }

    /**
     * Create a build with a copy from the other print attributes.
     *
     * @param other The other print attributes
     *
     * @return A builder that will build print attributes that match the other attributes
     */
    @RequiresApi(19)
    private static PrintAttributes.Builder copyAttributes(PrintAttributes other) {
        PrintAttributes.Builder b = new PrintAttributes.Builder()
                .setMediaSize(other.getMediaSize())
                .setResolution(other.getResolution())
                .setMinMargins(other.getMinMargins());

        if (other.getColorMode() != 0) {
            b.setColorMode(other.getColorMode());
        }

        if (Build.VERSION.SDK_INT >= 23) {
            if (other.getDuplexMode() != 0) {
                b.setDuplexMode(other.getDuplexMode());
            }
        }

        return b;
    }

    /**
     * Calculates the transform the print an Image to fill the page
     *
     * @param imageWidth  with of bitmap
     * @param imageHeight height of bitmap
     * @param content     The output page dimensions
     * @param fittingMode The mode of fitting {@link #SCALE_MODE_FILL} vs
     *                    {@link #SCALE_MODE_FIT}
     * @return Matrix to be used in canvas.drawBitmap(bitmap, matrix, null) call
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static Matrix getMatrix(int imageWidth, int imageHeight, RectF content,
            @ScaleMode int fittingMode) {
        Matrix matrix = new Matrix();

        // Compute and apply scale to fill the page.
        float scale = content.width() / imageWidth;
        if (fittingMode == SCALE_MODE_FILL) {
            scale = Math.max(scale, content.height() / imageHeight);
        } else {
            scale = Math.min(scale, content.height() / imageHeight);
        }
        matrix.postScale(scale, scale);

        // Center the content.
        final float translateX = (content.width()
                - imageWidth * scale) / 2;
        final float translateY = (content.height()
                - imageHeight * scale) / 2;
        matrix.postTranslate(translateX, translateY);
        return matrix;
    }

    /**
     * Write a bitmap for a PDF document.
     *
     * @param attributes          The print attributes
     * @param fittingMode         How to fit the bitmap
     * @param bitmap              The bitmap to write
     * @param fileDescriptor      The file to write to
     * @param cancellationSignal  Signal cancelling operation
     * @param writeResultCallback Callback to call once written
     */
    @RequiresApi(19)
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    void writeBitmap(final PrintAttributes attributes, final int fittingMode,
            final Bitmap bitmap, final ParcelFileDescriptor fileDescriptor,
            final CancellationSignal cancellationSignal,
            final PrintDocumentAdapter.WriteResultCallback writeResultCallback) {
        final PrintAttributes pdfAttributes;
        if (IS_MIN_MARGINS_HANDLING_CORRECT) {
            pdfAttributes = attributes;
        } else {
            // If the handling of any margin != 0 is broken, strip the margins and add them to
            // the bitmap later
            pdfAttributes = copyAttributes(attributes)
                    .setMinMargins(new PrintAttributes.Margins(0, 0, 0, 0)).build();
        }

        new android.os.AsyncTask<Void, Void, Throwable>() {
            @Override
            protected Throwable doInBackground(Void... params) {
                try {
                    if (cancellationSignal.isCanceled()) {
                        return null;
                    }

                    PrintedPdfDocument pdfDocument = new PrintedPdfDocument(mContext,
                            pdfAttributes);

                    Bitmap maybeGrayscale = convertBitmapForColorMode(bitmap,
                            pdfAttributes.getColorMode());

                    if (cancellationSignal.isCanceled()) {
                        return null;
                    }

                    try {
                        PdfDocument.Page page = pdfDocument.startPage(1);

                        RectF contentRect;
                        if (IS_MIN_MARGINS_HANDLING_CORRECT) {
                            contentRect = new RectF(page.getInfo().getContentRect());
                        } else {
                            // Create dummy doc that has the margins to compute correctly sized
                            // content rectangle
                            PrintedPdfDocument dummyDocument = new PrintedPdfDocument(mContext,
                                    attributes);
                            PdfDocument.Page dummyPage = dummyDocument.startPage(1);
                            contentRect = new RectF(dummyPage.getInfo().getContentRect());
                            dummyDocument.finishPage(dummyPage);
                            dummyDocument.close();
                        }

                        // Resize bitmap
                        Matrix matrix = getMatrix(
                                maybeGrayscale.getWidth(), maybeGrayscale.getHeight(),
                                contentRect, fittingMode);

                        if (IS_MIN_MARGINS_HANDLING_CORRECT) {
                            // The pdfDocument takes care of the positioning and margins
                        } else {
                            // Move it to the correct position.
                            matrix.postTranslate(contentRect.left, contentRect.top);

                            // Cut off margins
                            page.getCanvas().clipRect(contentRect);
                        }

                        // Draw the bitmap.
                        page.getCanvas().drawBitmap(maybeGrayscale, matrix, null);

                        // Finish the page.
                        pdfDocument.finishPage(page);

                        if (cancellationSignal.isCanceled()) {
                            return null;
                        }

                        // Write the document.
                        pdfDocument.writeTo(
                                new FileOutputStream(fileDescriptor.getFileDescriptor()));
                        return null;
                    } finally {
                        pdfDocument.close();

                        if (fileDescriptor != null) {
                            try {
                                fileDescriptor.close();
                            } catch (IOException ioe) {
                                // ignore
                            }
                        }
                        // If we created a new instance for grayscaling, then recycle it here.
                        if (maybeGrayscale != bitmap) {
                            maybeGrayscale.recycle();
                        }
                    }
                } catch (Throwable t) {
                    return t;
                }
            }

            @Override
            protected void onPostExecute(Throwable throwable) {
                if (cancellationSignal.isCanceled()) {
                    // Cancelled.
                    writeResultCallback.onWriteCancelled();
                } else if (throwable == null) {
                    // Done.
                    writeResultCallback.onWriteFinished(
                            new PageRange[] { PageRange.ALL_PAGES });
                } else {
                    // Failed.
                    Log.e(LOG_TAG, "Error writing printed content", throwable);
                    writeResultCallback.onWriteFailed(null);
                }
            }
        }.execute();
    }

    /**
     * Loads a bitmap while limiting its size
     *
     * @param uri           location of a valid image
     * @return the Bitmap
     * @throws FileNotFoundException if the Uri does not point to an image
     */
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    Bitmap loadConstrainedBitmap(Uri uri) throws FileNotFoundException {
        if (uri == null || mContext == null) {
            throw new IllegalArgumentException("bad argument to getScaledBitmap");
        }
        // Get width and height of stored bitmap
        BitmapFactory.Options opt = new BitmapFactory.Options();
        opt.inJustDecodeBounds = true;
        loadBitmap(uri, opt);

        int w = opt.outWidth;
        int h = opt.outHeight;

        // If bitmap cannot be decoded, return null
        if (w <= 0 || h <= 0) {
            return null;
        }

        // Find best downsampling size
        int imageSide = Math.max(w, h);

        int sampleSize = 1;
        while (imageSide > MAX_PRINT_SIZE) {
            imageSide >>>= 1;
            sampleSize <<= 1;
        }

        // Make sure sample size is reasonable
        if (sampleSize <= 0 || 0 >= (Math.min(w, h) / sampleSize)) {
            return null;
        }
        BitmapFactory.Options decodeOptions;
        synchronized (mLock) { // prevent race with set null below
            mDecodeOptions = new BitmapFactory.Options();
            mDecodeOptions.inMutable = true;
            mDecodeOptions.inSampleSize = sampleSize;
            if (Build.VERSION.SDK_INT >= 26) {
                mDecodeOptions.inPreferredColorSpace = ColorSpace.get(ColorSpace.Named.SRGB);
            }
            decodeOptions = mDecodeOptions;
        }
        try {
            return loadBitmap(uri, decodeOptions);
        } finally {
            synchronized (mLock) {
                mDecodeOptions = null;
            }
        }
    }

    /**
     * Returns the bitmap from the given uri loaded using the given options.
     * Returns null on failure.
     */
    private Bitmap loadBitmap(Uri uri, BitmapFactory.Options o) throws FileNotFoundException {
        if (uri == null || mContext == null) {
            throw new IllegalArgumentException("bad argument to loadBitmap");
        }
        InputStream is = null;
        try {
            is = mContext.getContentResolver().openInputStream(uri);
            return BitmapFactory.decodeStream(is, null, o);
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException t) {
                    Log.w(LOG_TAG, "close fail ", t);
                }
            }
        }
    }

    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static Bitmap convertBitmapForColorMode(Bitmap original, @ColorMode int colorMode) {
        if (colorMode != COLOR_MODE_MONOCHROME) {
            return original;
        }
        // Create a grayscale bitmap
        Bitmap grayscale = Bitmap.createBitmap(original.getWidth(), original.getHeight(),
                Bitmap.Config.ARGB_8888);
        Canvas c = new Canvas(grayscale);
        Paint p = new Paint();
        ColorMatrix cm = new ColorMatrix();
        cm.setSaturation(0);
        ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
        p.setColorFilter(f);
        c.drawBitmap(original, 0, 0, p);
        c.setBitmap(null);

        return grayscale;
    }
}