Make the text image readable under recovery

Encode the width, height and locale of the localized image as pixels so
that recovery can locate the correct range of the image from a
concatenated png file.

Also address a few todoes including wrapping the CJK text, making a
catch-all type for all languages.

Test: view the generated image under locale test
Change-Id: Icd3997eb4e992e76ef72526787d64c406f606970
diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java
index b910337..8ed696a 100644
--- a/tools/image_generator/ImageGenerator.java
+++ b/tools/image_generator/ImageGenerator.java
@@ -26,10 +26,12 @@
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Comparator;
+import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
 import java.util.StringTokenizer;
 
@@ -115,6 +117,25 @@
     put("zh", "NotoSansCJK-Regular");
   }};
 
+  // Languages that write from right to left.
+  private static final Set<String> RTL_LANGUAGE = new HashSet<String>() {{
+    add("ar"); // Arabic
+    add("fa"); // Persian
+    add("he"); // Hebrew
+    add("iw"); // Hebrew
+    add("ur"); // Urdu
+  }};
+
+  // Languages that breaks on arbitrary characters.
+  // TODO(xunchang) switch to icu library if possible.
+  private static final Set<String> LOGOGRAM_LANGUAGE = new HashSet<String>() {{
+    add("ja"); // Japanese
+    add("km"); // Khmer
+    add("ko"); // Korean
+    add("lo"); // Lao
+    add("zh"); // Chinese
+  }};
+
   /**
    * Exception to indicate the failure to find the translated text strings.
    */
@@ -220,7 +241,20 @@
     }
 
     Map<Locale, String> result =
-        new TreeMap<Locale, String>(Comparator.comparing(Locale::toLanguageTag));
+        // Overrides the string comparator so that sr is sorted behind sr-Latn. And thus recovery
+        // can find the most relevant locale when going down the list.
+        new TreeMap<>((Locale l1, Locale l2) -> {
+          if (l1.toLanguageTag().equals(l2.toLanguageTag())) {
+            return 0;
+          }
+          if (l1.getLanguage().equals(l2.toLanguageTag())) {
+            return -1;
+          }
+          if (l2.getLanguage().equals(l1.toLanguageTag())) {
+            return 1;
+          }
+          return l1.toLanguageTag().compareTo(l2.toLanguageTag());
+        });
 
     // Find all the localized resource subdirectories in the format of values-$LOCALE
     String[] nameList = resourceDir.list(
@@ -269,6 +303,8 @@
     List<String> wrappedText = new ArrayList<>();
     StringTokenizer st = new StringTokenizer(text, " \n");
 
+    // TODO(xunchang). We assume that all words can fit on the screen. Raise an
+    // IllegalStateException if the word is wider than the image width.
     StringBuilder line = new StringBuilder();
     while (st.hasMoreTokens()) {
       String token = st.nextToken();
@@ -284,6 +320,25 @@
   }
 
   /**
+   * One character is a word for CJK.
+   */
+  private List<String> wrapTextByCharacters(String text, FontMetrics metrics) {
+    List<String> wrappedText = new ArrayList<>();
+
+    StringBuilder line = new StringBuilder();
+    for (char token : text.toCharArray()) {
+      if (metrics.stringWidth(line + Character.toString(token)) > mImageWidth) {
+        wrappedText.add(line.toString());
+        line = new StringBuilder();
+      }
+      line.append(token);
+    }
+    wrappedText.add(line.toString());
+
+    return wrappedText;
+  }
+
+  /**
    * Wraps the text with a maximum of mImageWidth pixels per line.
    *
    * @param text the string representation of text to wrap
@@ -292,15 +347,36 @@
    *
    * @return a list of strings with their width smaller than mImageWidth pixels
    */
-  private List<String> wrapText(String text, FontMetrics metrics) {
-    // TODO(xunchang) handle other cases of text wrapping
-    // 1. RTL languages: "ar"(Arabic), "fa"(Persian), "he"(Hebrew), "iw"(Hebrew), "ur"(Urdu)
-    // 2. Language uses characters: CJK, "lo"(lao), "km"(khmer)
+  private List<String> wrapText(String text, FontMetrics metrics, String language) {
+    if (LOGOGRAM_LANGUAGE.contains(language)) {
+      return wrapTextByCharacters(text, metrics);
+    }
 
     return wrapTextByWords(text, metrics);
   }
 
   /**
+   * Encodes the information of the text image for |locale|.
+   * According to minui/resources.cpp, the width, height and locale of the image is decoded as:
+   *   int w = (row[1] << 8) | row[0];
+   *   int h = (row[3] << 8) | row[2];
+   *   __unused int len = row[4];
+   *   char* loc = reinterpret_cast<char*>(&row[5]);
+  */
+  private List<Integer> encodeTextInfo(int width, int height, String locale) {
+    List<Integer> info = new ArrayList<>(Arrays.asList(width & 0xff, width >> 8,
+        height & 0xff, height >> 8, locale.length()));
+
+    byte[] localeBytes = locale.getBytes();
+    for (byte b: localeBytes) {
+      info.add((int)b);
+    }
+    info.add(0);
+
+    return info;
+  }
+
+  /**
    * Draws the text string on the canvas for given locale.
    *
    * @param text the string to draw on canvas
@@ -309,17 +385,23 @@
    * @throws IOException if we cannot find the corresponding font file for the given locale.
    * @throws FontFormatException if we failed to load the font file for the given locale.
    */
-  private void drawText(String text, Locale locale) throws IOException, FontFormatException  {
+  private void drawText(String text, Locale locale, String languageTag, boolean centralAlignment)
+      throws IOException, FontFormatException  {
     Graphics2D graphics = mBufferedImage.createGraphics();
     graphics.setColor(Color.WHITE);
     graphics.setRenderingHint(
         RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_GASP);
     graphics.setFont(loadFontsByLocale(locale.getLanguage()));
 
-    System.out.println("Drawing text for locale " + locale + " text " + text);
+    System.out.println("Encoding \"" + locale + "\" as \"" + languageTag + "\": " + text);
 
     FontMetrics fontMetrics = graphics.getFontMetrics();
-    List<String> wrappedText = wrapTextByWords(text, fontMetrics);
+    List<String> wrappedText = 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) {
       int lineHeight = fontMetrics.getHeight();
       // Doubles the height of the image if we are short of space.
@@ -329,9 +411,23 @@
 
       // Draws the text at mVerticalOffset and increments the offset with line space.
       int baseLine = mVerticalOffset + lineHeight - fontMetrics.getDescent();
-      graphics.drawString(line, 0, baseLine);
+
+      // Draws from right if it's an RTL language.
+      int x = centralAlignment ? (mImageWidth - fontMetrics.stringWidth(line)) / 2 :
+          RTL_LANGUAGE.contains(languageTag) ? mImageWidth - fontMetrics.stringWidth(line) : 0;
+
+      graphics.drawString(line, x, baseLine);
+
       mVerticalOffset += lineHeight;
     }
+
+    // Encodes the metadata of the current localized image as pixels.
+    int currentImageHeight = mVerticalOffset - currentImageStart - 1;
+    List<Integer> info = encodeTextInfo(mImageWidth, currentImageHeight, languageTag);
+    for (int i = 0; i < info.size(); i++) {
+      int pixel[] =  { info.get(i) };
+      mBufferedImage.getRaster().setPixel(i, currentImageStart, pixel);
+    }
   }
 
   /**
@@ -362,11 +458,24 @@
    */
   public void generateImage(Map<Locale, String> localizedTextMap, String outputPath) throws
       FontFormatException, IOException {
+    Map<String, Integer> languageCount = new TreeMap<>();
     for (Locale locale : localizedTextMap.keySet()) {
-      // TODO(xunchang) reprocess the locales for the same language and make the last locale the
-      // catch-all type. e.g. "zh-CN, zh-HK, zh-TW" will become "zh-CN, zh-HK, zh"
-      // Or maybe we don't need to support these variants?
-      drawText(localizedTextMap.get(locale), locale);
+      String language = locale.getLanguage();
+      languageCount.put(language, languageCount.getOrDefault(language, 0) + 1 );
+    }
+
+    for (Locale locale : localizedTextMap.keySet()) {
+      Integer count = languageCount.get(locale.getLanguage());
+      // Recovery expects en-US instead of en_US.
+      String languageTag = locale.toLanguageTag();
+      if (count == 1) {
+        // Make the last country variant for a given language be the catch-all for that language.
+        languageTag = locale.getLanguage();
+      } else {
+        languageCount.put(locale.getLanguage(), count - 1);
+      }
+
+      drawText(localizedTextMap.get(locale), locale, languageTag, false);
     }
 
     // TODO(xunchang) adjust the width to save some space if all texts are smaller than imageWidth.