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