| /* |
| * Copyright (C) 2016 The Android Open Source Project |
| * Copyright (C) 2016 - 2024 Mopria Alliance, Inc. |
| * |
| * 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 com.android.bips.ipp; |
| |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.Canvas; |
| import android.graphics.Matrix; |
| import android.graphics.Paint; |
| import android.graphics.Path; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffXfermode; |
| import android.net.Uri; |
| import android.os.AsyncTask; |
| import android.os.Build; |
| import android.os.ParcelFileDescriptor; |
| import android.print.PrintAttributes; |
| import android.print.PrintDocumentInfo; |
| import android.print.PrintJobInfo; |
| import android.printservice.PrintJob; |
| import android.util.Log; |
| import android.view.Gravity; |
| |
| import com.android.bips.ImagePrintActivity; |
| import com.android.bips.jni.BackendConstants; |
| import com.android.bips.jni.LocalJobParams; |
| import com.android.bips.jni.LocalPrinterCapabilities; |
| import com.android.bips.jni.MediaSizes; |
| import com.android.bips.jni.PdfRender; |
| import com.android.bips.jni.SizeD; |
| import com.android.bips.util.FileUtils; |
| |
| import java.io.BufferedOutputStream; |
| import java.io.File; |
| import java.io.FileOutputStream; |
| import java.io.IOException; |
| import java.util.Objects; |
| import java.nio.ByteBuffer; |
| |
| /** |
| * A background task that starts sending a print job. The result of this task is an integer |
| * defined by {@link Backend} ERROR_* codes or a non-negative code for success. |
| */ |
| class StartJobTask extends AsyncTask<Void, Void, Integer> { |
| private static final String TAG = StartJobTask.class.getSimpleName(); |
| private static final boolean DEBUG = false; |
| |
| private static final String MIME_TYPE_PDF = "application/pdf"; |
| |
| // see wprint_df_types.h for enum values |
| private static final int MEDIA_TYPE_PLAIN = 0; |
| private static final int MEDIA_TYPE_AUTO = 98; |
| // Unused but present |
| // private static final int MEDIA_TYPE_PHOTO = 1; |
| // private static final int MEDIA_TYPE_PHOTO_GLOSSY = 2; |
| |
| private static final int SIDES_SIMPLEX = 0; |
| private static final int SIDES_DUPLEX_LONG_EDGE = 1; |
| private static final int SIDES_DUPLEX_SHORT_EDGE = 2; |
| |
| private static final int RESOLUTION_300_DPI = 300; |
| private static final int MARGIN_CHECK_72_DPI = 72; |
| |
| private static final int COLOR_SPACE_MONOCHROME = 0; |
| private static final int COLOR_SPACE_COLOR = 1; |
| |
| private static final int BORDERLESS_OFF = 0; |
| private static final int BORDERLESS_ON = 1; |
| private static final float POINTS_PER_INCH = 72; |
| |
| /** Threshold value for catering slight variation b/w source and paper size dimensions*/ |
| private static final float PAGE_SIZE_EPSILON = 0.04f; |
| private final double mZoomFactor = MARGIN_CHECK_72_DPI / POINTS_PER_INCH; |
| private final Context mContext; |
| private final Backend mBackend; |
| private final Uri mDestination; |
| private final LocalPrinterCapabilities mCapabilities; |
| private final LocalJobParams mJobParams; |
| private final ParcelFileDescriptor mSourceFileDescriptor; |
| private final String mJobId; |
| private final PrintJobInfo mJobInfo; |
| private final PrintDocumentInfo mDocInfo; |
| private final MediaSizes mMediaSizes; |
| |
| StartJobTask(Context context, Backend backend, Uri destination, PrintJob printJob, |
| LocalPrinterCapabilities capabilities) { |
| mContext = context; |
| mBackend = backend; |
| mDestination = destination; |
| mCapabilities = capabilities; |
| mJobParams = new LocalJobParams(); |
| mJobId = printJob.getId().toString(); |
| mJobInfo = printJob.getInfo(); |
| mDocInfo = printJob.getDocument().getInfo(); |
| mSourceFileDescriptor = printJob.getDocument().getData(); |
| mMediaSizes = MediaSizes.getInstance(mContext); |
| } |
| |
| private void populateJobParams() { |
| PrintAttributes.MediaSize mediaSize = mJobInfo.getAttributes().getMediaSize(); |
| |
| mJobParams.borderless = isBorderless() ? BORDERLESS_ON : BORDERLESS_OFF; |
| mJobParams.duplex = getSides(); |
| mJobParams.num_copies = mJobInfo.getCopies(); |
| mJobParams.pdf_render_resolution = RESOLUTION_300_DPI; |
| mJobParams.fit_to_page = !getFillPage(); |
| mJobParams.fill_page = getFillPage(); |
| mJobParams.job_name = mJobInfo.getLabel(); |
| mJobParams.job_originating_user_name = Build.MODEL; |
| mJobParams.auto_rotate = false; |
| mJobParams.portrait_mode = mediaSize == null || mediaSize.isPortrait(); |
| mJobParams.landscape_mode = !mJobParams.portrait_mode; |
| mJobParams.media_size = mMediaSizes.toMediaCode(mediaSize); |
| mJobParams.media_type = getMediaType(); |
| mJobParams.color_space = getColorSpace(); |
| mJobParams.document_category = getDocumentCategory(); |
| mJobParams.shared_photo = isSharedPhoto(); |
| |
| mJobParams.job_margin_top = Math.max(mJobParams.job_margin_top, 0.0f); |
| mJobParams.job_margin_left = Math.max(mJobParams.job_margin_left, 0.0f); |
| mJobParams.job_margin_right = Math.max(mJobParams.job_margin_right, 0.0f); |
| mJobParams.job_margin_bottom = Math.max(mJobParams.job_margin_bottom, 0.0f); |
| |
| mJobParams.alignment = Gravity.CENTER; |
| } |
| |
| @Override |
| protected Integer doInBackground(Void... voids) { |
| if (DEBUG) Log.d(TAG, "doInBackground() job=" + mJobParams + ", cap=" + mCapabilities); |
| File tempFolder = new File(mContext.getFilesDir(), Backend.TEMP_JOB_FOLDER); |
| if (!FileUtils.makeDirectory(tempFolder)) { |
| Log.w(TAG, "makeDirectory failure"); |
| return Backend.ERROR_FILE; |
| } |
| |
| File pdfFile = new File(tempFolder, mJobId + ".pdf"); |
| try { |
| try { |
| FileUtils.copy(new ParcelFileDescriptor.AutoCloseInputStream(mSourceFileDescriptor), |
| new BufferedOutputStream(new FileOutputStream(pdfFile))); |
| } catch (IOException e) { |
| Log.w(TAG, "Error while copying to " + pdfFile, e); |
| return Backend.ERROR_FILE; |
| } |
| String[] files = new String[]{pdfFile.toString()}; |
| |
| // Address, without port. |
| String address = mDestination.getHost() + mDestination.getPath(); |
| |
| if (isCancelled()) { |
| return Backend.ERROR_CANCEL; |
| } |
| |
| // Get default job parameters |
| int result = mBackend.nativeGetDefaultJobParameters(mJobParams); |
| if (result != 0) { |
| if (DEBUG) Log.w(TAG, "nativeGetDefaultJobParameters failure: " + result); |
| return Backend.ERROR_UNKNOWN; |
| } |
| |
| if (isCancelled()) { |
| return Backend.ERROR_CANCEL; |
| } |
| |
| // Fill in job parameters from capabilities and print job info. |
| populateJobParams(); |
| PdfRender pdfRender = PdfRender.getInstance(mContext); |
| int pageCount = pdfRender.openDocument(pdfFile.getPath()); |
| if (pageCount > 0) { |
| SizeD pageSize = pdfRender.getPageSize(1); |
| if (mJobParams.portrait_mode) { |
| mJobParams.source_height = (float) pageSize.getHeight() / POINTS_PER_INCH; |
| mJobParams.source_width = (float) pageSize.getWidth() / POINTS_PER_INCH; |
| } else { |
| mJobParams.source_width = (float) pageSize.getHeight() / POINTS_PER_INCH; |
| mJobParams.source_height = (float) pageSize.getWidth() / POINTS_PER_INCH; |
| } |
| |
| // Print at 1:1 scale only if the page count is 1, the document is not a photo, the |
| // document size matches the paper size, and there is no content in the margins. |
| if (pageCount == 1) { |
| mJobParams.print_at_scale = !getDocumentCategory().equals( |
| BackendConstants.PRINT_DOCUMENT_CATEGORY__PHOTO) && |
| isDocSizeEqualsPaperSize() && |
| isContentAtMarginsEmpty(pdfRender, pointsToPixels(pageSize.getHeight()), |
| pointsToPixels(pageSize.getWidth())); |
| } |
| pdfRender.closeDocument(); |
| } |
| |
| // Finalize job parameters |
| mBackend.nativeGetFinalJobParameters(mJobParams, mCapabilities); |
| |
| if (isCancelled()) { |
| return Backend.ERROR_CANCEL; |
| } |
| if (DEBUG) { |
| Log.d(TAG, "nativeStartJob address=" + address |
| + " port=" + mDestination.getPort() + " mime=" + MIME_TYPE_PDF |
| + " files=" + files[0] + " job=" + mJobParams); |
| } |
| // Initiate job |
| result = mBackend.nativeStartJob(Backend.getIp(address), mDestination.getPort(), |
| MIME_TYPE_PDF, mJobParams, mCapabilities, files, null, |
| mDestination.getScheme()); |
| if (result < 0) { |
| Log.w(TAG, "nativeStartJob failure: " + result); |
| return Backend.ERROR_UNKNOWN; |
| } |
| |
| pdfFile = null; |
| return result; |
| } finally { |
| if (pdfFile != null) { |
| pdfFile.delete(); |
| } |
| } |
| } |
| |
| private boolean isDocSizeEqualsPaperSize() { |
| PrintAttributes.MediaSize mediaSize = mJobInfo.getAttributes().getMediaSize(); |
| if (mediaSize == null) { |
| return false; |
| } |
| float paperWidth = mediaSize.getWidthMils() / 1000f; |
| float paperHeight = mediaSize.getHeightMils() / 1000f; |
| return Math.abs(mJobParams.source_width - paperWidth) < PAGE_SIZE_EPSILON |
| && Math.abs(mJobParams.source_height - paperHeight) < PAGE_SIZE_EPSILON; |
| } |
| |
| /** Converts cmm(hundredths of mm) to pixels at 72 DPI. */ |
| private int cmmToPixels(int cmm) { |
| return Math.round((float) cmm / 2540 * MARGIN_CHECK_72_DPI); |
| } |
| |
| /** Converts points to pixels. */ |
| private int pointsToPixels(double points) { |
| return (int) Math.round(points * mZoomFactor); |
| } |
| |
| /** |
| * Returns true if there is no content in the margins of the printer. |
| */ |
| private boolean isContentAtMarginsEmpty(PdfRender pdfRender, int pageHeight, int pageWidth) { |
| int topMargin = cmmToPixels(mCapabilities.printerTopMargin); |
| int bottomMargin = cmmToPixels(mCapabilities.printerBottomMargin); |
| int leftMargin = cmmToPixels(mCapabilities.printerLeftMargin); |
| int rightMargin = cmmToPixels(mCapabilities.printerRightMargin); |
| |
| if (topMargin == 0 && bottomMargin == 0 && leftMargin == 0 && rightMargin == 0) { |
| return false; |
| } |
| |
| boolean emptyContentAtMargins = false; |
| Bitmap pageBitmap = pdfRender.renderPage(1, pageWidth, pageHeight); |
| if (pageBitmap != null) { |
| Bitmap overlayBmp = overlayBitmap(pageBitmap, topMargin, bottomMargin, leftMargin, |
| rightMargin); |
| ByteBuffer buff = ByteBuffer.allocate(overlayBmp.getByteCount()); |
| overlayBmp.copyPixelsToBuffer(buff); |
| emptyContentAtMargins = isEmptyByteArray(buff.array()); |
| overlayBmp.recycle(); |
| } |
| return emptyContentAtMargins; |
| } |
| |
| private boolean isEmptyByteArray(byte[] byteArray) { |
| for (byte b : byteArray) { |
| if (b > 0) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| private Bitmap overlayBitmap(Bitmap bmp, int topMargin, int bottomMargin, int leftMargin, |
| int rightMargin) { |
| Bitmap bmpOverlay = Bitmap.createBitmap(bmp.getWidth(), bmp.getHeight(), bmp.getConfig()); |
| Canvas canvas = new Canvas(bmpOverlay); |
| canvas.drawBitmap(bmp, new Matrix(), null); |
| |
| int printableWidth = bmp.getWidth() - (leftMargin + rightMargin); |
| int printableHeight = bmp.getHeight() - (topMargin + bottomMargin); |
| int posX = leftMargin; |
| int posY = topMargin; |
| |
| Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); |
| |
| Path path = new Path(); |
| path.addRect(posX, posY, posX + printableWidth, posY + printableHeight, Path.Direction.CW); |
| // Draw the path to clip on the canvas, thus removing the pixels. |
| canvas.drawPath(path, paint); |
| |
| bmp.recycle(); |
| return bmpOverlay; |
| } |
| |
| private boolean isBorderless() { |
| return mCapabilities.borderless |
| && mDocInfo.getContentType() == PrintDocumentInfo.CONTENT_TYPE_PHOTO; |
| } |
| |
| private int getSides() { |
| // Never duplex photo media; may damage printers |
| if (mDocInfo.getContentType() == PrintDocumentInfo.CONTENT_TYPE_PHOTO) { |
| return SIDES_SIMPLEX; |
| } |
| |
| switch (mJobInfo.getAttributes().getDuplexMode()) { |
| case PrintAttributes.DUPLEX_MODE_LONG_EDGE: |
| return SIDES_DUPLEX_LONG_EDGE; |
| case PrintAttributes.DUPLEX_MODE_SHORT_EDGE: |
| return SIDES_DUPLEX_SHORT_EDGE; |
| case PrintAttributes.DUPLEX_MODE_NONE: |
| default: |
| return SIDES_SIMPLEX; |
| } |
| } |
| |
| private boolean getFillPage() { |
| switch (mDocInfo.getContentType()) { |
| case PrintDocumentInfo.CONTENT_TYPE_PHOTO: |
| return true; |
| case PrintDocumentInfo.CONTENT_TYPE_UNKNOWN: |
| case PrintDocumentInfo.CONTENT_TYPE_DOCUMENT: |
| default: |
| return false; |
| } |
| } |
| |
| private int getMediaType() { |
| int mediaType = MEDIA_TYPE_PLAIN; |
| for (int supportedType : mCapabilities.supportedMediaTypes) { |
| if (supportedType == MEDIA_TYPE_AUTO) { |
| // if auto media is supported, use that and break out of the loop |
| mediaType = MEDIA_TYPE_AUTO; |
| break; |
| } else if (mDocInfo.getContentType() == PrintDocumentInfo.CONTENT_TYPE_PHOTO |
| && supportedType > mediaType) { |
| // Select the best (highest #) supported type for photos |
| mediaType = supportedType; |
| } |
| } |
| return mediaType; |
| } |
| |
| private int getColorSpace() { |
| switch (mJobInfo.getAttributes().getColorMode()) { |
| case PrintAttributes.COLOR_MODE_COLOR: |
| return COLOR_SPACE_COLOR; |
| case PrintAttributes.COLOR_MODE_MONOCHROME: |
| default: |
| return COLOR_SPACE_MONOCHROME; |
| } |
| } |
| |
| private String getDocumentCategory() { |
| switch (mDocInfo.getContentType()) { |
| case PrintDocumentInfo.CONTENT_TYPE_PHOTO: |
| return BackendConstants.PRINT_DOCUMENT_CATEGORY__PHOTO; |
| |
| case PrintDocumentInfo.CONTENT_TYPE_DOCUMENT: |
| default: |
| return BackendConstants.PRINT_DOCUMENT_CATEGORY__DOCUMENT; |
| } |
| } |
| |
| private boolean isSharedPhoto() { |
| return Objects.equals(mJobInfo.getId(), ImagePrintActivity.getLastPrintJobId()); |
| } |
| } |