/* * 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 com.android.printspooler.renderer; import android.app.Service; import android.content.Intent; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.pdf.PdfEditor; import android.graphics.pdf.PdfRenderer; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.print.PageRange; import android.print.PrintAttributes; import android.print.PrintAttributes.Margins; import android.util.Log; import android.view.View; import com.android.printspooler.util.PageRangeUtils; import libcore.io.IoUtils; import com.android.printspooler.util.BitmapSerializeUtils; import java.io.IOException; /** * Service for manipulation of PDF documents in an isolated process. */ public final class PdfManipulationService extends Service { public static final String ACTION_GET_RENDERER = "com.android.printspooler.renderer.ACTION_GET_RENDERER"; public static final String ACTION_GET_EDITOR = "com.android.printspooler.renderer.ACTION_GET_EDITOR"; public static final int ERROR_MALFORMED_PDF_FILE = -2; public static final int ERROR_SECURE_PDF_FILE = -3; private static final String LOG_TAG = "PdfManipulationService"; private static final boolean DEBUG = false; private static final int MILS_PER_INCH = 1000; private static final int POINTS_IN_INCH = 72; @Override public IBinder onBind(Intent intent) { String action = intent.getAction(); switch (action) { case ACTION_GET_RENDERER: { return new PdfRendererImpl(); } case ACTION_GET_EDITOR: { return new PdfEditorImpl(); } default: { throw new IllegalArgumentException("Invalid intent action:" + action); } } } private final class PdfRendererImpl extends IPdfRenderer.Stub { private final Object mLock = new Object(); private Bitmap mBitmap; private PdfRenderer mRenderer; @Override public int openDocument(ParcelFileDescriptor source) throws RemoteException { synchronized (mLock) { try { throwIfOpened(); if (DEBUG) { Log.i(LOG_TAG, "openDocument()"); } mRenderer = new PdfRenderer(source); return mRenderer.getPageCount(); } catch (IOException | IllegalStateException e) { IoUtils.closeQuietly(source); Log.e(LOG_TAG, "Cannot open file", e); return ERROR_MALFORMED_PDF_FILE; } catch (SecurityException e) { IoUtils.closeQuietly(source); Log.e(LOG_TAG, "Cannot open file", e); return ERROR_SECURE_PDF_FILE; } } } @Override public void renderPage(int pageIndex, int bitmapWidth, int bitmapHeight, PrintAttributes attributes, ParcelFileDescriptor destination) { synchronized (mLock) { try { throwIfNotOpened(); try (PdfRenderer.Page page = mRenderer.openPage(pageIndex)) { final int srcWidthPts = page.getWidth(); final int srcHeightPts = page.getHeight(); final int dstWidthPts = pointsFromMils( attributes.getMediaSize().getWidthMils()); final int dstHeightPts = pointsFromMils( attributes.getMediaSize().getHeightMils()); final boolean scaleContent = mRenderer.shouldScaleForPrinting(); final boolean contentLandscape = !attributes.getMediaSize().isPortrait(); final float displayScale; Matrix matrix = new Matrix(); if (scaleContent) { displayScale = Math.min((float) bitmapWidth / srcWidthPts, (float) bitmapHeight / srcHeightPts); } else { if (contentLandscape) { displayScale = (float) bitmapHeight / dstHeightPts; } else { displayScale = (float) bitmapWidth / dstWidthPts; } } matrix.postScale(displayScale, displayScale); Configuration configuration = PdfManipulationService.this.getResources() .getConfiguration(); if (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { matrix.postTranslate(bitmapWidth - srcWidthPts * displayScale, 0); } Margins minMargins = attributes.getMinMargins(); final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils()); final int paddingTopPts = pointsFromMils(minMargins.getTopMils()); final int paddingRightPts = pointsFromMils(minMargins.getRightMils()); final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils()); Rect clip = new Rect(); clip.left = (int) (paddingLeftPts * displayScale); clip.top = (int) (paddingTopPts * displayScale); clip.right = (int) (bitmapWidth - paddingRightPts * displayScale); clip.bottom = (int) (bitmapHeight - paddingBottomPts * displayScale); if (DEBUG) { Log.i(LOG_TAG, "Rendering page:" + pageIndex); } Bitmap bitmap = getBitmapForSize(bitmapWidth, bitmapHeight); page.render(bitmap, clip, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY); BitmapSerializeUtils.writeBitmapPixels(bitmap, destination); } } catch (Throwable e) { Log.e(LOG_TAG, "Cannot render page", e); // The error is propagated to the caller when it tries to read the bitmap and // the pipe is closed prematurely } finally { IoUtils.closeQuietly(destination); } } } @Override public void closeDocument() { synchronized (mLock) { throwIfNotOpened(); if (DEBUG) { Log.i(LOG_TAG, "closeDocument()"); } mRenderer.close(); mRenderer = null; } } private Bitmap getBitmapForSize(int width, int height) { if (mBitmap != null) { if (mBitmap.getWidth() == width && mBitmap.getHeight() == height) { mBitmap.eraseColor(Color.WHITE); return mBitmap; } mBitmap.recycle(); } mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mBitmap.eraseColor(Color.WHITE); return mBitmap; } private void throwIfOpened() { if (mRenderer != null) { throw new IllegalStateException("Already opened"); } } private void throwIfNotOpened() { if (mRenderer == null) { throw new IllegalStateException("Not opened"); } } } private final class PdfEditorImpl extends IPdfEditor.Stub { private final Object mLock = new Object(); private PdfEditor mEditor; @Override public int openDocument(ParcelFileDescriptor source) throws RemoteException { synchronized (mLock) { try { throwIfOpened(); if (DEBUG) { Log.i(LOG_TAG, "openDocument()"); } mEditor = new PdfEditor(source); return mEditor.getPageCount(); } catch (IOException | IllegalStateException e) { IoUtils.closeQuietly(source); Log.e(LOG_TAG, "Cannot open file", e); throw new RemoteException(e.toString()); } } } @Override public void removePages(PageRange[] ranges) { synchronized (mLock) { throwIfNotOpened(); if (DEBUG) { Log.i(LOG_TAG, "removePages()"); } ranges = PageRangeUtils.normalize(ranges); int lastPageIdx = mEditor.getPageCount() - 1; final int rangeCount = ranges.length; for (int i = rangeCount - 1; i >= 0; i--) { PageRange range = ranges[i]; // Ignore removal of pages that are outside the document if (range.getEnd() > lastPageIdx) { if (range.getStart() > lastPageIdx) { continue; } range = new PageRange(range.getStart(), lastPageIdx); } for (int j = range.getEnd(); j >= range.getStart(); j--) { mEditor.removePage(j); } } } } @Override public void applyPrintAttributes(PrintAttributes attributes) { synchronized (mLock) { throwIfNotOpened(); if (DEBUG) { Log.i(LOG_TAG, "applyPrintAttributes()"); } Rect mediaBox = new Rect(); Rect cropBox = new Rect(); Matrix transform = new Matrix(); final boolean layoutDirectionRtl = getResources().getConfiguration() .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; // We do not want to rotate the media box, so take into account orientation. final int dstWidthPts = pointsFromMils(attributes.getMediaSize().getWidthMils()); final int dstHeightPts = pointsFromMils(attributes.getMediaSize().getHeightMils()); final boolean scaleForPrinting = mEditor.shouldScaleForPrinting(); final int pageCount = mEditor.getPageCount(); for (int i = 0; i < pageCount; i++) { if (!mEditor.getPageMediaBox(i, mediaBox)) { Log.e(LOG_TAG, "Malformed PDF file"); return; } final int srcWidthPts = mediaBox.width(); final int srcHeightPts = mediaBox.height(); // Update the media box with the desired size. mediaBox.right = dstWidthPts; mediaBox.bottom = dstHeightPts; mEditor.setPageMediaBox(i, mediaBox); // Make sure content is top-left after media box resize. transform.setTranslate(0, srcHeightPts - dstHeightPts); // Scale the content if document allows it. final float scale; if (scaleForPrinting) { scale = Math.min((float) dstWidthPts / srcWidthPts, (float) dstHeightPts / srcHeightPts); transform.postScale(scale, scale); } else { scale = 1.0f; } // Update the crop box relatively to the media box change, if needed. if (mEditor.getPageCropBox(i, cropBox)) { cropBox.left = (int) (cropBox.left * scale + 0.5f); cropBox.top = (int) (cropBox.top * scale + 0.5f); cropBox.right = (int) (cropBox.right * scale + 0.5f); cropBox.bottom = (int) (cropBox.bottom * scale + 0.5f); cropBox.intersect(mediaBox); mEditor.setPageCropBox(i, cropBox); } // If in RTL mode put the content in the logical top-right corner. if (layoutDirectionRtl) { final float dx = dstWidthPts - (int) (srcWidthPts * scale + 0.5f); final float dy = 0; transform.postTranslate(dx, dy); } // Adjust the physical margins if needed. Margins minMargins = attributes.getMinMargins(); final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils()); final int paddingTopPts = pointsFromMils(minMargins.getTopMils()); final int paddingRightPts = pointsFromMils(minMargins.getRightMils()); final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils()); Rect clip = new Rect(mediaBox); clip.left += paddingLeftPts; clip.top += paddingTopPts; clip.right -= paddingRightPts; clip.bottom -= paddingBottomPts; // Apply the accumulated transforms. mEditor.setTransformAndClip(i, transform, clip); } } } @Override public void write(ParcelFileDescriptor destination) throws RemoteException { synchronized (mLock) { try { throwIfNotOpened(); if (DEBUG) { Log.i(LOG_TAG, "write()"); } mEditor.write(destination); } catch (IOException | IllegalStateException e) { IoUtils.closeQuietly(destination); Log.e(LOG_TAG, "Error writing PDF to file.", e); throw new RemoteException(e.toString()); } } } @Override public void closeDocument() { synchronized (mLock) { throwIfNotOpened(); if (DEBUG) { Log.i(LOG_TAG, "closeDocument()"); } mEditor.close(); mEditor = null; } } private void throwIfOpened() { if (mEditor != null) { throw new IllegalStateException("Already opened"); } } private void throwIfNotOpened() { if (mEditor == null) { throw new IllegalStateException("Not opened"); } } } private static int pointsFromMils(int mils) { return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH); } }