/* * Copyright (C) 2014 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 com.android.builder.png; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertEquals; import com.android.SdkConstants; import com.android.annotations.NonNull; import com.android.ide.common.internal.PngCruncher; import com.android.ide.common.internal.PngException; import com.android.sdklib.BuildToolInfo; import com.android.sdklib.SdkManager; import com.android.sdklib.repository.FullRevision; import com.android.testutils.TestUtils; import com.android.utils.ILogger; import com.android.utils.StdLogger; import com.google.common.collect.ImmutableList; import com.google.common.collect.Maps; import com.google.common.io.Files; import org.junit.Assert; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.util.Arrays; import java.util.Collection; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import java.util.zip.DataFormatException; import javax.imageio.ImageIO; /** * Utilities common to tests for both the synchronous and the asynchronous Aapt processor. */ public class NinePatchAaptProcessorTestUtils { /** * Signature of a PNG file. */ public static final byte[] SIGNATURE = new byte[]{ (byte) 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}; /** * Returns the lastest build tools that's at least the passed version. * @param fullRevision the minimum required build tools version. * @return the latest build tools. * @throws RuntimeException if the latest build tools is older than fullRevision. */ static File getAapt(FullRevision fullRevision) { ILogger logger = new StdLogger(StdLogger.Level.VERBOSE); SdkManager sdkManager = SdkManager.createManager(getSdkDir().getAbsolutePath(), logger); assert sdkManager != null; BuildToolInfo buildToolInfo = sdkManager.getLatestBuildTool(); if (buildToolInfo == null || buildToolInfo.getRevision().compareTo(fullRevision) < 0) { throw new RuntimeException("Test requires build-tools " + fullRevision.toShortString()); } return new File(buildToolInfo.getPath(BuildToolInfo.PathId.AAPT)); } public static void tearDownAndCheck(int cruncherKey, Map<File, File> sourceAndCrunchedFiles, PngCruncher cruncher, AtomicLong classStartTime) throws IOException, DataFormatException { long startTime = System.currentTimeMillis(); try { cruncher.end(cruncherKey); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println( "waiting for requests completion : " + (System.currentTimeMillis() - startTime)); System.out.println("total time : " + (System.currentTimeMillis() - classStartTime.get())); System.out.println("Comparing crunched files"); long comparisonStartTime = System.currentTimeMillis(); for (Map.Entry<File, File> sourceAndCrunched : sourceAndCrunchedFiles.entrySet()) { System.out.println(sourceAndCrunched.getKey().getName()); File crunched = new File(sourceAndCrunched.getKey().getParent(), sourceAndCrunched.getKey().getName() + getControlFileSuffix()); //copyFile(sourceAndCrunched.getValue(), crunched); Map<String, Chunk> testedChunks = compareChunks(crunched, sourceAndCrunched.getValue()); try { compareImageContent(crunched, sourceAndCrunched.getValue(), false); } catch (Throwable e) { throw new RuntimeException("Failed with " + testedChunks.get("IHDR"), e); } } System.out.println("Done comparing crunched files " + (System.currentTimeMillis() - comparisonStartTime)); } protected static String getControlFileSuffix() { return ".crunched.aapt"; } private static void copyFile(File source, File dest) throws IOException { FileChannel inputChannel = null; FileChannel outputChannel = null; try { inputChannel = new FileInputStream(source).getChannel(); outputChannel = new FileOutputStream(dest).getChannel(); outputChannel.transferFrom(inputChannel, 0, inputChannel.size()); } finally { inputChannel.close(); outputChannel.close(); } } @NonNull static File crunchFile(int crunchKey, @NonNull File file, PngCruncher aaptCruncher) throws PngException, IOException { File outFile = File.createTempFile("pngWriterTest", ".png"); outFile.deleteOnExit(); try { aaptCruncher.crunchPng(crunchKey, file, outFile); } catch (PngException e) { e.printStackTrace(); throw e; } System.out.println("crunch " + file.getPath()); return outFile; } private static Map<String, Chunk> compareChunks(@NonNull File original, @NonNull File tested) throws IOException, DataFormatException { Map<String, Chunk> originalChunks = readChunks(original); Map<String, Chunk> testedChunks = readChunks(tested); compareChunk(originalChunks, testedChunks, "IHDR"); compareChunk(originalChunks, testedChunks, "npLb"); compareChunk(originalChunks, testedChunks, "npTc"); return testedChunks; } private static void compareChunk( @NonNull Map<String, Chunk> originalChunks, @NonNull Map<String, Chunk> testedChunks, @NonNull String chunkType) { assertEquals(originalChunks.get(chunkType), testedChunks.get(chunkType)); } public static Collection<Object[]> getNinePatches() { File pngFolder = getPngFolder(); File ninePatchFolder = new File(pngFolder, "ninepatch"); File[] files = ninePatchFolder.listFiles(new FileFilter() { @Override public boolean accept(File file) { return file.getPath().endsWith(SdkConstants.DOT_9PNG); } }); if (files != null) { ImmutableList.Builder<Object[]> params = ImmutableList.builder(); for (File file : files) { params.add(new Object[]{file, file.getName()}); } return params.build(); } return ImmutableList.of(); } protected static void compareImageContent(@NonNull File originalFile, @NonNull File createdFile, boolean is9Patch) throws IOException { BufferedImage originalImage = ImageIO.read(originalFile); BufferedImage createdImage = ImageIO.read(createdFile); int originalWidth = originalImage.getWidth(); int originalHeight = originalImage.getHeight(); int createdWidth = createdImage.getWidth(); int createdHeight = createdImage.getHeight(); // compare sizes taking into account if the image is a 9-patch // in which case the original is bigger by 2 since it has the patch area still. Assert.assertEquals(originalWidth, createdWidth + (is9Patch ? 2 : 0)); Assert.assertEquals(originalHeight, createdHeight + (is9Patch ? 2 : 0)); // get the file content // always use the created Size. And for the original image, if 9-patch, just take // the image minus the 1-pixel border all around. int[] originalContent = new int[createdWidth * createdHeight]; if (is9Patch) { originalImage .getRGB(1, 1, createdWidth, createdHeight, originalContent, 0, createdWidth); } else { originalImage .getRGB(0, 0, createdWidth, createdHeight, originalContent, 0, createdWidth); } int[] createdContent = new int[createdWidth * createdHeight]; createdImage.getRGB(0, 0, createdWidth, createdHeight, createdContent, 0, createdWidth); for (int y = 0; y < createdHeight; y++) { for (int x = 0; x < createdWidth; x++) { int originalRGBA = originalContent[y * createdWidth + x]; int createdRGBA = createdContent[y * createdWidth + x]; Assert.assertEquals( String.format("%dx%d: 0x%08x : 0x%08x", x, y, originalRGBA, createdRGBA), originalRGBA, createdRGBA); } } } @NonNull protected static Map<String, Chunk> readChunks(@NonNull File file) throws IOException { Map<String, Chunk> chunks = Maps.newHashMap(); byte[] fileBuffer = Files.toByteArray(file); ByteBuffer buffer = ByteBuffer.wrap(fileBuffer); byte[] sig = new byte[8]; buffer.get(sig); assertTrue(Arrays.equals(sig, SIGNATURE)); byte[] data, type; int len; int crc32; while (buffer.hasRemaining()) { len = buffer.getInt(); type = new byte[4]; buffer.get(type); data = new byte[len]; buffer.get(data); // crc crc32 = buffer.getInt(); Chunk chunk = new Chunk(type, data, crc32); chunks.put(chunk.getTypeAsString(), chunk); } return chunks; } /** * Returns the SDK folder as built from the Android source tree. * * @return the SDK */ @NonNull protected static File getSdkDir() { String androidHome = System.getenv("ANDROID_HOME"); if (androidHome != null) { File f = new File(androidHome); if (f.isDirectory()) { return f; } } throw new IllegalStateException("SDK not defined with ANDROID_HOME"); } @NonNull protected static File getFile(@NonNull String name) { return new File(getPngFolder(), name); } @NonNull protected static File getPngFolder() { File folder = TestUtils.getRoot("png"); assertTrue(folder.isDirectory()); return folder; } }