| /* |
| * 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); |
| } |
| } |