From 542c61788350a77f36c38c866a96a04bc19c2e72 Mon Sep 17 00:00:00 2001 From: Tianjie Xu Date: Tue, 13 Nov 2018 16:36:41 -0800 Subject: Handle rendering problem for the word "Android" and punctuations The word "Android" is not translated. As a result, some locales fail to render this word and some punctuations, leading to holes in the middle of the text. In these cases, we will need to fall back to the default font and re-measure the text width. For now, we haven't handled the mix of latin and logogram languages; and we can blacklist the problematic ones first. Bug: 74397117 Test: generate and inspect the image Change-Id: I07d22c0dae2e31eb74f2954e354cd39a42c22f14 --- tools/image_generator/ImageGenerator.java | 158 +++++++++++++++++++++++++----- 1 file changed, 134 insertions(+), 24 deletions(-) diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java index 8730945b5..0a1c85b59 100644 --- a/tools/image_generator/ImageGenerator.java +++ b/tools/image_generator/ImageGenerator.java @@ -32,9 +32,11 @@ import java.awt.FontFormatException; 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; @@ -83,6 +85,20 @@ public class ImageGenerator { // 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 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 +176,72 @@ public class ImageGenerator { } } + /** + * 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 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 +259,7 @@ public class ImageGenerator { mTextName = textName; mFontSize = fontSize; mFontDirPath = fontDirPath; + mLoadedFontMap = new TreeMap<>(); mCenterAlignment = centerAlignment; } @@ -295,12 +378,18 @@ public class ImageGenerator { * @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 +398,53 @@ public class ImageGenerator { } /** Separates the text string by spaces and wraps it by words. */ - private List wrapTextByWords(String text, FontMetrics metrics) { - List 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 wrapTextByCharacters(String text, FontMetrics metrics) { - List 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 +453,10 @@ public class ImageGenerator { * @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 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 +505,11 @@ public class ImageGenerator { throws IOException, FontFormatException { Graphics2D graphics = createGraphics(locale); FontMetrics fontMetrics = graphics.getFontMetrics(); - List 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. @@ -436,17 +540,19 @@ public class ImageGenerator { Graphics2D graphics = createGraphics(locale); FontMetrics fontMetrics = graphics.getFontMetrics(); - List 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 +561,11 @@ public class ImageGenerator { // 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 +607,11 @@ public class ImageGenerator { */ public void generateImage(Map localizedTextMap, String outputPath) throws FontFormatException, IOException { + FontMetrics defaultFontMetrics = + createGraphics(Locale.forLanguageTag("en")).getFontMetrics(); + mDefaultFont = defaultFontMetrics.getFont(); + mAndroidStringWidth = defaultFontMetrics.stringWidth(ANDROID_STRING); + Map languageCount = new TreeMap<>(); int textWidth = 0; for (Locale locale : localizedTextMap.keySet()) { -- cgit v1.2.3 From 7b636b64a1cb2a91fadc2c3ec1e70f697253a505 Mon Sep 17 00:00:00 2001 From: Tianjie Xu Date: Wed, 14 Nov 2018 16:25:42 -0800 Subject: ImageGenerator: add an option to specify the supported locales A smaller list of locales helps to reduce the size of the generated images; and mitigate the additional space requirement on the tight boot image for some devices. The caller of the program will be responsible for providing the locale list. Also add a verbose option and switch to logger. Bug: 74397117 Test: generate and check the image Change-Id: I7b7cee681ccdc6e13fdd0575cf02f8add2ad9a2d --- tools/image_generator/ImageGenerator.java | 46 ++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/tools/image_generator/ImageGenerator.java b/tools/image_generator/ImageGenerator.java index 0a1c85b59..9d882678a 100644 --- a/tools/image_generator/ImageGenerator.java +++ b/tools/image_generator/ImageGenerator.java @@ -46,6 +46,8 @@ import java.util.Map; 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; @@ -59,6 +61,8 @@ public class ImageGenerator { 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; @@ -322,12 +326,13 @@ public class ImageGenerator { * 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 readLocalizedStringFromXmls(String resourcePath) - throws IOException, LocalizedStringNotFoundException { + public Map readLocalizedStringFromXmls(String resourcePath, + Set localesSet) throws IOException, LocalizedStringNotFoundException { File resourceDir = new File(resourcePath); if (!resourceDir.isDirectory()) { throw new LocalizedStringNotFoundException(resourcePath + " is not a directory."); @@ -354,6 +359,12 @@ public class ImageGenerator { 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 { @@ -536,7 +547,7 @@ public class ImageGenerator { */ 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(); @@ -697,6 +708,19 @@ public class ImageGenerator { .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; } @@ -716,6 +740,12 @@ public class ImageGenerator { 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, @@ -724,8 +754,16 @@ public class ImageGenerator { cmd.getOptionValue("font_dir"), cmd.hasOption("center_alignment")); + Set 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 localizedStringMap = - imageGenerator.readLocalizedStringFromXmls(cmd.getOptionValue("resource_dir")); + imageGenerator.readLocalizedStringFromXmls(cmd.getOptionValue("resource_dir"), + localesSet); imageGenerator.generateImage(localizedStringMap, cmd.getOptionValue("output_file")); } } -- cgit v1.2.3