blob: 4d43e09f2c6efbdabb8e9e939dccb0d95f547d3e [file] [log] [blame]
/*
* Copyright (C) 2016 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 android.graphics.pdf.cts;
import static org.junit.Assert.fail;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.pdf.PdfRenderer;
import android.os.ParcelFileDescriptor;
import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RawRes;
import android.util.ArrayMap;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Map;
/**
* Utilities for this package
*/
class Utils {
private static final String LOG_TAG = "Utils";
private static Map<Integer, File> sFiles = new ArrayMap<>();
private static Map<Integer, Bitmap> sRenderedBitmaps = new ArrayMap<>();
static final int A4_WIDTH_PTS = 595;
static final int A4_HEIGHT_PTS = 841;
static final int A4_PORTRAIT = android.graphics.pdf.cts.R.raw.a4_portrait_rgbb;
static final int A5_PORTRAIT = android.graphics.pdf.cts.R.raw.a5_portrait_rgbb;
/**
* Create a {@link PdfRenderer} pointing to a file copied from a resource.
*
* @param docRes The resource to load
* @param context The context to use for creating the renderer
*
* @return the renderer
*
* @throws IOException If anything went wrong
*/
static @NonNull PdfRenderer createRenderer(@RawRes int docRes, @NonNull Context context)
throws IOException {
File pdfFile = sFiles.get(docRes);
if (pdfFile == null) {
pdfFile = File.createTempFile("pdf", null, context.getCacheDir());
// Copy resource to file so that we can open it as a ParcelFileDescriptor
try (OutputStream os = new BufferedOutputStream(new FileOutputStream(pdfFile))) {
try (InputStream is = new BufferedInputStream(
context.getResources().openRawResource(docRes))) {
byte buffer[] = new byte[1024];
while (true) {
int numRead = is.read(buffer, 0, buffer.length);
if (numRead == -1) {
break;
}
os.write(Arrays.copyOf(buffer, numRead));
}
os.flush();
}
}
sFiles.put(docRes, pdfFile);
}
return new PdfRenderer(
ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY));
}
/**
* Render a pdf onto a bitmap <u>while</u> applying the transformation <u>in the</u>
* PDFRenderer. Hence use PdfRenderer.*'s translation and clipping methods.
*
* @param bmWidth The width of the destination bitmap
* @param bmHeight The height of the destination bitmap
* @param docRes The resolution of the doc
* @param clipping The clipping for the PDF document
* @param transformation The transformation of the PDF
* @param renderMode The render mode to use to render the PDF
* @param context The context to use for creating the renderer
*
* @return The rendered bitmap
*/
static @NonNull Bitmap renderWithTransform(int bmWidth, int bmHeight, @RawRes int docRes,
@Nullable Rect clipping, @Nullable Matrix transformation, int renderMode,
@NonNull Context context)
throws IOException {
try (PdfRenderer renderer = createRenderer(docRes, context)) {
try (PdfRenderer.Page page = renderer.openPage(0)) {
Bitmap bm = Bitmap.createBitmap(bmWidth, bmHeight, Bitmap.Config.ARGB_8888);
page.render(bm, clipping, transformation, renderMode);
return bm;
}
}
}
/**
* Render a pdf onto a bitmap <u>and then</u> apply then render the resulting bitmap onto
* another bitmap while applying the transformation. Hence use canvas' translation and clipping
* methods.
*
* @param bmWidth The width of the destination bitmap
* @param bmHeight The height of the destination bitmap
* @param docRes The resolution of the doc
* @param clipping The clipping for the PDF document
* @param transformation The transformation of the PDF
* @param renderMode The render mode to use to render the PDF
* @param context The context to use for creating the renderer
*
* @return The rendered bitmap
*/
private static @NonNull Bitmap renderAndThenTransform(int bmWidth, int bmHeight,
@RawRes int docRes, @Nullable Rect clipping, @Nullable Matrix transformation,
int renderMode, @NonNull Context context) throws IOException {
Bitmap renderedBm;
renderedBm = sRenderedBitmaps.get(docRes);
if (renderedBm == null) {
try (PdfRenderer renderer = Utils.createRenderer(docRes, context)) {
try (PdfRenderer.Page page = renderer.openPage(0)) {
renderedBm = Bitmap.createBitmap(page.getWidth(), page.getHeight(),
Bitmap.Config.ARGB_8888);
page.render(renderedBm, null, null, renderMode);
}
}
sRenderedBitmaps.put(docRes, renderedBm);
}
if (transformation == null) {
// According to PdfRenderer.page#render transformation == null means that the bitmap
// should be stretched to clipping (if provided) or otherwise destination size
transformation = new Matrix();
if (clipping != null) {
transformation.postScale((float) clipping.width() / renderedBm.getWidth(),
(float) clipping.height() / renderedBm.getHeight());
transformation.postTranslate(clipping.left, clipping.top);
} else {
transformation.postScale((float) bmWidth / renderedBm.getWidth(),
(float) bmHeight / renderedBm.getHeight());
}
}
Bitmap transformedBm = Bitmap.createBitmap(bmWidth, bmHeight, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(transformedBm);
canvas.drawBitmap(renderedBm, transformation, null);
Bitmap clippedBm;
if (clipping != null) {
clippedBm = Bitmap.createBitmap(bmWidth, bmHeight, Bitmap.Config.ARGB_8888);
canvas = new Canvas(clippedBm);
canvas.drawBitmap(transformedBm, clipping, clipping, null);
transformedBm.recycle();
} else {
clippedBm = transformedBm;
}
return clippedBm;
}
/**
* Get the fraction of non-matching pixels of two bitmaps. 1 == no pixels match, 0 == all pixels
* match.
*
* @param a The first bitmap
* @param b The second bitmap
*
* @return The fraction of non-matching pixels.
*/
private static @FloatRange(from = 0, to = 1) float getNonMatching(@NonNull Bitmap a,
@NonNull Bitmap b) {
if (a.getWidth() != b.getWidth() || a.getHeight() != b.getHeight()) {
return 1;
}
int[] aPx = new int[a.getWidth() * a.getHeight()];
int[] bPx = new int[b.getWidth() * b.getHeight()];
a.getPixels(aPx, 0, a.getWidth(), 0, 0, a.getWidth(), a.getHeight());
b.getPixels(bPx, 0, b.getWidth(), 0, 0, b.getWidth(), b.getHeight());
int badPixels = 0;
int totalPixels = a.getWidth() * a.getHeight();
for (int i = 0; i < totalPixels; i++) {
if (aPx[i] != bPx[i]) {
badPixels++;
}
}
return ((float) badPixels) / totalPixels;
}
/**
* Render the PDF two times. Once with applying the transformation and clipping in the {@link
* PdfRenderer}. The other time render the PDF onto a bitmap and then clip and transform that
* image. The result should be the same beside some minor aliasing.
*
* @param width The width of the resulting bitmap
* @param height The height of the resulting bitmap
* @param docRes The resource of the PDF document
* @param clipping The clipping to apply
* @param transformation The transformation to apply
* @param renderMode The render mode to use
* @param context The context to use for creating the renderer
*
* @throws IOException
*/
static void renderAndCompare(int width, int height, @RawRes int docRes,
@Nullable Rect clipping, @Nullable Matrix transformation, int renderMode,
@NonNull Context context) throws IOException {
Bitmap a = renderWithTransform(width, height, docRes, clipping, transformation,
renderMode, context);
Bitmap b = renderAndThenTransform(width, height, docRes, clipping, transformation,
renderMode, context);
try {
// We allow 1% aliasing error
float nonMatching = getNonMatching(a, b);
if (nonMatching == 0) {
Log.d(LOG_TAG, "bitmaps match");
} else if (nonMatching > 0.01) {
fail("Testing width:" + width + ", height:" + height + ", docRes:" + docRes +
", clipping:" + clipping + ", transform:" + transformation + ". Bitmaps " +
"differ by " + Math.ceil(nonMatching * 10000) / 100 +
"%. That is too much.");
} else {
Log.d(LOG_TAG, "bitmaps differ by " + Math.ceil(nonMatching * 10000) / 100 + "%");
}
} finally {
a.recycle();
b.recycle();
}
}
/**
* Run a runnable and expect an exception of a certain type.
*
* @param r The {@link Invokable} to run
* @param expectedClass The expected exception type
*/
static void verifyException(@NonNull Invokable r,
@NonNull Class<? extends Exception> expectedClass) {
try {
r.run();
} catch (Exception e) {
if (e.getClass().isAssignableFrom(expectedClass)) {
return;
} else {
Log.e(LOG_TAG, "Incorrect exception", e);
fail("Expected: " + expectedClass.getName() + ", got: " + e.getClass().getName());
}
}
fail("Expected to have " + expectedClass.getName() + " exception thrown");
}
/**
* A runnable that can throw an exception.
*/
interface Invokable {
void run() throws Exception;
}
}