/* * Copyright (C) 2013 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.tools.idea.wizard; import com.android.assetstudiolib.*; import com.android.tools.idea.rendering.ImageUtils; import com.google.common.base.Strings; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Iterables; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VfsUtil; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; import java.io.*; import java.util.LinkedHashMap; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import static java.awt.image.BufferedImage.TYPE_INT_ARGB; public class AssetStudioAssetGenerator implements GraphicGeneratorContext { public static final String ATTR_TEXT = "text"; public static final String ATTR_SCALING = "scaling"; public static final String ATTR_SHAPE = "shape"; public static final String ATTR_PADDING = "padding"; public static final String ATTR_TRIM = "trim"; public static final String ATTR_FONT = "font"; public static final String ATTR_FONT_SIZE = "fontSize"; public static final String ATTR_SOURCE_TYPE = "sourceType"; public static final String ATTR_IMAGE_PATH = "imagePath"; public static final String ATTR_CLIPART_NAME = "clipartPath"; public static final String ATTR_FOREGROUND_COLOR = "foregroundColor"; public static final String ATTR_BACKGROUND_COLOR = "backgroundColor"; public static final String ATTR_ASSET_TYPE = "assetType"; public static final String ATTR_ASSET_THEME = "assetTheme"; public static final String ATTR_ASSET_NAME = "assetName"; private static final Logger LOG = Logger.getInstance("#" + AssetStudioAssetGenerator.class.getName()); private static final String OUTPUT_DIRECTORY = "src/main/"; private static Cache<String, BufferedImage> ourImageCache = CacheBuilder.newBuilder().build(); private final ActionBarIconGenerator myActionBarIconGenerator; private final NotificationIconGenerator myNotificationIconGenerator; private final LauncherIconGenerator myLauncherIconGenerator; private AssetStudioContext myContext; /** * This is needed to migrate between "old" and "new" wizard frameworks */ public interface AssetStudioContext { int getPadding(); void setPadding(int padding); @Nullable SourceType getSourceType(); void setSourceType(SourceType sourceType); @Nullable AssetType getAssetType(); boolean isTrim(); void setTrim(boolean trim); @Nullable String getImagePath(); @Nullable String getText(); void setText(String text); @Nullable String getClipartName(); void setClipartName(String clipartName); Color getForegroundColor(); void setForegroundColor(Color fg); @Nullable String getFont(); void setFont(String font); int getFontSize(); void setFontSize(int fontSize); Scaling getScaling(); void setScaling(Scaling scaling); @Nullable String getAssetName(); GraphicGenerator.Shape getShape(); void setShape(GraphicGenerator.Shape shape); Color getBackgroundColor(); void setBackgroundColor(Color bg); @Nullable String getAssetTheme(); } public static class ImageGeneratorException extends Exception { public ImageGeneratorException(String message) { super(message); } } /** * Types of sources that the asset studio can use to generate icons from */ public enum SourceType { IMAGE, CLIPART, TEXT } public enum Scaling { CENTER, CROP } /** * The type of asset to create: launcher icon, menu icon, etc. */ public enum AssetType { /** * Launcher icon to be shown in the application list */ LAUNCHER("Launcher Icons", "ic_launcher"), /** * Icons shown in the action bar */ ACTIONBAR("Action Bar and Tab Icons", "ic_action_%s"), /** * Icons shown in a notification message */ NOTIFICATION("Notification Icons", "ic_stat_%s"); /** * Icons shown as part of tabs */ //TAB("Pre-Android 3.0 Tab Icons", "ic_tab_%s"), /** * Icons shown in menus */ //MENU("Pre-Android 3.0 Menu Icons", "ic_menu_%s"); /** * Display name to show to the user in the asset type selection list */ private final String myDisplayName; /** * Default asset name format */ private String myDefaultNameFormat; AssetType(String displayName, String defaultNameFormat) { myDisplayName = displayName; myDefaultNameFormat = defaultNameFormat; } /** * Returns the display name of this asset type to show to the user in the * asset wizard selection page etc */ public String getDisplayName() { return myDisplayName; } @Override public String toString() { return getDisplayName(); } /** * Returns the default format to use to suggest a name for the asset */ public String getDefaultNameFormat() { return myDefaultNameFormat; } /** * Whether this asset type configures foreground scaling */ public boolean needsForegroundScaling() { return this == LAUNCHER; } /** * Whether this asset type needs a shape parameter */ public boolean needsShape() { return this == LAUNCHER; } /** * Whether this asset type needs foreground and background color parameters */ public boolean needsColors() { return this == LAUNCHER; } /** * Whether this asset type needs an effects parameter */ public boolean needsEffects() { return this == LAUNCHER; } /** * Whether this asset type needs a theme parameter */ public boolean needsTheme() { return this == ACTIONBAR; } } public AssetStudioAssetGenerator(AssetStudioContext context) { this(context, new ActionBarIconGenerator(), new NotificationIconGenerator(), new LauncherIconGenerator()); } public AssetStudioAssetGenerator(TemplateWizardState state) { this(new TemplateWizardContextAdapter(state), new ActionBarIconGenerator(), new NotificationIconGenerator(), new LauncherIconGenerator()); } /** * Allows dependency injection for testing */ @SuppressWarnings("UseJBColor") // These colors are for the graphics generator, not the plugin UI public AssetStudioAssetGenerator(@NotNull AssetStudioContext context, @Nullable ActionBarIconGenerator actionBarIconGenerator, @Nullable NotificationIconGenerator notificationIconGenerator, @Nullable LauncherIconGenerator launcherIconGenerator) { myContext = context; myActionBarIconGenerator = actionBarIconGenerator != null ? actionBarIconGenerator : new ActionBarIconGenerator(); myNotificationIconGenerator = notificationIconGenerator != null ? notificationIconGenerator : new NotificationIconGenerator(); myLauncherIconGenerator = launcherIconGenerator != null ? launcherIconGenerator : new LauncherIconGenerator(); myContext.setText("Aa"); myContext.setFont("Arial Black"); myContext.setScaling(Scaling.CROP); myContext.setShape(GraphicGenerator.Shape.NONE); myContext.setFontSize(144); myContext.setSourceType(AssetStudioAssetGenerator.SourceType.IMAGE); myContext.setClipartName("android.png"); myContext.setForegroundColor(Color.BLUE); myContext.setBackgroundColor(Color.WHITE); myContext.setTrim(false); myContext.setPadding(0); } @Override @Nullable public BufferedImage loadImageResource(@NotNull final String path) { try { return ourImageCache.get(path, new Callable<BufferedImage>() { @Override public BufferedImage call() throws Exception { return getImage(path, true); } }); } catch (ExecutionException e) { LOG.error(e); return null; } } /** * Generate images using the given wizard state * * @param previewOnly whether we are only generating previews * @return a map of image objects */ @NotNull public Map<String, Map<String, BufferedImage>> generateImages(boolean previewOnly) throws ImageGeneratorException { Map<String, Map<String, BufferedImage>> categoryMap = new LinkedHashMap<String, Map<String, BufferedImage>>(); generateImages(categoryMap, false, previewOnly); return categoryMap; } /** * Generate images using the given wizard state into the given map * @param categoryMap the map to store references to the resultant images in * @param clearMap if true, the map will be emptied before use * @param previewOnly whether we are only generating previews * @throws ImageGeneratorException */ public void generateImages(Map<String, Map<String, BufferedImage>> categoryMap, boolean clearMap, boolean previewOnly) throws ImageGeneratorException { if (clearMap) { categoryMap.clear(); } AssetType type = myContext.getAssetType(); if (type == null) { // If we don't know what we're building, don't do it yet. return; } boolean trim = myContext.isTrim(); int padding = myContext.getPadding(); SourceType sourceType = myContext.getSourceType(); if (sourceType == null) { return; } BufferedImage sourceImage = null; switch (sourceType) { case IMAGE: { String path = myContext.getImagePath(); if (path == null || path.isEmpty()) { throw new ImageGeneratorException("Path to image is empty."); } try { sourceImage = getImage(path, false); } catch (FileNotFoundException e) { throw new ImageGeneratorException("Image file not found: " + path); } catch (IOException e) { throw new ImageGeneratorException("Unable to load image file: " + path); } break; } case CLIPART: { String clipartName = myContext.getClipartName(); try { sourceImage = GraphicGenerator.getClipartImage(clipartName); } catch (IOException e) { throw new ImageGeneratorException("Unable to load clip art image: " + clipartName); } if (type.needsColors()) { Paint paint = myContext.getForegroundColor(); sourceImage = Util.filledImage(sourceImage, paint); } break; } case TEXT: { TextRenderUtil.Options options = new TextRenderUtil.Options(); options.font = Font.decode(myContext.getFont() + " " + myContext.getFontSize()); options.foregroundColor = type.needsColors() ? myContext.getForegroundColor().getRGB() : 0xFFFFFFFF; sourceImage = TextRenderUtil.renderTextImage(myContext.getText(), 1, options); break; } } if (trim) { sourceImage = crop(sourceImage); } if (padding != 0) { sourceImage = Util.paddedImage(sourceImage, padding); } GraphicGenerator generator = null; GraphicGenerator.Options options = null; String baseName = Strings.nullToEmpty(myContext.getAssetName()); switch (type) { case LAUNCHER: { generator = myLauncherIconGenerator; LauncherIconGenerator.LauncherOptions launcherOptions = new LauncherIconGenerator.LauncherOptions(); launcherOptions.shape = myContext.getShape(); launcherOptions.crop = Scaling.CROP.equals(myContext.getScaling()); launcherOptions.style = GraphicGenerator.Style.SIMPLE; launcherOptions.backgroundColor = myContext.getBackgroundColor().getRGB(); launcherOptions.isWebGraphic = !previewOnly; options = launcherOptions; } break; case ACTIONBAR: { generator = myActionBarIconGenerator; ActionBarIconGenerator.ActionBarOptions actionBarOptions = new ActionBarIconGenerator.ActionBarOptions(); String themeName = myContext.getAssetTheme(); if (!StringUtil.isEmpty(themeName)) { ActionBarIconGenerator.Theme theme = ActionBarIconGenerator.Theme.valueOf(themeName); if (theme != null) { switch (theme) { case HOLO_DARK: actionBarOptions.theme = ActionBarIconGenerator.Theme.HOLO_DARK; break; case HOLO_LIGHT: actionBarOptions.theme = ActionBarIconGenerator.Theme.HOLO_LIGHT; break; case CUSTOM: actionBarOptions.theme = ActionBarIconGenerator.Theme.CUSTOM; actionBarOptions.customThemeColor = myContext.getForegroundColor().getRGB(); break; } } } actionBarOptions.sourceIsClipart = (sourceType == SourceType.CLIPART); options = actionBarOptions; } break; case NOTIFICATION: generator = myNotificationIconGenerator; NotificationIconGenerator.NotificationOptions notificationOptions = new NotificationIconGenerator.NotificationOptions(); notificationOptions.version = NotificationIconGenerator.Version.V11; options = notificationOptions; break; } options.sourceImage = sourceImage; generator.generate(null, categoryMap, this, options, baseName); } /** * Outputs final-rendered images to disk, rooted at the given variant directory. */ public void outputImagesIntoVariantRoot(@NotNull File variantDir) { try { Map<String, Map<String, BufferedImage>> images = generateImages(false); for (Map<String, BufferedImage> density : images.values()) { for (Map.Entry<String, BufferedImage> image : density.entrySet()) { // TODO: The output directory needs to take flavor and build type into account, which will need to be configurable by the user File file = new File(variantDir, image.getKey()); try { VirtualFile directory = VfsUtil.createDirectories(file.getParentFile().getAbsolutePath()); VirtualFile imageFile = directory.findChild(file.getName()); if (imageFile == null || !imageFile.exists()) { imageFile = directory.createChildData(this, file.getName()); } OutputStream outputStream = imageFile.getOutputStream(this); try { ImageIO.write(image.getValue(), "PNG", outputStream); } finally { outputStream.close(); } } catch (IOException e) { LOG.error(e); } } } } catch (Exception e) { LOG.error(e); } } /** * Outputs final-rendered images to disk, rooted at the given module directory */ public void outputImagesIntoDefaultVariant(@NotNull File contentRoot) { File directory = new File(contentRoot, OUTPUT_DIRECTORY); outputImagesIntoVariantRoot(directory); } @NotNull protected static BufferedImage getImage(@NotNull String path, boolean isPluginRelative) throws IOException { BufferedImage image; if (isPluginRelative) { image = GraphicGenerator.getStencilImage(path); } else { image = ImageIO.read(new File(path)); } if (image == null) { //noinspection UndesirableClassUsage image = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); } return image; } @NotNull protected static BufferedImage crop(@NotNull BufferedImage sourceImage) { BufferedImage cropped = ImageUtils.cropBlank(sourceImage, null, TYPE_INT_ARGB); return cropped != null ? cropped : sourceImage; } }