Merge changes I7b7cee68,I07d22c0d am: fb8a636a2d
am: 5e789287d4

Change-Id: Ibf6bf05090a70fa49636c5d517df309736f1f3d1
diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java
index 8730945..9d88267 100644
--- a/tools/image_generator/ImageGenerator.java
+++ b/tools/image_generator/ImageGenerator.java
@@ -32,9 +32,11 @@
 import java.awt.FontMetrics;
 import java.awt.Graphics2D;
 import java.awt.RenderingHints;
+import java.awt.font.TextAttribute;
 import java.awt.image.BufferedImage;
 import java.io.File;
 import java.io.IOException;
+import java.text.AttributedString;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.HashSet;
@@ -44,6 +46,8 @@
 import java.util.Set;
 import java.util.StringTokenizer;
 import java.util.TreeMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import javax.imageio.ImageIO;
 import javax.xml.parsers.DocumentBuilder;
@@ -57,6 +61,8 @@
 
     private static final float DEFAULT_FONT_SIZE = 40;
 
+    private static final Logger LOGGER = Logger.getLogger(ImageGenerator.class.getName());
+
     // This is the canvas we used to draw texts.
     private BufferedImage mBufferedImage;
 
@@ -83,6 +89,20 @@
     // Align the text in the center of the image.
     private final boolean mCenterAlignment;
 
+    // Some localized font cannot draw the word "Android" and some PUNCTUATIONS; we need to fall
+    // back to use our default latin font instead.
+    private static final char[] PUNCTUATIONS = {',', ';', '.', '!' };
+
+    private static final String ANDROID_STRING = "Android";
+
+    // The width of the word "Android" when drawing with the default font.
+    private int mAndroidStringWidth;
+
+    // The default Font to draw latin characters. It's loaded from DEFAULT_FONT_NAME.
+    private Font mDefaultFont;
+    // Cache of the loaded fonts for all languages.
+    private Map<String, Font> mLoadedFontMap;
+
     // An explicit map from language to the font name to use.
     // The map is extracted from frameworks/base/data/fonts/fonts.xml.
     // And the language-subtag-registry is found in:
@@ -160,6 +180,72 @@
         }
     }
 
+    /**
+     *  This class maintains the content of wrapped text, the attributes to draw these text, and
+     *  the width of each wrapped lines.
+     */
+    private class WrappedTextInfo {
+        /** LineInfo holds the AttributedString and width of each wrapped line. */
+        private class LineInfo {
+            public AttributedString mLineContent;
+            public int mLineWidth;
+
+            LineInfo(AttributedString text, int width) {
+                mLineContent = text;
+                mLineWidth = width;
+            }
+        }
+
+        // Maintains the content of each line, as well as the width needed to draw these lines for
+        // a given language.
+        public List<LineInfo> mWrappedLines;
+
+        WrappedTextInfo() {
+            mWrappedLines = new ArrayList<>();
+        }
+
+        /**
+         * Checks if the given text has words "Android" and some PUNCTUATIONS. If it does, and its
+         * associated textFont cannot display them correctly (e.g. for persian and hebrew); sets the
+         * attributes of these substrings to use our default font instead.
+         *
+         * @param text the input string to perform the check on
+         * @param width the pre-calculated width for the given text
+         * @param textFont the localized font to draw the input string
+         * @param fallbackFont our default font to draw latin characters
+         */
+        public void addLine(String text, int width, Font textFont, Font fallbackFont) {
+            AttributedString attributedText = new AttributedString(text);
+            attributedText.addAttribute(TextAttribute.FONT, textFont);
+            attributedText.addAttribute(TextAttribute.SIZE, mFontSize);
+
+            // Skips the check if we don't specify a fallbackFont.
+            if (fallbackFont != null) {
+                // Adds the attribute to use default font to draw the word "Android".
+                if (text.contains(ANDROID_STRING)
+                        && textFont.canDisplayUpTo(ANDROID_STRING) != -1) {
+                    int index = text.indexOf(ANDROID_STRING);
+                    attributedText.addAttribute(TextAttribute.FONT, fallbackFont, index,
+                            index + ANDROID_STRING.length());
+                }
+
+                // Adds the attribute to use default font to draw the PUNCTUATIONS ", . !"
+                for (char punctuation : PUNCTUATIONS) {
+                    if (text.indexOf(punctuation) != -1 && !textFont.canDisplay(punctuation)) {
+                        int index = 0;
+                        while ((index = text.indexOf(punctuation, index)) != -1) {
+                            attributedText.addAttribute(TextAttribute.FONT, fallbackFont, index,
+                                    index + 1);
+                            index += 1;
+                        }
+                    }
+                }
+            }
+
+            mWrappedLines.add(new LineInfo(attributedText, width));
+        }
+    }
+
     /** Initailizes the fields of the image image. */
     public ImageGenerator(
             int initialImageWidth,
@@ -177,6 +263,7 @@
         mTextName = textName;
         mFontSize = fontSize;
         mFontDirPath = fontDirPath;
+        mLoadedFontMap = new TreeMap<>();
 
         mCenterAlignment = centerAlignment;
     }
@@ -239,12 +326,13 @@
      * directory and collect the translated text.
      *
      * @param resourcePath the path to the resource directory
+     * @param localesSet a list of supported locales; resources of other locales will be omitted.
      * @return a map with the locale as key, and translated text as value
      * @throws LocalizedStringNotFoundException if we cannot find the translated text for the given
      *     locale
      */
-    public Map<Locale, String> readLocalizedStringFromXmls(String resourcePath)
-            throws IOException, LocalizedStringNotFoundException {
+    public Map<Locale, String> readLocalizedStringFromXmls(String resourcePath,
+            Set<String> localesSet) throws IOException, LocalizedStringNotFoundException {
         File resourceDir = new File(resourcePath);
         if (!resourceDir.isDirectory()) {
             throw new LocalizedStringNotFoundException(resourcePath + " is not a directory.");
@@ -271,6 +359,12 @@
         String[] nameList =
                 resourceDir.list((File file, String name) -> name.startsWith("values-"));
         for (String name : nameList) {
+            String localeString = name.substring(7);
+            if (localesSet != null && !localesSet.contains(localeString)) {
+                LOGGER.info("Skip parsing text for locale " + localeString);
+                continue;
+            }
+
             File textFile = new File(resourcePath, name + "/strings.xml");
             String localizedText;
             try {
@@ -295,12 +389,18 @@
      * @throws FontFormatException if the font file doesn't have the expected format
      */
     private Font loadFontsByLocale(String language) throws IOException, FontFormatException {
+        if (mLoadedFontMap.containsKey(language)) {
+            return mLoadedFontMap.get(language);
+        }
+
         String fontName = LANGUAGE_TO_FONT_MAP.getOrDefault(language, DEFAULT_FONT_NAME);
         String[] suffixes = {".otf", ".ttf", ".ttc"};
         for (String suffix : suffixes) {
             File fontFile = new File(mFontDirPath, fontName + suffix);
             if (fontFile.isFile()) {
-                return Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(mFontSize);
+                Font result = Font.createFont(Font.TRUETYPE_FONT, fontFile).deriveFont(mFontSize);
+                mLoadedFontMap.put(language, result);
+                return result;
             }
         }
 
@@ -309,39 +409,53 @@
     }
 
     /** Separates the text string by spaces and wraps it by words. */
-    private List<String> wrapTextByWords(String text, FontMetrics metrics) {
-        List<String> wrappedText = new ArrayList<>();
+    private WrappedTextInfo wrapTextByWords(String text, FontMetrics metrics) {
+        WrappedTextInfo info = new WrappedTextInfo();
         StringTokenizer st = new StringTokenizer(text, " \n");
 
+        int lineWidth = 0;  // Width of the processed words of the current line.
         StringBuilder line = new StringBuilder();
         while (st.hasMoreTokens()) {
             String token = st.nextToken();
-            if (metrics.stringWidth(line + token + " ") > mImageWidth) {
-                wrappedText.add(line.toString());
+            int tokenWidth = metrics.stringWidth(token + " ");
+            // Handles the width mismatch of the word "Android" between different fonts.
+            if (token.contains(ANDROID_STRING)
+                    && metrics.getFont().canDisplayUpTo(ANDROID_STRING) != -1) {
+                tokenWidth = tokenWidth - metrics.stringWidth(ANDROID_STRING) + mAndroidStringWidth;
+            }
+
+            if (lineWidth + tokenWidth > mImageWidth) {
+                info.addLine(line.toString(), lineWidth, metrics.getFont(), mDefaultFont);
+
                 line = new StringBuilder();
+                lineWidth = 0;
             }
             line.append(token).append(" ");
+            lineWidth += tokenWidth;
         }
-        wrappedText.add(line.toString());
 
-        return wrappedText;
+        info.addLine(line.toString(), lineWidth, metrics.getFont(), mDefaultFont);
+
+        return info;
     }
 
     /** One character is a word for CJK. */
-    private List<String> wrapTextByCharacters(String text, FontMetrics metrics) {
-        List<String> wrappedText = new ArrayList<>();
-
+    private WrappedTextInfo wrapTextByCharacters(String text, FontMetrics metrics) {
+        WrappedTextInfo info = new WrappedTextInfo();
+        // TODO (xunchang) handle the text wrapping with logogram language mixed with latin.
         StringBuilder line = new StringBuilder();
         for (char token : text.toCharArray()) {
             if (metrics.stringWidth(line + Character.toString(token)) > mImageWidth) {
-                wrappedText.add(line.toString());
+                info.addLine(line.toString(), metrics.stringWidth(line.toString()),
+                        metrics.getFont(), null);
                 line = new StringBuilder();
             }
             line.append(token);
         }
-        wrappedText.add(line.toString());
+        info.addLine(line.toString(), metrics.stringWidth(line.toString()), metrics.getFont(),
+                null);
 
-        return wrappedText;
+        return info;
     }
 
     /**
@@ -350,9 +464,10 @@
      * @param text the string representation of text to wrap
      * @param metrics the metrics of the Font used to draw the text; it gives the width in pixels of
      *     the text given its string representation
-     * @return a list of strings with their width smaller than mImageWidth pixels
+     * @return a WrappedTextInfo class with the width of each AttributedString smaller than
+     *     mImageWidth pixels
      */
-    private List<String> wrapText(String text, FontMetrics metrics, String language) {
+    private WrappedTextInfo wrapText(String text, FontMetrics metrics, String language) {
         if (LOGOGRAM_LANGUAGE.contains(language)) {
             return wrapTextByCharacters(text, metrics);
         }
@@ -401,11 +516,11 @@
             throws IOException, FontFormatException {
         Graphics2D graphics = createGraphics(locale);
         FontMetrics fontMetrics = graphics.getFontMetrics();
-        List<String> wrappedText = wrapText(text, fontMetrics, locale.getLanguage());
+        WrappedTextInfo wrappedTextInfo = wrapText(text, fontMetrics, locale.getLanguage());
 
         int textWidth = 0;
-        for (String line : wrappedText) {
-            textWidth = Math.max(textWidth, fontMetrics.stringWidth(line));
+        for (WrappedTextInfo.LineInfo lineInfo : wrappedTextInfo.mWrappedLines) {
+            textWidth = Math.max(textWidth, lineInfo.mLineWidth);
         }
 
         // This may happen if one single word is larger than the image width.
@@ -432,21 +547,23 @@
      */
     private void drawText(String text, Locale locale, String languageTag)
             throws IOException, FontFormatException {
-        System.out.println("Encoding \"" + locale + "\" as \"" + languageTag + "\": " + text);
+        LOGGER.info("Encoding \"" + locale + "\" as \"" + languageTag + "\": " + text);
 
         Graphics2D graphics = createGraphics(locale);
         FontMetrics fontMetrics = graphics.getFontMetrics();
-        List<String> wrappedText = wrapText(text, fontMetrics, locale.getLanguage());
+        WrappedTextInfo wrappedTextInfo = wrapText(text, fontMetrics, locale.getLanguage());
 
         // Marks the start y offset for the text image of current locale; and reserves one line to
         // encode the image metadata.
         int currentImageStart = mVerticalOffset;
         mVerticalOffset += 1;
-        for (String line : wrappedText) {
+        for (WrappedTextInfo.LineInfo lineInfo : wrappedTextInfo.mWrappedLines) {
             int lineHeight = fontMetrics.getHeight();
             // Doubles the height of the image if we are short of space.
             if (mVerticalOffset + lineHeight >= mImageHeight) {
                 resize(mImageWidth, mImageHeight * 2);
+                // Recreates the graphics since it's attached to the buffered image.
+                graphics = createGraphics(locale);
             }
 
             // Draws the text at mVerticalOffset and increments the offset with line space.
@@ -455,12 +572,11 @@
             // Draws from right if it's an RTL language.
             int x =
                     mCenterAlignment
-                            ? (mImageWidth - fontMetrics.stringWidth(line)) / 2
+                            ? (mImageWidth - lineInfo.mLineWidth) / 2
                             : RTL_LANGUAGE.contains(languageTag)
-                                    ? mImageWidth - fontMetrics.stringWidth(line)
+                                    ? mImageWidth - lineInfo.mLineWidth
                                     : 0;
-
-            graphics.drawString(line, x, baseLine);
+            graphics.drawString(lineInfo.mLineContent.getIterator(), x, baseLine);
 
             mVerticalOffset += lineHeight;
         }
@@ -502,6 +618,11 @@
      */
     public void generateImage(Map<Locale, String> localizedTextMap, String outputPath)
             throws FontFormatException, IOException {
+        FontMetrics defaultFontMetrics =
+                createGraphics(Locale.forLanguageTag("en")).getFontMetrics();
+        mDefaultFont = defaultFontMetrics.getFont();
+        mAndroidStringWidth = defaultFontMetrics.stringWidth(ANDROID_STRING);
+
         Map<String, Integer> languageCount = new TreeMap<>();
         int textWidth = 0;
         for (Locale locale : localizedTextMap.keySet()) {
@@ -587,6 +708,19 @@
                         .hasArg(false)
                         .create());
 
+        options.addOption(
+                OptionBuilder.withLongOpt("verbose")
+                        .withDescription("Output the logging above info level.")
+                        .hasArg(false)
+                        .create());
+
+        options.addOption(
+                OptionBuilder.withLongOpt("locales")
+                        .withDescription("A list of android locales separated by ',' e.g."
+                                + " 'af,en,zh-rTW'")
+                        .hasArg(true)
+                        .create());
+
         return options;
     }
 
@@ -606,6 +740,12 @@
 
         int imageWidth = Integer.parseUnsignedInt(cmd.getOptionValue("image_width"));
 
+        if (cmd.hasOption("verbose")) {
+            LOGGER.setLevel(Level.INFO);
+        } else {
+            LOGGER.setLevel(Level.WARNING);
+        }
+
         ImageGenerator imageGenerator =
                 new ImageGenerator(
                         imageWidth,
@@ -614,8 +754,16 @@
                         cmd.getOptionValue("font_dir"),
                         cmd.hasOption("center_alignment"));
 
+        Set<String> localesSet = null;
+        if (cmd.hasOption("locales")) {
+            String[] localesList = cmd.getOptionValue("locales").split(",");
+            localesSet = new HashSet<>(Arrays.asList(localesList));
+            // Ensures that we have the default locale, all english translations are identical.
+            localesSet.add("en-rAU");
+        }
         Map<Locale, String> localizedStringMap =
-                imageGenerator.readLocalizedStringFromXmls(cmd.getOptionValue("resource_dir"));
+                imageGenerator.readLocalizedStringFromXmls(cmd.getOptionValue("resource_dir"),
+                        localesSet);
         imageGenerator.generateImage(localizedStringMap, cmd.getOptionValue("output_file"));
     }
 }