1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.printspooler.renderer;
18
19import android.app.Service;
20import android.content.Intent;
21import android.content.res.Configuration;
22import android.graphics.Bitmap;
23import android.graphics.Color;
24import android.graphics.Matrix;
25import android.graphics.Rect;
26import android.graphics.pdf.PdfEditor;
27import android.graphics.pdf.PdfRenderer;
28import android.os.IBinder;
29import android.os.ParcelFileDescriptor;
30import android.os.RemoteException;
31import android.print.PageRange;
32import android.print.PrintAttributes;
33import android.print.PrintAttributes.Margins;
34import android.util.Log;
35import android.view.View;
36import com.android.printspooler.util.PageRangeUtils;
37import libcore.io.IoUtils;
38import com.android.printspooler.util.BitmapSerializeUtils;
39import java.io.IOException;
40
41/**
42 * Service for manipulation of PDF documents in an isolated process.
43 */
44public final class PdfManipulationService extends Service {
45    public static final String ACTION_GET_RENDERER =
46            "com.android.printspooler.renderer.ACTION_GET_RENDERER";
47    public static final String ACTION_GET_EDITOR =
48            "com.android.printspooler.renderer.ACTION_GET_EDITOR";
49
50    public static final int ERROR_MALFORMED_PDF_FILE = -2;
51
52    public static final int ERROR_SECURE_PDF_FILE = -3;
53
54    private static final String LOG_TAG = "PdfManipulationService";
55    private static final boolean DEBUG = false;
56
57    private static final int MILS_PER_INCH = 1000;
58    private static final int POINTS_IN_INCH = 72;
59
60    @Override
61    public IBinder onBind(Intent intent) {
62        String action = intent.getAction();
63        switch (action) {
64            case ACTION_GET_RENDERER: {
65                return new PdfRendererImpl();
66            }
67            case ACTION_GET_EDITOR: {
68                return new PdfEditorImpl();
69            }
70            default: {
71                throw new IllegalArgumentException("Invalid intent action:" + action);
72            }
73        }
74    }
75
76    private final class PdfRendererImpl extends IPdfRenderer.Stub {
77        private final Object mLock = new Object();
78
79        private Bitmap mBitmap;
80        private PdfRenderer mRenderer;
81
82        @Override
83        public int openDocument(ParcelFileDescriptor source) throws RemoteException {
84            synchronized (mLock) {
85                try {
86                    throwIfOpened();
87                    if (DEBUG) {
88                        Log.i(LOG_TAG, "openDocument()");
89                    }
90                    mRenderer = new PdfRenderer(source);
91                    return mRenderer.getPageCount();
92                } catch (IOException | IllegalStateException e) {
93                    IoUtils.closeQuietly(source);
94                    Log.e(LOG_TAG, "Cannot open file", e);
95                    return ERROR_MALFORMED_PDF_FILE;
96                } catch (SecurityException e) {
97                    IoUtils.closeQuietly(source);
98                    Log.e(LOG_TAG, "Cannot open file", e);
99                    return ERROR_SECURE_PDF_FILE;
100                }
101            }
102        }
103
104        @Override
105        public void renderPage(int pageIndex, int bitmapWidth, int bitmapHeight,
106                PrintAttributes attributes, ParcelFileDescriptor destination) {
107            synchronized (mLock) {
108                try {
109                    throwIfNotOpened();
110
111                    try (PdfRenderer.Page page = mRenderer.openPage(pageIndex)) {
112                        final int srcWidthPts = page.getWidth();
113                        final int srcHeightPts = page.getHeight();
114
115                        final int dstWidthPts = pointsFromMils(
116                                attributes.getMediaSize().getWidthMils());
117                        final int dstHeightPts = pointsFromMils(
118                                attributes.getMediaSize().getHeightMils());
119
120                        final boolean scaleContent = mRenderer.shouldScaleForPrinting();
121                        final boolean contentLandscape = !attributes.getMediaSize().isPortrait();
122
123                        final float displayScale;
124                        Matrix matrix = new Matrix();
125
126                        if (scaleContent) {
127                            displayScale = Math.min((float) bitmapWidth / srcWidthPts,
128                                    (float) bitmapHeight / srcHeightPts);
129                        } else {
130                            if (contentLandscape) {
131                                displayScale = (float) bitmapHeight / dstHeightPts;
132                            } else {
133                                displayScale = (float) bitmapWidth / dstWidthPts;
134                            }
135                        }
136                        matrix.postScale(displayScale, displayScale);
137
138                        Configuration configuration = PdfManipulationService.this.getResources()
139                                .getConfiguration();
140                        if (configuration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
141                            matrix.postTranslate(bitmapWidth - srcWidthPts * displayScale, 0);
142                        }
143
144                        Margins minMargins = attributes.getMinMargins();
145                        final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils());
146                        final int paddingTopPts = pointsFromMils(minMargins.getTopMils());
147                        final int paddingRightPts = pointsFromMils(minMargins.getRightMils());
148                        final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils());
149
150                        Rect clip = new Rect();
151                        clip.left = (int) (paddingLeftPts * displayScale);
152                        clip.top = (int) (paddingTopPts * displayScale);
153                        clip.right = (int) (bitmapWidth - paddingRightPts * displayScale);
154                        clip.bottom = (int) (bitmapHeight - paddingBottomPts * displayScale);
155
156                        if (DEBUG) {
157                            Log.i(LOG_TAG, "Rendering page:" + pageIndex);
158                        }
159
160                        Bitmap bitmap = getBitmapForSize(bitmapWidth, bitmapHeight);
161                        page.render(bitmap, clip, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
162
163                        BitmapSerializeUtils.writeBitmapPixels(bitmap, destination);
164                    }
165                } catch (Throwable e) {
166                    Log.e(LOG_TAG, "Cannot render page", e);
167
168                    // The error is propagated to the caller when it tries to read the bitmap and
169                    // the pipe is closed prematurely
170                } finally {
171                    IoUtils.closeQuietly(destination);
172                }
173            }
174        }
175
176        @Override
177        public void closeDocument() {
178            synchronized (mLock) {
179                throwIfNotOpened();
180                if (DEBUG) {
181                    Log.i(LOG_TAG, "closeDocument()");
182                }
183                mRenderer.close();
184                mRenderer = null;
185            }
186        }
187
188        private Bitmap getBitmapForSize(int width, int height) {
189            if (mBitmap != null) {
190                if (mBitmap.getWidth() == width && mBitmap.getHeight() == height) {
191                    mBitmap.eraseColor(Color.WHITE);
192                    return mBitmap;
193                }
194                mBitmap.recycle();
195            }
196            mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
197            mBitmap.eraseColor(Color.WHITE);
198            return mBitmap;
199        }
200
201        private void throwIfOpened() {
202            if (mRenderer != null) {
203                throw new IllegalStateException("Already opened");
204            }
205        }
206
207        private void throwIfNotOpened() {
208            if (mRenderer == null) {
209                throw new IllegalStateException("Not opened");
210            }
211        }
212    }
213
214    private final class PdfEditorImpl extends IPdfEditor.Stub {
215        private final Object mLock = new Object();
216
217        private PdfEditor mEditor;
218
219        @Override
220        public int openDocument(ParcelFileDescriptor source) throws RemoteException {
221            synchronized (mLock) {
222                try {
223                    throwIfOpened();
224                    if (DEBUG) {
225                        Log.i(LOG_TAG, "openDocument()");
226                    }
227                    mEditor = new PdfEditor(source);
228                    return mEditor.getPageCount();
229                } catch (IOException | IllegalStateException e) {
230                    IoUtils.closeQuietly(source);
231                    Log.e(LOG_TAG, "Cannot open file", e);
232                    throw new RemoteException(e.toString());
233                }
234            }
235        }
236
237        @Override
238        public void removePages(PageRange[] ranges) {
239            synchronized (mLock) {
240                throwIfNotOpened();
241                if (DEBUG) {
242                    Log.i(LOG_TAG, "removePages()");
243                }
244
245                ranges = PageRangeUtils.normalize(ranges);
246
247                int lastPageIdx = mEditor.getPageCount() - 1;
248
249                final int rangeCount = ranges.length;
250                for (int i = rangeCount - 1; i >= 0; i--) {
251                    PageRange range = ranges[i];
252
253                    // Ignore removal of pages that are outside the document
254                    if (range.getEnd() > lastPageIdx) {
255                        if (range.getStart() > lastPageIdx) {
256                            continue;
257                        }
258                        range = new PageRange(range.getStart(), lastPageIdx);
259                    }
260
261                    for (int j = range.getEnd(); j >= range.getStart(); j--) {
262                        mEditor.removePage(j);
263                    }
264                }
265            }
266        }
267
268        @Override
269        public void applyPrintAttributes(PrintAttributes attributes) {
270            synchronized (mLock) {
271                throwIfNotOpened();
272                if (DEBUG) {
273                    Log.i(LOG_TAG, "applyPrintAttributes()");
274                }
275
276                Rect mediaBox = new Rect();
277                Rect cropBox = new Rect();
278                Matrix transform = new Matrix();
279
280                final boolean layoutDirectionRtl = getResources().getConfiguration()
281                        .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
282
283                // We do not want to rotate the media box, so take into account orientation.
284                final int dstWidthPts = pointsFromMils(attributes.getMediaSize().getWidthMils());
285                final int dstHeightPts = pointsFromMils(attributes.getMediaSize().getHeightMils());
286
287                final boolean scaleForPrinting = mEditor.shouldScaleForPrinting();
288
289                final int pageCount = mEditor.getPageCount();
290                for (int i = 0; i < pageCount; i++) {
291                    if (!mEditor.getPageMediaBox(i, mediaBox)) {
292                        Log.e(LOG_TAG, "Malformed PDF file");
293                        return;
294                    }
295
296                    final int srcWidthPts = mediaBox.width();
297                    final int srcHeightPts = mediaBox.height();
298
299                    // Update the media box with the desired size.
300                    mediaBox.right = dstWidthPts;
301                    mediaBox.bottom = dstHeightPts;
302                    mEditor.setPageMediaBox(i, mediaBox);
303
304                    // Make sure content is top-left after media box resize.
305                    transform.setTranslate(0, srcHeightPts - dstHeightPts);
306
307                    // Scale the content if document allows it.
308                    final float scale;
309                    if (scaleForPrinting) {
310                        scale = Math.min((float) dstWidthPts / srcWidthPts,
311                                (float) dstHeightPts / srcHeightPts);
312                        transform.postScale(scale, scale);
313                    } else {
314                        scale = 1.0f;
315                    }
316
317                    // Update the crop box relatively to the media box change, if needed.
318                    if (mEditor.getPageCropBox(i, cropBox)) {
319                        cropBox.left = (int) (cropBox.left * scale + 0.5f);
320                        cropBox.top = (int) (cropBox.top * scale + 0.5f);
321                        cropBox.right = (int) (cropBox.right * scale + 0.5f);
322                        cropBox.bottom = (int) (cropBox.bottom * scale + 0.5f);
323                        cropBox.intersect(mediaBox);
324                        mEditor.setPageCropBox(i, cropBox);
325                    }
326
327                    // If in RTL mode put the content in the logical top-right corner.
328                    if (layoutDirectionRtl) {
329                        final float dx = dstWidthPts - (int) (srcWidthPts * scale + 0.5f);
330                        final float dy = 0;
331                        transform.postTranslate(dx, dy);
332                    }
333
334                    // Adjust the physical margins if needed.
335                    Margins minMargins = attributes.getMinMargins();
336                    final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils());
337                    final int paddingTopPts = pointsFromMils(minMargins.getTopMils());
338                    final int paddingRightPts = pointsFromMils(minMargins.getRightMils());
339                    final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils());
340
341                    Rect clip = new Rect(mediaBox);
342                    clip.left += paddingLeftPts;
343                    clip.top += paddingTopPts;
344                    clip.right -= paddingRightPts;
345                    clip.bottom -= paddingBottomPts;
346
347                    // Apply the accumulated transforms.
348                    mEditor.setTransformAndClip(i, transform, clip);
349                }
350            }
351        }
352
353        @Override
354        public void write(ParcelFileDescriptor destination) throws RemoteException {
355            synchronized (mLock) {
356                try {
357                    throwIfNotOpened();
358                    if (DEBUG) {
359                        Log.i(LOG_TAG, "write()");
360                    }
361                    mEditor.write(destination);
362                } catch (IOException | IllegalStateException e) {
363                    IoUtils.closeQuietly(destination);
364                    Log.e(LOG_TAG, "Error writing PDF to file.", e);
365                    throw new RemoteException(e.toString());
366                }
367            }
368        }
369
370        @Override
371        public void closeDocument() {
372            synchronized (mLock) {
373                throwIfNotOpened();
374                if (DEBUG) {
375                    Log.i(LOG_TAG, "closeDocument()");
376                }
377                mEditor.close();
378                mEditor = null;
379            }
380        }
381
382        private void throwIfOpened() {
383            if (mEditor != null) {
384                throw new IllegalStateException("Already opened");
385            }
386        }
387
388        private void throwIfNotOpened() {
389            if (mEditor == null) {
390                throw new IllegalStateException("Not opened");
391            }
392        }
393    }
394
395    private static int pointsFromMils(int mils) {
396        return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH);
397    }
398}
399