/*
* Copyright (C) 2011 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.assetstudiolib;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.resources.Density;
import com.android.resources.ResourceFolderType;
import com.android.utils.SdkUtils;
import com.google.common.collect.Lists;
import com.google.common.io.Closeables;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import javax.imageio.ImageIO;
/**
* The base Generator class.
*/
public abstract class GraphicGenerator {
/**
* Options used for all generators.
*/
public static class Options {
/** Minimum version (API level) of the SDK to generate icons for */
public int minSdk = 1;
/** Source image to use as a basis for the icon */
public BufferedImage sourceImage;
/** The density to generate the icon with */
public Density density = Density.XHIGH;
/** Whether the icon should be written out to the mipmap folder instead of drawable */
public boolean mipmap;
}
/** Shapes that can be used for icon backgrounds */
public enum Shape {
/** No background */
NONE("none"),
/** Circular background */
CIRCLE("circle"),
/** Square background */
SQUARE("square"),
/** Vertical rectangular background */
VRECT("vrect"),
/** Horizontal rectangular background */
HRECT("hrect"),
/** Square background with Dog-ear effect */
SQUARE_DOG("square_dogear"),
/** Vertical rectangular background with Dog-ear effect */
VRECT_DOG("vrect_dogear"),
/** Horizontal rectangular background with Dog-ear effect */
HRECT_DOG("hrect_dogear");
/** Id, used in filenames to identify associated stencils */
public final String id;
Shape(String id) {
this.id = id;
}
}
/** Foreground effects styles */
public enum Style {
/** No effects */
SIMPLE("fore1");
/** Id, used in filenames to identify associated stencils */
public final String id;
Style(String id) {
this.id = id;
}
}
/**
* Generate a single icon using the given options
*
* @param context render context to use for looking up resources etc
* @param options options controlling the appearance of the icon
* @return a {@link BufferedImage} with the generated icon
*/
public abstract BufferedImage generate(GraphicGeneratorContext context, Options options);
/**
* Computes the target filename (relative to the Android project folder)
* where an icon rendered with the given options should be stored. This is
* also used as the map keys in the result map used by
* {@link #generate(String, Map, GraphicGeneratorContext, Options, String)}.
*
* @param options the options object used by the generator for the current
* image
* @param name the base name to use when creating the path
* @return a path relative to the res/ folder where the image should be
* stored (will always use / as a path separator, not \ on Windows)
*/
protected String getIconPath(Options options, String name) {
return getIconFolder(options) + '/' + getIconName(options, name);
}
/**
* Gets name of the file itself. It is sometimes modified by options, for
* example in unselected tabs we change foo.png to foo-unselected.png
*/
protected String getIconName(Options options, String name) {
if (options.density == Density.ANYDPI) {
return name + SdkConstants.DOT_XML;
}
return name + SdkConstants.DOT_PNG; //$NON-NLS-1$
}
/**
* Gets name of the folder to contain the resource. It usually includes the
* density, but is also sometimes modified by options. For example, in some
* notification icons we add in -v9 or -v11.
*/
protected String getIconFolder(Options options) {
if (options.density == Density.ANYDPI) {
return SdkConstants.FD_RES + '/' +
ResourceFolderType.DRAWABLE.getName();
}
StringBuilder sb = new StringBuilder(50);
sb.append(SdkConstants.FD_RES);
sb.append('/');
if (options.mipmap) {
sb.append(ResourceFolderType.MIPMAP.getName());
} else {
sb.append(ResourceFolderType.DRAWABLE.getName());
}
sb.append('-');
sb.append(options.density.getResourceValue());
return sb.toString();
}
/**
* Generates a full set of icons into the given map. The values in the map
* will be the generated images, and each value is keyed by the
* corresponding relative path of the image, which is determined by the
* {@link #getIconPath(Options, String)} method.
*
* @param category the current category to place images into (if null the
* density name will be used)
* @param categoryMap the map to put images into, should not be null. The
* map is a map from a category name, to a map from file path to
* image.
* @param context a generator context which for example can load resources
* @param options options to apply to this generator
* @param name the base name of the icons to generate
*/
public void generate(String category, Map<String, Map<String, BufferedImage>> categoryMap,
GraphicGeneratorContext context, Options options, String name) {
// Vector image only need to generate one preview image, so we by pass all the
// other image densities.
if (options.density == Density.ANYDPI) {
generateImageAndUpdateMap(category, categoryMap, context, options, name);
return;
}
Density[] densityValues = Density.values();
// Sort density values into ascending order
Arrays.sort(densityValues, new Comparator<Density>() {
@Override
public int compare(Density d1, Density d2) {
return d1.getDpiValue() - d2.getDpiValue();
}
});
for (Density density : densityValues) {
if (!density.isValidValueForDevice()) {
continue;
}
if (!includeDensity(density)) {
// Not yet supported -- missing stencil image
// TODO don't manually check and instead gracefully handle missing stencils.
continue;
}
options.density = density;
generateImageAndUpdateMap(category, categoryMap, context, options, name);
}
}
private void generateImageAndUpdateMap(String category,
Map<String, Map<String, BufferedImage>> categoryMap,
GraphicGeneratorContext context,
Options options,
String name) {
BufferedImage image = generate(context, options);
if (image != null) {
String mapCategory = category;
if (mapCategory == null) {
mapCategory = options.density.getResourceValue();
}
Map<String, BufferedImage> imageMap = categoryMap.get(mapCategory);
if (imageMap == null) {
imageMap = new LinkedHashMap<String, BufferedImage>();
categoryMap.put(mapCategory, imageMap);
}
imageMap.put(getIconPath(options, name), image);
}
}
protected boolean includeDensity(@NonNull Density density) {
return density.isRecommended() && density != Density.LOW && density != Density.XXXHIGH;
}
/**
* Returns the scale factor to apply for a given MDPI density to compute the
* absolute pixel count to use to draw an icon of the given target density
*
* @param density the density
* @return a factor to multiple mdpi distances with to compute the target density
*/
public static float getMdpiScaleFactor(Density density) {
if (density == Density.ANYDPI) {
density = Density.XXXHIGH;
}
return density.getDpiValue() / (float) Density.MEDIUM.getDpiValue();
}
/**
* Returns one of the built in stencil images, or null
*
* @param relativePath stencil path such as "launcher-stencil/square/web/back.png"
* @return the image, or null
* @throws IOException if an unexpected I/O error occurs
*/
public static BufferedImage getStencilImage(String relativePath) throws IOException {
InputStream is = GraphicGenerator.class.getResourceAsStream(relativePath);
if (is == null) {
return null;
}
try {
return ImageIO.read(is);
} finally {
Closeables.close(is, true /* swallowIOException */);
}
}
/**
* Returns the icon (32x32) for a given clip art image.
*
* @param name the name of the image to be loaded (which can be looked up via
* {@link #getClipartNames()})
* @return the icon image
* @throws IOException if the image cannot be loaded
*/
public static BufferedImage getClipartIcon(String name) throws IOException {
InputStream is = GraphicGenerator.class.getResourceAsStream(
"/images/clipart/small/" + name);
try {
return ImageIO.read(is);
} finally {
Closeables.close(is, true /* swallowIOException */);
}
}
/**
* Returns the full size clip art image for a given image name.
*
* @param name the name of the image to be loaded (which can be looked up via
* {@link #getClipartNames()})
* @return the clip art image
* @throws IOException if the image cannot be loaded
*/
public static BufferedImage getClipartImage(String name) throws IOException {
InputStream is = GraphicGenerator.class.getResourceAsStream(
"/images/clipart/big/" + name);
try {
return ImageIO.read(is);
} finally {
Closeables.close(is, true /* swallowIOException */);
}
}
/**
* Returns the names of available clip art images which can be obtained by passing the
* name to {@link #getClipartIcon(String)} or
* {@link GraphicGenerator#getClipartImage(String)}
*
* @return an iterator for the available image names
*/
public static Iterator<String> getResourcesNames(String pathPrefix, String filenameExtension) {
List<String> names = new ArrayList<String>(80);
try {
ZipFile zipFile = null;
ProtectionDomain protectionDomain = GraphicGenerator.class.getProtectionDomain();
URL url = protectionDomain.getCodeSource().getLocation();
if (url != null) {
File file = SdkUtils.urlToFile(url);
zipFile = new JarFile(file);
} else {
Enumeration<URL> en =
GraphicGenerator.class.getClassLoader().getResources(pathPrefix);
if (en.hasMoreElements()) {
url = en.nextElement();
URLConnection urlConnection = url.openConnection();
if (urlConnection instanceof JarURLConnection) {
JarURLConnection urlConn = (JarURLConnection)(urlConnection);
zipFile = urlConn.getJarFile();
} else if ("file".equals(url.getProtocol())) { //$NON-NLS-1$
File directory = new File(url.getPath());
return Lists.newArrayList(directory.list()).iterator();
}
}
}
Enumeration<? extends ZipEntry> enumeration = zipFile.entries();
while (enumeration.hasMoreElements()) {
ZipEntry zipEntry = enumeration.nextElement();
String name = zipEntry.getName();
if (!name.startsWith(pathPrefix) || !name.endsWith(filenameExtension)) { //$NON-NLS-1$
continue;
}
int lastSlash = name.lastIndexOf('/');
if (lastSlash != -1) {
name = name.substring(lastSlash + 1);
}
names.add(name);
}
} catch (final Exception e) {
e.printStackTrace();
}
return names.iterator();
}
}