1 /*
2  * Copyright (C) 2016 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 
17 package android.graphics.pdf.cts;
18 
19 import static org.junit.Assert.fail;
20 
21 import android.content.Context;
22 import android.graphics.Bitmap;
23 import android.graphics.Canvas;
24 import android.graphics.Matrix;
25 import android.graphics.Rect;
26 import android.graphics.pdf.PdfRenderer;
27 import android.os.ParcelFileDescriptor;
28 import androidx.annotation.FloatRange;
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 import androidx.annotation.RawRes;
32 import android.util.ArrayMap;
33 import android.util.Log;
34 
35 import java.io.BufferedInputStream;
36 import java.io.BufferedOutputStream;
37 import java.io.File;
38 import java.io.FileOutputStream;
39 import java.io.IOException;
40 import java.io.InputStream;
41 import java.io.OutputStream;
42 import java.util.Arrays;
43 import java.util.Map;
44 
45 /**
46  * Utilities for this package
47  */
48 class Utils {
49     private static final String LOG_TAG = "Utils";
50 
51     private static Map<Integer, File> sFiles = new ArrayMap<>();
52     private static Map<Integer, Bitmap> sRenderedBitmaps = new ArrayMap<>();
53 
54     static final int A4_WIDTH_PTS = 595;
55     static final int A4_HEIGHT_PTS = 841;
56     static final int A4_PORTRAIT = android.graphics.pdf.cts.R.raw.a4_portrait_rgbb;
57     static final int A5_PORTRAIT = android.graphics.pdf.cts.R.raw.a5_portrait_rgbb;
58 
59     /**
60      * Create a {@link PdfRenderer} pointing to a file copied from a resource.
61      *
62      * @param docRes  The resource to load
63      * @param context The context to use for creating the renderer
64      *
65      * @return the renderer
66      *
67      * @throws IOException If anything went wrong
68      */
createRenderer(@awRes int docRes, @NonNull Context context)69     static @NonNull PdfRenderer createRenderer(@RawRes int docRes, @NonNull Context context)
70             throws IOException {
71         File pdfFile = sFiles.get(docRes);
72 
73         if (pdfFile == null) {
74             pdfFile = File.createTempFile("pdf", null, context.getCacheDir());
75 
76             // Copy resource to file so that we can open it as a ParcelFileDescriptor
77             try (OutputStream os = new BufferedOutputStream(new FileOutputStream(pdfFile))) {
78                 try (InputStream is = new BufferedInputStream(
79                         context.getResources().openRawResource(docRes))) {
80                     byte buffer[] = new byte[1024];
81 
82                     while (true) {
83                         int numRead = is.read(buffer, 0, buffer.length);
84 
85                         if (numRead == -1) {
86                             break;
87                         }
88 
89                         os.write(Arrays.copyOf(buffer, numRead));
90                     }
91 
92                     os.flush();
93                 }
94             }
95 
96             sFiles.put(docRes, pdfFile);
97         }
98 
99         return new PdfRenderer(
100                 ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY));
101     }
102 
103     /**
104      * Render a pdf onto a bitmap <u>while</u> applying the transformation <u>in the</u>
105      * PDFRenderer. Hence use PdfRenderer.*'s translation and clipping methods.
106      *
107      * @param bmWidth        The width of the destination bitmap
108      * @param bmHeight       The height of the destination bitmap
109      * @param docRes         The resolution of the doc
110      * @param clipping       The clipping for the PDF document
111      * @param transformation The transformation of the PDF
112      * @param renderMode     The render mode to use to render the PDF
113      * @param context        The context to use for creating the renderer
114      *
115      * @return The rendered bitmap
116      */
renderWithTransform(int bmWidth, int bmHeight, @RawRes int docRes, @Nullable Rect clipping, @Nullable Matrix transformation, int renderMode, @NonNull Context context)117     static @NonNull Bitmap renderWithTransform(int bmWidth, int bmHeight, @RawRes int docRes,
118             @Nullable Rect clipping, @Nullable Matrix transformation, int renderMode,
119             @NonNull Context context)
120             throws IOException {
121         try (PdfRenderer renderer = createRenderer(docRes, context)) {
122             try (PdfRenderer.Page page = renderer.openPage(0)) {
123                 Bitmap bm = Bitmap.createBitmap(bmWidth, bmHeight, Bitmap.Config.ARGB_8888);
124 
125                 page.render(bm, clipping, transformation, renderMode);
126 
127                 return bm;
128             }
129         }
130     }
131 
132     /**
133      * Render a pdf onto a bitmap <u>and then</u> apply then render the resulting bitmap onto
134      * another bitmap while applying the transformation. Hence use canvas' translation and clipping
135      * methods.
136      *
137      * @param bmWidth        The width of the destination bitmap
138      * @param bmHeight       The height of the destination bitmap
139      * @param docRes         The resolution of the doc
140      * @param clipping       The clipping for the PDF document
141      * @param transformation The transformation of the PDF
142      * @param renderMode     The render mode to use to render the PDF
143      * @param context        The context to use for creating the renderer
144      *
145      * @return The rendered bitmap
146      */
renderAndThenTransform(int bmWidth, int bmHeight, @RawRes int docRes, @Nullable Rect clipping, @Nullable Matrix transformation, int renderMode, @NonNull Context context)147     private static @NonNull Bitmap renderAndThenTransform(int bmWidth, int bmHeight,
148             @RawRes int docRes, @Nullable Rect clipping, @Nullable Matrix transformation,
149             int renderMode, @NonNull Context context) throws IOException {
150         Bitmap renderedBm;
151 
152         renderedBm = sRenderedBitmaps.get(docRes);
153 
154         if (renderedBm == null) {
155             try (PdfRenderer renderer = Utils.createRenderer(docRes, context)) {
156                 try (PdfRenderer.Page page = renderer.openPage(0)) {
157                     renderedBm = Bitmap.createBitmap(page.getWidth(), page.getHeight(),
158                             Bitmap.Config.ARGB_8888);
159                     page.render(renderedBm, null, null, renderMode);
160                 }
161             }
162             sRenderedBitmaps.put(docRes, renderedBm);
163         }
164 
165         if (transformation == null) {
166             // According to PdfRenderer.page#render transformation == null means that the bitmap
167             // should be stretched to clipping (if provided) or otherwise destination size
168             transformation = new Matrix();
169 
170             if (clipping != null) {
171                 transformation.postScale((float) clipping.width() / renderedBm.getWidth(),
172                         (float) clipping.height() / renderedBm.getHeight());
173                 transformation.postTranslate(clipping.left, clipping.top);
174             } else {
175                 transformation.postScale((float) bmWidth / renderedBm.getWidth(),
176                         (float) bmHeight / renderedBm.getHeight());
177             }
178         }
179 
180         Bitmap transformedBm = Bitmap.createBitmap(bmWidth, bmHeight, Bitmap.Config.ARGB_8888);
181         Canvas canvas = new Canvas(transformedBm);
182         canvas.drawBitmap(renderedBm, transformation, null);
183 
184         Bitmap clippedBm;
185         if (clipping != null) {
186             clippedBm = Bitmap.createBitmap(bmWidth, bmHeight, Bitmap.Config.ARGB_8888);
187             canvas = new Canvas(clippedBm);
188             canvas.drawBitmap(transformedBm, clipping, clipping, null);
189             transformedBm.recycle();
190         } else {
191             clippedBm = transformedBm;
192         }
193 
194         return clippedBm;
195     }
196 
197     /**
198      * Get the fraction of non-matching pixels of two bitmaps. 1 == no pixels match, 0 == all pixels
199      * match.
200      *
201      * @param a The first bitmap
202      * @param b The second bitmap
203      *
204      * @return The fraction of non-matching pixels.
205      */
getNonMatching(@onNull Bitmap a, @NonNull Bitmap b)206     private static @FloatRange(from = 0, to = 1) float getNonMatching(@NonNull Bitmap a,
207             @NonNull Bitmap b) {
208         if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight()) {
209             return 1;
210         }
211 
212         int[] aPx = new int[a.getWidth() * a.getHeight()];
213         int[] bPx = new int[b.getWidth() * b.getHeight()];
214         a.getPixels(aPx, 0, a.getWidth(), 0, 0, a.getWidth(), a.getHeight());
215         b.getPixels(bPx, 0, b.getWidth(), 0, 0, b.getWidth(), b.getHeight());
216 
217         int badPixels = 0;
218         int totalPixels = a.getWidth() * a.getHeight();
219         for (int i = 0; i < totalPixels; i++) {
220             if (aPx[i] != bPx[i]) {
221                 badPixels++;
222             }
223         }
224 
225         return ((float) badPixels) / totalPixels;
226     }
227 
228     /**
229      * Render the PDF two times. Once with applying the transformation and clipping in the {@link
230      * PdfRenderer}. The other time render the PDF onto a bitmap and then clip and transform that
231      * image. The result should be the same beside some minor aliasing.
232      *
233      * @param width          The width of the resulting bitmap
234      * @param height         The height of the resulting bitmap
235      * @param docRes         The resource of the PDF document
236      * @param clipping       The clipping to apply
237      * @param transformation The transformation to apply
238      * @param renderMode     The render mode to use
239      * @param context        The context to use for creating the renderer
240      *
241      * @throws IOException
242      */
renderAndCompare(int width, int height, @RawRes int docRes, @Nullable Rect clipping, @Nullable Matrix transformation, int renderMode, @NonNull Context context)243     static void renderAndCompare(int width, int height, @RawRes int docRes,
244             @Nullable Rect clipping, @Nullable Matrix transformation, int renderMode,
245             @NonNull Context context) throws IOException {
246         Bitmap a = renderWithTransform(width, height, docRes, clipping, transformation,
247                 renderMode, context);
248         Bitmap b = renderAndThenTransform(width, height, docRes, clipping, transformation,
249                 renderMode, context);
250 
251         try {
252             // We allow 1% aliasing error
253             float nonMatching = getNonMatching(a, b);
254 
255             if (nonMatching == 0) {
256                 Log.d(LOG_TAG, "bitmaps match");
257             } else if (nonMatching > 0.01) {
258                 fail("Testing width:" + width + ", height:" + height + ", docRes:" + docRes +
259                         ", clipping:" + clipping + ", transform:" + transformation + ". Bitmaps " +
260                         "differ by " + Math.ceil(nonMatching * 10000) / 100 +
261                         "%. That is too much.");
262             } else {
263                 Log.d(LOG_TAG, "bitmaps differ by " + Math.ceil(nonMatching * 10000) / 100 + "%");
264             }
265         } finally {
266             a.recycle();
267             b.recycle();
268         }
269     }
270 
271     /**
272      * Run a runnable and expect an exception of a certain type.
273      *
274      * @param r             The {@link Invokable} to run
275      * @param expectedClass The expected exception type
276      */
verifyException(@onNull Invokable r, @NonNull Class<? extends Exception> expectedClass)277     static void verifyException(@NonNull Invokable r,
278             @NonNull Class<? extends Exception> expectedClass) {
279         try {
280             r.run();
281         } catch (Exception e) {
282             if (e.getClass().isAssignableFrom(expectedClass)) {
283                 return;
284             } else {
285                 Log.e(LOG_TAG, "Incorrect exception", e);
286                 fail("Expected: " + expectedClass.getName() + ", got: " + e.getClass().getName());
287             }
288         }
289 
290         fail("Expected to have " + expectedClass.getName() + " exception thrown");
291     }
292 
293     /**
294      * A runnable that can throw an exception.
295      */
296     interface Invokable {
run()297         void run() throws Exception;
298     }
299 }
300