blob: af4c34795917d2d7ded6886e8eba5f025615afbb [file] [log] [blame]
Svet Ganov13f542ca2014-08-29 15:35:49 -07001/*
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;
Svetoslav6f249832014-09-02 21:12:04 -070023import android.graphics.Color;
Svet Ganov13f542ca2014-08-29 15:35:49 -070024import android.graphics.Matrix;
25import android.graphics.Rect;
Svetoslav62ce3322014-09-04 21:17:17 -070026import android.graphics.pdf.PdfEditor;
Svet Ganov13f542ca2014-08-29 15:35:49 -070027import 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;
Svetoslav62ce3322014-09-04 21:17:17 -070036import com.android.printspooler.util.PageRangeUtils;
Svet Ganov13f542ca2014-08-29 15:35:49 -070037import libcore.io.IoUtils;
Svet Ganovdf644492014-08-29 15:35:49 -070038import com.android.printspooler.util.BitmapSerializeUtils;
Svet Ganov13f542ca2014-08-29 15:35:49 -070039import java.io.IOException;
40
41/**
Svetoslav62ce3322014-09-04 21:17:17 -070042 * Service for manipulation of PDF documents in an isolated process.
Svet Ganov13f542ca2014-08-29 15:35:49 -070043 */
Svetoslav62ce3322014-09-04 21:17:17 -070044public 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
Svet Ganovfce84f02014-10-31 16:56:52 -070050 public static final int ERROR_MALFORMED_PDF_FILE = -2;
51
52 public static final int ERROR_SECURE_PDF_FILE = -3;
Svetoslavd23bfa92014-09-24 13:19:49 -070053
Svetoslav62ce3322014-09-04 21:17:17 -070054 private static final String LOG_TAG = "PdfManipulationService";
Svet Ganov13f542ca2014-08-29 15:35:49 -070055 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) {
Svetoslav62ce3322014-09-04 21:17:17 -070062 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 }
Svet Ganov13f542ca2014-08-29 15:35:49 -070074 }
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) {
Svet Ganov13f542ca2014-08-29 15:35:49 -070085 try {
Svetoslav62ce3322014-09-04 21:17:17 -070086 throwIfOpened();
87 if (DEBUG) {
88 Log.i(LOG_TAG, "openDocument()");
89 }
Svet Ganov13f542ca2014-08-29 15:35:49 -070090 mRenderer = new PdfRenderer(source);
91 return mRenderer.getPageCount();
Svetoslavbec22be2014-09-25 13:03:20 -070092 } catch (IOException | IllegalStateException e) {
Svetoslav62ce3322014-09-04 21:17:17 -070093 IoUtils.closeQuietly(source);
94 Log.e(LOG_TAG, "Cannot open file", e);
Svet Ganovfce84f02014-10-31 16:56:52 -070095 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;
Svet Ganov13f542ca2014-08-29 15:35:49 -0700100 }
101 }
102 }
103
104 @Override
105 public void renderPage(int pageIndex, int bitmapWidth, int bitmapHeight,
106 PrintAttributes attributes, ParcelFileDescriptor destination) {
Svet Ganov13f542ca2014-08-29 15:35:49 -0700107 synchronized (mLock) {
108 try {
109 throwIfNotOpened();
110
Philip P. Moltmann066bf812016-03-09 16:54:52 -0800111 try (PdfRenderer.Page page = mRenderer.openPage(pageIndex)) {
112 final int srcWidthPts = page.getWidth();
113 final int srcHeightPts = page.getHeight();
Svet Ganov13f542ca2014-08-29 15:35:49 -0700114
Philip P. Moltmann066bf812016-03-09 16:54:52 -0800115 final int dstWidthPts = pointsFromMils(
116 attributes.getMediaSize().getWidthMils());
117 final int dstHeightPts = pointsFromMils(
118 attributes.getMediaSize().getHeightMils());
Svet Ganov13f542ca2014-08-29 15:35:49 -0700119
Philip P. Moltmann066bf812016-03-09 16:54:52 -0800120 final boolean scaleContent = mRenderer.shouldScaleForPrinting();
121 final boolean contentLandscape = !attributes.getMediaSize().isPortrait();
Svet Ganov13f542ca2014-08-29 15:35:49 -0700122
Philip P. Moltmann066bf812016-03-09 16:54:52 -0800123 final float displayScale;
124 Matrix matrix = new Matrix();
Svet Ganov13f542ca2014-08-29 15:35:49 -0700125
Philip P. Moltmann066bf812016-03-09 16:54:52 -0800126 if (scaleContent) {
127 displayScale = Math.min((float) bitmapWidth / srcWidthPts,
128 (float) bitmapHeight / srcHeightPts);
Svet Ganov13f542ca2014-08-29 15:35:49 -0700129 } else {
Philip P. Moltmann066bf812016-03-09 16:54:52 -0800130 if (contentLandscape) {
131 displayScale = (float) bitmapHeight / dstHeightPts;
132 } else {
133 displayScale = (float) bitmapWidth / dstWidthPts;
134 }
Svet Ganov13f542ca2014-08-29 15:35:49 -0700135 }
Philip P. Moltmann066bf812016-03-09 16:54:52 -0800136 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);
Svet Ganov13f542ca2014-08-29 15:35:49 -0700164 }
Philip P. Moltmann066bf812016-03-09 16:54:52 -0800165 } catch (Throwable e) {
166 Log.e(LOG_TAG, "Cannot render page", e);
Svet Ganov13f542ca2014-08-29 15:35:49 -0700167
Philip P. Moltmann066bf812016-03-09 16:54:52 -0800168 // The error is propagated to the caller when it tries to read the bitmap and
169 // the pipe is closed prematurely
Svet Ganov13f542ca2014-08-29 15:35:49 -0700170 } finally {
Svet Ganov13f542ca2014-08-29 15:35:49 -0700171 IoUtils.closeQuietly(destination);
172 }
173 }
174 }
175
176 @Override
177 public void closeDocument() {
178 synchronized (mLock) {
179 throwIfNotOpened();
180 if (DEBUG) {
Svetoslav62ce3322014-09-04 21:17:17 -0700181 Log.i(LOG_TAG, "closeDocument()");
Svet Ganov13f542ca2014-08-29 15:35:49 -0700182 }
183 mRenderer.close();
184 mRenderer = null;
185 }
186 }
187
Svet Ganov13f542ca2014-08-29 15:35:49 -0700188 private Bitmap getBitmapForSize(int width, int height) {
189 if (mBitmap != null) {
190 if (mBitmap.getWidth() == width && mBitmap.getHeight() == height) {
Svetoslav6f249832014-09-02 21:12:04 -0700191 mBitmap.eraseColor(Color.WHITE);
Svet Ganov13f542ca2014-08-29 15:35:49 -0700192 return mBitmap;
193 }
194 mBitmap.recycle();
195 }
196 mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Svetoslav6f249832014-09-02 21:12:04 -0700197 mBitmap.eraseColor(Color.WHITE);
Svet Ganov13f542ca2014-08-29 15:35:49 -0700198 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
Svetoslav62ce3322014-09-04 21:17:17 -0700214 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();
Svetoslavbec22be2014-09-25 13:03:20 -0700229 } catch (IOException | IllegalStateException e) {
Svetoslav62ce3322014-09-04 21:17:17 -0700230 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 final int rangeCount = ranges.length;
248 for (int i = rangeCount - 1; i >= 0; i--) {
249 PageRange range = ranges[i];
250 for (int j = range.getEnd(); j >= range.getStart(); j--) {
251 mEditor.removePage(j);
252 }
253 }
254 }
255 }
256
257 @Override
Svetoslavbec22be2014-09-25 13:03:20 -0700258 public void applyPrintAttributes(PrintAttributes attributes) {
259 synchronized (mLock) {
260 throwIfNotOpened();
261 if (DEBUG) {
262 Log.i(LOG_TAG, "applyPrintAttributes()");
263 }
264
265 Rect mediaBox = new Rect();
266 Rect cropBox = new Rect();
267 Matrix transform = new Matrix();
268
269 final boolean contentPortrait = attributes.getMediaSize().isPortrait();
270
271 final boolean layoutDirectionRtl = getResources().getConfiguration()
272 .getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
273
274 // We do not want to rotate the media box, so take into account orientation.
275 final int dstWidthPts = contentPortrait
276 ? pointsFromMils(attributes.getMediaSize().getWidthMils())
277 : pointsFromMils(attributes.getMediaSize().getHeightMils());
278 final int dstHeightPts = contentPortrait
279 ? pointsFromMils(attributes.getMediaSize().getHeightMils())
280 : pointsFromMils(attributes.getMediaSize().getWidthMils());
281
282 final boolean scaleForPrinting = mEditor.shouldScaleForPrinting();
283
284 final int pageCount = mEditor.getPageCount();
285 for (int i = 0; i < pageCount; i++) {
286 if (!mEditor.getPageMediaBox(i, mediaBox)) {
287 Log.e(LOG_TAG, "Malformed PDF file");
288 return;
289 }
290
291 final int srcWidthPts = mediaBox.width();
292 final int srcHeightPts = mediaBox.height();
293
294 // Update the media box with the desired size.
295 mediaBox.right = dstWidthPts;
296 mediaBox.bottom = dstHeightPts;
297 mEditor.setPageMediaBox(i, mediaBox);
298
299 // Make sure content is top-left after media box resize.
300 transform.setTranslate(0, srcHeightPts - dstHeightPts);
301
302 // Rotate the content if in landscape.
303 if (!contentPortrait) {
304 transform.postRotate(270);
305 transform.postTranslate(0, dstHeightPts);
306 }
307
308 // Scale the content if document allows it.
309 final float scale;
310 if (scaleForPrinting) {
311 if (contentPortrait) {
312 scale = Math.min((float) dstWidthPts / srcWidthPts,
313 (float) dstHeightPts / srcHeightPts);
314 transform.postScale(scale, scale);
315 } else {
316 scale = Math.min((float) dstWidthPts / srcHeightPts,
317 (float) dstHeightPts / srcWidthPts);
318 transform.postScale(scale, scale, mediaBox.left, mediaBox.bottom);
319 }
320 } else {
321 scale = 1.0f;
322 }
323
324 // Update the crop box relatively to the media box change, if needed.
325 if (mEditor.getPageCropBox(i, cropBox)) {
326 cropBox.left = (int) (cropBox.left * scale + 0.5f);
327 cropBox.top = (int) (cropBox.top * scale + 0.5f);
328 cropBox.right = (int) (cropBox.right * scale + 0.5f);
329 cropBox.bottom = (int) (cropBox.bottom * scale + 0.5f);
330 cropBox.intersect(mediaBox);
331 mEditor.setPageCropBox(i, cropBox);
332 }
333
334 // If in RTL mode put the content in the logical top-right corner.
335 if (layoutDirectionRtl) {
336 final float dx = contentPortrait
337 ? dstWidthPts - (int) (srcWidthPts * scale + 0.5f) : 0;
338 final float dy = contentPortrait
339 ? 0 : - (dstHeightPts - (int) (srcWidthPts * scale + 0.5f));
340 transform.postTranslate(dx, dy);
341 }
342
343 // Adjust the physical margins if needed.
344 Margins minMargins = attributes.getMinMargins();
345 final int paddingLeftPts = pointsFromMils(minMargins.getLeftMils());
346 final int paddingTopPts = pointsFromMils(minMargins.getTopMils());
347 final int paddingRightPts = pointsFromMils(minMargins.getRightMils());
348 final int paddingBottomPts = pointsFromMils(minMargins.getBottomMils());
349
350 Rect clip = new Rect(mediaBox);
351 clip.left += paddingLeftPts;
352 clip.top += paddingTopPts;
353 clip.right -= paddingRightPts;
354 clip.bottom -= paddingBottomPts;
355
356 // Apply the accumulated transforms.
357 mEditor.setTransformAndClip(i, transform, clip);
358 }
359 }
360 }
361
362 @Override
Svetoslav62ce3322014-09-04 21:17:17 -0700363 public void write(ParcelFileDescriptor destination) throws RemoteException {
364 synchronized (mLock) {
365 try {
366 throwIfNotOpened();
367 if (DEBUG) {
368 Log.i(LOG_TAG, "write()");
369 }
370 mEditor.write(destination);
371 } catch (IOException | IllegalStateException e) {
372 IoUtils.closeQuietly(destination);
373 Log.e(LOG_TAG, "Error writing PDF to file.", e);
374 throw new RemoteException(e.toString());
375 }
376 }
377 }
378
379 @Override
380 public void closeDocument() {
381 synchronized (mLock) {
382 throwIfNotOpened();
383 if (DEBUG) {
384 Log.i(LOG_TAG, "closeDocument()");
385 }
386 mEditor.close();
387 mEditor = null;
388 }
389 }
390
391 private void throwIfOpened() {
392 if (mEditor != null) {
393 throw new IllegalStateException("Already opened");
394 }
395 }
396
397 private void throwIfNotOpened() {
398 if (mEditor == null) {
399 throw new IllegalStateException("Not opened");
400 }
401 }
402 }
403
Svet Ganov13f542ca2014-08-29 15:35:49 -0700404 private static int pointsFromMils(int mils) {
405 return (int) (((float) mils / MILS_PER_INCH) * POINTS_IN_INCH);
406 }
407}