Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8043509

java.awt.Font gets initialized with the wrong font name for some Locales

    XMLWordPrintable

Details

    • Bug
    • Resolution: Fixed
    • P4
    • 9
    • 8
    • client-libs
    • 2d
    • b22
    • x86_64
    • windows_7

    Description

      FULL PRODUCT VERSION :
      java version "1.8.0"
      Java(TM) SE Runtime Environment (build 1.8.0-b132)
      Java HotSpot(TM) Client VM (build 25.0-b70, mixed mode, sharing)

      ADDITIONAL OS VERSION INFORMATION :
      Microsoft Windows 7

      EXTRA RELEVANT SYSTEM CONFIGURATION :
      Installed "Verdana" font family in the Windows/Fonts directory:
      verdana.ttf ("Verdana", ver. 5.05)
      verdanab.ttf ("Verdana Bold", ver. 5.03)
      verdanai.ttf ("Verdana Italic", ver. 5.03)
      verdanaz.ttf ("Verdana Bold Italic", ver. 5.03)

      A DESCRIPTION OF THE PROBLEM :
      When a java.awt.Font is created for a startup Locale, whose Microsoft LCID does not exist in the corresponding font file, then it's font name gets initialized to the default English font name, even if there exists another language compatible LCID inside the font file. The reason for this bug lies inside the implementation of the class sun.font.TrueTypeFont. A possible fix is added to the end of this report.

      For example the Font "verdanab.ttf" ("Verdana Bold", ver. 5.03) does not include an LCID for the "de_AT" Locale (Austria), but it does include an LCID for "de_GE" (Germany). Since the language of both locales is the same ("de"), a Font created for the "de_AT" Locale should by default fall back to the existing German LCID font name ("Verdana Fett"), after no Austrian LCID name is found. But instead the Font falls back to the name of the English locale ("Verdana Bold").

      Please note, that this bug is also one of the reasons for bug JDK-7083197: sun.awt.Win32FontManager initializes its font names from GDI, which seems to fall back on any existing language compatible names if possible (e.g. it falls back to the German name, if the Austrian LCID does not exist in the font). Now, if there is a discrepancy between the GDI font names and the font names provided by the Windows registry, sun.font.SunFontManager#resolveWindowsFonts() will try to resolve this discrepancy by getting the actual font name from temporarily instantiated TrueTypeFont classes. But since TrueTypeFont does fall back on the English font name instead of the German font name, the corresponding fonts will never get resolved, although they should. (There are more reasons for bug JDK-7083197, but I'll skip them in this description and open a up another bug for them).

      ---------------------------------
      PROPOSED SOLUTION
      ---------------------------------

      The following implementation of sun.font.TrueTypeFont includes a fix for this bug. The main bug fix is inside the method TrueTypeFont#initNames(). Other than that there are some new supporting methods and fields appended to the end of the class. All changes to the original TrueTypeFont source code have been marked with "XXX" tags. Feel free to use this source code as you see fit (I do hereby abandon any possible copyrights on this source).

      Source Code for sun.font.TrueTypeFont.java:

      import java.awt.Font;
      import java.awt.FontFormatException;
      import java.awt.GraphicsEnvironment;
      import java.awt.geom.Point2D;
      import java.io.FileNotFoundException;
      import java.io.IOException;
      import java.io.RandomAccessFile;
      import java.io.UnsupportedEncodingException;
      import java.nio.ByteBuffer;
      import java.nio.CharBuffer;
      import java.nio.IntBuffer;
      import java.nio.ShortBuffer;
      import java.nio.channels.ClosedChannelException;
      import java.nio.channels.FileChannel;
      import java.util.ArrayList;
      import java.util.HashMap;
      import java.util.HashSet;
      import java.util.List;
      import java.util.Locale;
      import java.util.Map;
      import java.util.Map.Entry;

      import sun.java2d.Disposer;
      import sun.java2d.DisposerRecord;



      /**
       * TrueTypeFont is not called SFntFont because it is not expected
       * to handle all types that may be housed in a such a font file.
       * If additional types are supported later, it may make sense to
       * create an SFnt superclass. Eg to handle sfnt-housed postscript fonts.
       * OpenType fonts are handled by this class, and possibly should be
       * represented by a subclass.
       * An instance stores some information from the font file to faciliate
       * faster access. File size, the table directory and the names of the font
       * are the most important of these. It amounts to approx 400 bytes
       * for a typical font. Systems with mutiple locales sometimes have up to 400
       * font files, and an app which loads all font files would need around
       * 160Kbytes. So storing any more info than this would be expensive.
       */
      public class TrueTypeFont extends FileFont {

         /* -- Tags for required TrueType tables */
          public static final int cmapTag = 0x636D6170; // 'cmap'
          public static final int glyfTag = 0x676C7966; // 'glyf'
          public static final int headTag = 0x68656164; // 'head'
          public static final int hheaTag = 0x68686561; // 'hhea'
          public static final int hmtxTag = 0x686D7478; // 'hmtx'
          public static final int locaTag = 0x6C6F6361; // 'loca'
          public static final int maxpTag = 0x6D617870; // 'maxp'
          public static final int nameTag = 0x6E616D65; // 'name'
          public static final int postTag = 0x706F7374; // 'post'
          public static final int os_2Tag = 0x4F532F32; // 'OS/2'

          /* -- Tags for opentype related tables */
          public static final int GDEFTag = 0x47444546; // 'GDEF'
          public static final int GPOSTag = 0x47504F53; // 'GPOS'
          public static final int GSUBTag = 0x47535542; // 'GSUB'
          public static final int mortTag = 0x6D6F7274; // 'mort'

          /* -- Tags for non-standard tables */
          public static final int fdscTag = 0x66647363; // 'fdsc' - gxFont descriptor
          public static final int fvarTag = 0x66766172; // 'fvar' - gxFont variations
          public static final int featTag = 0x66656174; // 'feat' - layout features
          public static final int EBLCTag = 0x45424C43; // 'EBLC' - embedded bitmaps
          public static final int gaspTag = 0x67617370; // 'gasp' - hint/smooth sizes

          /* -- Other tags */
          public static final int ttcfTag = 0x74746366; // 'ttcf' - TTC file
          public static final int v1ttTag = 0x00010000; // 'v1tt' - Version 1 TT font
          public static final int trueTag = 0x74727565; // 'true' - Version 2 TT font
          public static final int ottoTag = 0x4f54544f; // 'otto' - OpenType font

          /* -- ID's used in the 'name' table */
          public static final int MS_PLATFORM_ID = 3;
          /* MS locale id for US English is the "default" */
          public static final short ENGLISH_LOCALE_ID = 0x0409; // 1033 decimal
          public static final int FAMILY_NAME_ID = 1;
          // public static final int STYLE_WEIGHT_ID = 2; // currently unused.
          public static final int FULL_NAME_ID = 4;
          public static final int POSTSCRIPT_NAME_ID = 6;

          private static final short US_LCID = 0x0409; // US English - default

          private static Map<String, Short> lcidMap;

          class DirectoryEntry {
              int tag;
              int offset;
              int length;
          }

          /* There is a pool which limits the number of fd's that are in
           * use. Normally fd's are closed as they are replaced in the pool.
           * But if an instance of this class becomes unreferenced, then there
           * needs to be a way to close the fd. A finalize() method could do this,
           * but using the Disposer class will ensure its called in a more timely
           * manner. This is not something which should be relied upon to free
           * fd's - its a safeguard.
           */
          private static class TTDisposerRecord implements DisposerRecord {

              FileChannel channel = null;

              public synchronized void dispose() {
                  try {
                      if (channel != null) {
                          channel.close();
                      }
                  } catch (IOException e) {
                  } finally {
                      channel = null;
                  }
              }
          }

          TTDisposerRecord disposerRecord = new TTDisposerRecord();

          /* > 0 only if this font is a part of a collection */
          int fontIndex = 0;

          /* Number of fonts in this collection. ==1 if not a collection */
          int directoryCount = 1;

          /* offset in file of table directory for this font */
          int directoryOffset; // 12 if its not a collection.

          /* number of table entries in the directory/offsets table */
          int numTables;

          /* The contents of the the directory/offsets table */
          DirectoryEntry []tableDirectory;

      // protected byte []gposTable = null;
      // protected byte []gdefTable = null;
      // protected byte []gsubTable = null;
      // protected byte []mortTable = null;
      // protected boolean hintsTabledChecked = false;
      // protected boolean containsHintsTable = false;

          /* These fields are set from os/2 table info. */
          private boolean supportsJA;
          private boolean supportsCJK;

          /* These are for faster access to the name of the font as
           * typically exposed via API to applications.
           */
          private Locale nameLocale;
          private String localeFamilyName;
          private String localeFullName;

          /**
           * - does basic verification of the file
           * - reads the header table for this font (within a collection)
           * - reads the names (full, family).
           * - determines the style of the font.
           * - initializes the CMAP
           * @throws FontFormatException - if the font can't be opened
           * or fails verification, or there's no usable cmap
           */
          public TrueTypeFont(String platname, Object nativeNames, int fIndex,
                       boolean javaRasterizer)
              throws FontFormatException {
              super(platname, nativeNames);
              useJavaRasterizer = javaRasterizer;
              fontRank = Font2D.TTF_RANK;
              try {
                  verify();
                  init(fIndex);
              } catch (Throwable t) {
                  close();
                  if (t instanceof FontFormatException) {
                      throw (FontFormatException)t;
                  } else {
                      throw new FontFormatException("Unexpected runtime exception.");
                  }
              }
              Disposer.addObjectRecord(this, disposerRecord);
          }

          /* Enable natives just for fonts picked up from the platform that
           * may have external bitmaps on Solaris. Could do this just for
           * the fonts that are specified in font configuration files which
           * would lighten the burden (think about that).
           * The EBLCTag is used to skip natives for fonts that contain embedded
           * bitmaps as there's no need to use X11 for those fonts.
           * Skip all the latin fonts as they don't need this treatment.
           * Further refine this to fonts that are natively accessible (ie
           * as PCF bitmap fonts on the X11 font path).
           * This method is called when creating the first strike for this font.
           */
          @Override
          protected boolean checkUseNatives() {
              if (checkedNatives) {
                  return useNatives;
              }
              if (!FontUtilities.isSolaris || useJavaRasterizer ||
                  FontUtilities.useT2K || nativeNames == null ||
                  getDirectoryEntry(EBLCTag) != null ||
                  GraphicsEnvironment.isHeadless()) {
                  checkedNatives = true;
                  return false; /* useNatives is false */
              } else if (nativeNames instanceof String) {
                  String name = (String)nativeNames;
                  /* Don't do do this for Latin fonts */
                  if (name.indexOf("8859") > 0) {
                      checkedNatives = true;
                      return false;
                  } else if (NativeFont.hasExternalBitmaps(name)) {
                      nativeFonts = new NativeFont[1];
                      try {
                          nativeFonts[0] = new NativeFont(name, true);
                          /* If reach here we have an non-latin font that has
                           * external bitmaps and we successfully created it.
                           */
                          useNatives = true;
                      } catch (FontFormatException e) {
                          nativeFonts = null;
                      }
                  }
              } else if (nativeNames instanceof String[]) {
                  String[] natNames = (String[])nativeNames;
                  int numNames = natNames.length;
                  boolean externalBitmaps = false;
                  for (int nn = 0; nn < numNames; nn++) {
                      if (natNames[nn].indexOf("8859") > 0) {
                          checkedNatives = true;
                          return false;
                      } else if (NativeFont.hasExternalBitmaps(natNames[nn])) {
                          externalBitmaps = true;
                      }
                  }
                  if (!externalBitmaps) {
                      checkedNatives = true;
                      return false;
                  }
                  useNatives = true;
                  nativeFonts = new NativeFont[numNames];
                  for (int nn = 0; nn < numNames; nn++) {
                      try {
                          nativeFonts[nn] = new NativeFont(natNames[nn], true);
                      } catch (FontFormatException e) {
                          useNatives = false;
                          nativeFonts = null;
                      }
                  }
              }
              if (useNatives) {
                  glyphToCharMap = new char[getMapper().getNumGlyphs()];
              }
              checkedNatives = true;
              return useNatives;
          }


          /* This is intended to be called, and the returned value used,
           * from within a block synchronized on this font object.
           * ie the channel returned may be nulled out at any time by "close()"
           * unless the caller holds a lock.
           * Deadlock warning: FontManager.addToPool(..) acquires a global lock,
           * which means nested locks may be in effect.
           */
          private synchronized FileChannel open() throws FontFormatException {
              if (disposerRecord.channel == null) {
                  if (FontUtilities.isLogging()) {
                      FontUtilities.getLogger().info("open TTF: " + platName);
                  }
                  try {
                    
      RandomAccessFile raf = (RandomAccessFile)
                      java.security.AccessController.doPrivileged(
                          new java.security.PrivilegedAction() {
                              public Object run() {
                                  try {
                                      return new RandomAccessFile(platName, "r");
                                  } catch (FileNotFoundException ffne) {
                                  }
                                  return null;
                          }
                      });
                      disposerRecord.channel = raf.getChannel();
                      fileSize = (int)disposerRecord.channel.size();
                      FontManager fm = FontManagerFactory.getInstance();
                      if (fm instanceof SunFontManager) {
                          ((SunFontManager) fm).addToPool(this);
                      }
                  } catch (NullPointerException e) {
                      close();
                      throw new FontFormatException(e.toString());
                  } catch (ClosedChannelException e) {
                      /* NIO I/O is interruptible, recurse to retry operation.
                       * The call to channel.size() above can throw this exception.
                       * Clear interrupts before recursing in case NIO didn't.
                       * Note that close() sets disposerRecord.channel to null.
                       */
                      Thread.interrupted();
                      close();
                      open();
                  } catch (IOException e) {
                      close();
                      throw new FontFormatException(e.toString());
                  }
              }
              return disposerRecord.channel;
          }

          protected synchronized void close() {
              disposerRecord.dispose();
          }


          int readBlock(ByteBuffer buffer, int offset, int length) {
              int bread = 0;
              try {
                  synchronized (this) {
                      if (disposerRecord.channel == null) {
                          open();
                      }
                      if (offset + length > fileSize) {
                          if (offset >= fileSize) {
                              /* Since the caller ensures that offset is < fileSize
                               * this condition suggests that fileSize is now
                               * different than the value we originally provided
                               * to native when the scaler was created.
                               * Also fileSize is updated every time we
                               * open() the file here, but in native the value
                               * isn't updated. If the file has changed whilst we
                               * are executing we want to bail, not spin.
                               */
                              if (FontUtilities.isLogging()) {
                                  String msg = "Read offset is " + offset +
                                      " file size is " + fileSize+
                                      " file is " + platName;
                                  FontUtilities.getLogger().severe(msg);
                              }
                              return -1;
                          } else {
                              length = fileSize - offset;
                          }
                      }
                      buffer.clear();
                      disposerRecord.channel.position(offset);
                      while (bread < length) {
                          int cnt = disposerRecord.channel.read(buffer);
                          if (cnt == -1) {
                              String msg = "Unexpected EOF " + this;
                              int currSize = (int)disposerRecord.channel.size();
                              if (currSize != fileSize) {
                                  msg += " File size was " + fileSize +
                                      " and now is " + currSize;
                              }
                              if (FontUtilities.isLogging()) {
                                  FontUtilities.getLogger().severe(msg);
                              }
                              // We could still flip() the buffer here because
                              // it's possible that we did read some data in
                              // an earlier loop, and we probably should
                              // return that to the caller. Although if
                              // the caller expected 8K of data and we return
                              // only a few bytes then maybe it's better instead to
                              // set bread = -1 to indicate failure.
                              // The following is therefore using arbitrary values
                              // but is meant to allow cases where enough
                              // data was read to probably continue.
                              if (bread > length/2 || bread > 16384) {
                                  buffer.flip();
                                  if (FontUtilities.isLogging()) {
                                      msg = "Returning " + bread +
                                          " bytes instead of " + length;
                                      FontUtilities.getLogger().severe(msg);
                                  }
                              } else {
                                  bread = -1;
                              }
                              throw new IOException(msg);
                          }
                          bread += cnt;
                      }
                      buffer.flip();
                      if (bread > length) { // possible if buffer.size() > length
                          bread = length;
                      }
                  }
              } catch (FontFormatException e) {
                  if (FontUtilities.isLogging()) {
                      FontUtilities.getLogger().severe(
                                             "While reading " + platName, e);
                  }
                  bread = -1; // signal EOF
                  deregisterFontAndClearStrikeCache();
              } catch (ClosedChannelException e) {
                  /* NIO I/O is interruptible, recurse to retry operation.
                   * Clear interrupts before recursing in case NIO didn't.
                   */
                  Thread.interrupted();
                  close();
                  return readBlock(buffer, offset, length);
              } catch (IOException e) {
                  /* If we did not read any bytes at all and the exception is
                   * not a recoverable one (ie is not ClosedChannelException) then
                   * we should indicate that there is no point in re-trying.
                   * Other than an attempt to read past the end of the file it
                   * seems unlikely this would occur as problems opening the
                   * file are handled as a FontFormatException.
                   */
                  if (FontUtilities.isLogging()) {
                      FontUtilities.getLogger().severe(
                                             "While reading " + platName, e);
                  }
                  if (bread == 0) {
                      bread = -1; // signal EOF
                      deregisterFontAndClearStrikeCache();
                  }
              }
              return bread;
          }

          ByteBuffer readBlock(int offset, int length) {

              ByteBuffer buffer = ByteBuffer.allocate(length);
              try {
                  synchronized (this) {
                      if (disposerRecord.channel == null) {
                          open();
                      }
                      if (offset + length > fileSize) {
                          if (offset > fileSize) {
                              return null; // assert?
                          } else {
                              buffer = ByteBuffer.allocate(fileSize-offset);
                          }
                      }
                      disposerRecord.channel.position(offset);
                      disposerRecord.channel.read(buffer);
                      buffer.flip();
                  }
              } catch (FontFormatException e) {
                  return null;
              } catch (ClosedChannelException e) {
                  /* NIO I/O is interruptible, recurse to retry operation.
                   * Clear interrupts before recursing in case NIO didn't.
                   */
                  Thread.interrupted();
                  close();
                  readBlock(buffer, offset, length);
              } catch (IOException e) {
                  return null;
              }
              return buffer;
          }

          /* This is used by native code which can't allocate a direct byte
           * buffer because of bug 4845371. It, and references to it in native
           * code in scalerMethods.c can be removed once that bug is fixed.
           * 4845371 is now fixed but we'll keep this around as it doesn't cost
           * us anything if its never used/called.
           */
          byte[] readBytes(int offset, int length) {
              ByteBuffer buffer = readBlock(offset, length);
              if (buffer.hasArray()) {
                  return buffer.array();
              } else {
                  byte[] bufferBytes = new byte[buffer.limit()];
                  buffer.get(bufferBytes);
                  return bufferBytes;
              }
          }

          private void verify() throws FontFormatException {
              open();
          }

          /* sizes, in bytes, of TT/TTC header records */
          private static final int TTCHEADERSIZE = 12;
          private static final int DIRECTORYHEADERSIZE = 12;
          private static final int DIRECTORYENTRYSIZE = 16;

          protected void init(int fIndex) throws FontFormatException {
              int headerOffset = 0;
              ByteBuffer buffer = readBlock(0, TTCHEADERSIZE);
              try {
                  switch (buffer.getInt()) {

                  case ttcfTag:
                      buffer.getInt(); // skip TTC version ID
                      directoryCount = buffer.getInt();
                      if (fIndex >= directoryCount) {
                          throw new FontFormatException("Bad collection index");
                      }
                      fontIndex = fIndex;
                      buffer = readBlock(TTCHEADERSIZE+4*fIndex, 4);
                      headerOffset = buffer.getInt();
                      break;

                  case v1ttTag:
                  case trueTag:
                  case ottoTag:
                      break;

                  default:
                      throw new FontFormatException("Unsupported sfnt " +
                                                    getPublicFileName());
                  }

                  /* Now have the offset of this TT font (possibly within a TTC)
                   * After the TT version/scaler type field, is the short
                   * representing the number of tables in the table directory.
                   * The table directory begins at 12 bytes after the header.
                   * Each table entry is 16 bytes long (4 32-bit ints)
                   */
                  buffer = readBlock(headerOffset+4, 2);
                  numTables = buffer.getShort();
                  directoryOffset = headerOffset+DIRECTORYHEADERSIZE;
                  ByteBuffer bbuffer = readBlock(directoryOffset,
                                                 numTables*DIRECTORYENTRYSIZE);
                  IntBuffer ibuffer = bbuffer.asIntBuffer();
                  DirectoryEntry table;
                  tableDirectory = new DirectoryEntry[numTables];
                  for (int i=0; i<numTables;i++) {
                      tableDirectory[i] = table = new DirectoryEntry();
                      table.tag = ibuffer.get();
                      /* checksum */ ibuffer.get();
                      table.offset = ibuffer.get();
                      table.length = ibuffer.get();
                      if (table.offset + table.length > fileSize) {
                          throw new FontFormatException("bad table, tag="+table.tag);
                      }
                  }
                  initNames();
              } catch (Exception e) {
                  if (FontUtilities.isLogging()) {
                      FontUtilities.getLogger().severe(e.toString());
                  }
                  if (e instanceof FontFormatException) {
                      throw (FontFormatException)e;
                  } else {
                      throw new FontFormatException(e.toString());
                  }
              }
              if (familyName == null || fullName == null) {
                  throw new FontFormatException("Font name not found");
              }
              /* The os2_Table is needed to gather some info, but we don't
               * want to keep it around (as a field) so obtain it once and
               * pass it to the code that needs it.
               */
              ByteBuffer os2_Table = getTableBuffer(os_2Tag);
              setStyle(os2_Table);
              setCJKSupport(os2_Table);
          }

          /* The array index corresponds to a bit offset in the TrueType
           * font's OS/2 compatibility table's code page ranges fields.
           * These are two 32 bit unsigned int fields at offsets 78 and 82.
           * We are only interested in determining if the font supports
           * the windows encodings we expect as the default encoding in
           * supported locales, so we only map the first of these fields.
           */
          static final String encoding_mapping[] = {
              "cp1252", /* 0:Latin 1 */
              "cp1250", /* 1:Latin 2 */
              "cp1251", /* 2:Cyrillic */
              "cp1253", /* 3:Greek */
              "cp1254", /* 4:Turkish/Latin 5 */
              "cp1255", /* 5:Hebrew */
              "cp1256", /* 6:Arabic */
              "cp1257", /* 7:Windows Baltic */
              "", /* 8:reserved for alternate ANSI */
              "", /* 9:reserved for alternate ANSI */
              "", /* 10:reserved for alternate ANSI */
              "", /* 11:reserved for alternate ANSI */
              "", /* 12:reserved for alternate ANSI */
              "", /* 13:reserved for alternate ANSI */
              "", /* 14:reserved for alternate ANSI */
              "", /* 15:reserved for alternate ANSI */
              "ms874", /* 16:Thai */
              "ms932", /* 17:JIS/Japanese */
              "gbk", /* 18:PRC GBK Cp950 */
              "ms949", /* 19:Korean Extended Wansung */
              "ms950", /* 20:Chinese (Taiwan, Hongkong, Macau) */
              "ms1361", /* 21:Korean Johab */
              "", /* 22 */
              "", /* 23 */
              "", /* 24 */
              "", /* 25 */
              "", /* 26 */
              "", /* 27 */
              "", /* 28 */
              "", /* 29 */
              "", /* 30 */
              "", /* 31 */
          };

          /* This maps two letter language codes to a Windows code page.
           * Note that eg Cp1252 (the first subarray) is not exactly the same as
           * Latin-1 since Windows code pages are do not necessarily correspond.
           * There are two codepages for zh and ko so if a font supports
           * only one of these ranges then we need to distinguish based on
           * country. So far this only seems to matter for zh.
           * REMIND: Unicode locales such as Hindi do not have a code page so
           * this whole mechansim needs to be revised to map languages to
           * the Unicode ranges either when this fails, or as an additional
           * validating test. Basing it on Unicode ranges should get us away
           * from needing to map to this small and incomplete set of Windows
           * code pages which looks odd on non-Windows platforms.
           */
          private static final String languages[][] = {

              /* cp1252/Latin 1 */
              { "en", "ca", "da", "de", "es", "fi", "fr", "is", "it",
                "nl", "no", "pt", "sq", "sv", },

               /* cp1250/Latin2 */
              { "cs", "cz", "et", "hr", "hu", "nr", "pl", "ro", "sk",
                "sl", "sq", "sr", },

              /* cp1251/Cyrillic */
              { "bg", "mk", "ru", "sh", "uk" },

              /* cp1253/Greek*/
              { "el" },

               /* cp1254/Turkish,Latin 5 */
              { "tr" },

               /* cp1255/Hebrew */
              { "he" },

              /* cp1256/Arabic */
              { "ar" },

               /* cp1257/Windows Baltic */
              { "et", "lt", "lv" },

              /* ms874/Thai */
              { "th" },

               /* ms932/Japanese */
              { "ja" },

              /* gbk/Chinese (PRC GBK Cp950) */
              { "zh", "zh_CN", },

              /* ms949/Korean Extended Wansung */
              { "ko" },

              /* ms950/Chinese (Taiwan, Hongkong, Macau) */
              { "zh_HK", "zh_TW", },

              /* ms1361/Korean Johab */
              { "ko" },
          };

          private static final String codePages[] = {
              "cp1252",
              "cp1250",
              "cp1251",
              "cp1253",
              "cp1254",
              "cp1255",
              "cp1256",
              "cp1257",
              "ms874",
              "ms932",
              "gbk",
              "ms949",
              "ms950",
              "ms1361",
          };

          private static String defaultCodePage = null;
          static String getCodePage() {

              if (defaultCodePage != null) {
                  return defaultCodePage;
              }

              if (FontUtilities.isWindows) {
                  defaultCodePage =
                      (String)java.security.AccessController.doPrivileged(
                         new sun.security.action.GetPropertyAction("file.encoding"));
              } else {
                  if (languages.length != codePages.length) {
                      throw new InternalError("wrong code pages array length");
                  }
                  Locale locale = sun.awt.SunToolkit.getStartupLocale();

                  String language = locale.getLanguage();
                  if (language != null) {
                      if (language.equals("zh")) {
                          String country = locale.getCountry();
                          if (country != null) {
                              language = language + "_" + country;
                          }
                      }
                      for (int i=0; i<languages.length;i++) {
                          for (int l=0;l<languages[i].length; l++) {
                              if (language.equals(languages[i][l])) {
                                  defaultCodePage = codePages[i];
                                  return defaultCodePage;
                              }
                          }
                      }
                  }
              }
              if (defaultCodePage == null) {
                  defaultCodePage = "";
              }
              return defaultCodePage;
          }

          /* Theoretically, reserved bits must not be set, include symbol bits */
          public static final int reserved_bits1 = 0x80000000;
          public static final int reserved_bits2 = 0x0000ffff;
          @Override
          boolean supportsEncoding(String encoding) {
              if (encoding == null) {
                  encoding = getCodePage();
              }
              if ("".equals(encoding)) {
                  return false;
              }

              encoding = encoding.toLowerCase();

              /* java_props_md.c has a couple of special cases
               * if language packs are installed. In these encodings the
               * fontconfig files pick up different fonts :
               * SimSun-18030 and MingLiU_HKSCS. Since these fonts will
               * indicate they support the base encoding, we need to rewrite
               * these encodings here before checking the map/array.
               */
              if (encoding.equals("gb18030")) {
                  encoding = "gbk";
              } else if (encoding.equals("ms950_hkscs")) {
                  encoding = "ms950";
              }

              ByteBuffer buffer = getTableBuffer(os_2Tag);
              /* required info is at offsets 78 and 82 */
              if (buffer == null || buffer.capacity() < 86) {
                  return false;
              }

              int range1 = buffer.getInt(78); /* ulCodePageRange1 */
              int range2 = buffer.getInt(82); /* ulCodePageRange2 */

              /* This test is too stringent for Arial on Solaris (and perhaps
               * other fonts). Arial has at least one reserved bit set for an
               * unknown reason.
               */
      // if (((range1 & reserved_bits1) | (range2 & reserved_bits2)) != 0) {
      // return false;
      // }

              for (int em=0; em<encoding_mapping.length; em++) {
                  if (encoding_mapping[em].equals(encoding)) {
                      if (((1 << em) & range1) != 0) {
                          return true;
                      }
                  }
              }
              return false;
          }


          /* Use info in the os_2Table to test CJK support */
          private void setCJKSupport(ByteBuffer os2Table) {
              /* required info is in ulong at offset 46 */
              if (os2Table == null || os2Table.capacity() < 50) {
                  return;
              }
              int range2 = os2Table.getInt(46); /* ulUnicodeRange2 */

              /* Any of these bits set in the 32-63 range indicate a font with
               * support for a CJK range. We aren't looking at some other bits
               * in the 64-69 range such as half width forms as its unlikely a font
               * would include those and none of these.
               */
              supportsCJK = ((range2 & 0x29bf0000) != 0);

              /* This should be generalised, but for now just need to know if
               * Hiragana or Katakana ranges are supported by the font.
               * In the 4 longs representing unicode ranges supported
               * bits 49 & 50 indicate hiragana and katakana
               * This is bits 17 & 18 in the 2nd ulong. If either is supported
               * we presume this is a JA font.
               */
              supportsJA = ((range2 & 0x60000) != 0);
          }

          boolean supportsJA() {
              return supportsJA;
          }

           ByteBuffer getTableBuffer(int tag) {
              DirectoryEntry entry = null;

              for (int i=0;i<numTables;i++) {
                  if (tableDirectory[i].tag == tag) {
                      entry = tableDirectory[i];
                      break;
                  }
              }
              if (entry == null || entry.length == 0 ||
                  entry.offset+entry.length > fileSize) {
                  return null;
              }

              int bread = 0;
              ByteBuffer buffer = ByteBuffer.allocate(entry.length);
              synchronized (this) {
                  try {
                      if (disposerRecord.channel == null) {
                          open();
                      }
                      disposerRecord.channel.position(entry.offset);
                      bread = disposerRecord.channel.read(buffer);
                      buffer.flip();
                  } catch (ClosedChannelException e) {
                      /* NIO I/O is interruptible, recurse to retry operation.
                       * Clear interrupts before recursing in case NIO didn't.
                       */
                      Thread.interrupted();
                      close();
                      return getTableBuffer(tag);
                  } catch (IOException e) {
                      return null;
                  } catch (FontFormatException e) {
                      return null;
                  }

                  if (bread < entry.length) {
                      return null;
                  } else {
                      return buffer;
                  }
              }
          }

          /* NB: is it better to move declaration to Font2D? */
          long getLayoutTableCache() {
              try {
                return getScaler().getLayoutTableCache();
              } catch(FontScalerException fe) {
                  return 0L;
              }
          }

          @Override
          byte[] getTableBytes(int tag) {
              ByteBuffer buffer = getTableBuffer(tag);
              if (buffer == null) {
                  return null;
              } else if (buffer.hasArray()) {
                  try {
                      return buffer.array();
                  } catch (Exception re) {
                  }
              }
              byte []data = new byte[getTableSize(tag)];
              buffer.get(data);
              return data;
          }

          int getTableSize(int tag) {
              for (int i=0;i<numTables;i++) {
                  if (tableDirectory[i].tag == tag) {
                      return tableDirectory[i].length;
                  }
              }
              return 0;
          }

          int getTableOffset(int tag) {
              for (int i=0;i<numTables;i++) {
                  if (tableDirectory[i].tag == tag) {
                      return tableDirectory[i].offset;
                  }
              }
              return 0;
          }

          DirectoryEntry getDirectoryEntry(int tag) {
              for (int i=0;i<numTables;i++) {
                  if (tableDirectory[i].tag == tag) {
                      return tableDirectory[i];
                  }
              }
              return null;
          }

          /* Used to determine if this size has embedded bitmaps, which
           * for CJK fonts should be used in preference to LCD glyphs.
           */
          boolean useEmbeddedBitmapsForSize(int ptSize) {
              if (!supportsCJK) {
                  return false;
              }
              if (getDirectoryEntry(EBLCTag) == null) {
                  return false;
              }
              ByteBuffer eblcTable = getTableBuffer(EBLCTag);
              int numSizes = eblcTable.getInt(4);
              /* The bitmapSizeTable's start at offset of 8.
               * Each bitmapSizeTable entry is 48 bytes.
               * The offset of ppemY in the entry is 45.
               */
              for (int i=0;i<numSizes;i++) {
                  int ppemY = eblcTable.get(8+(i*48)+45) &0xff;
                  if (ppemY == ptSize) {
                      return true;
                  }
              }
              return false;
          }

          public String getFullName() {
              return fullName;
          }

          /* This probably won't get called but is there to support the
           * contract() of setStyle() defined in the superclass.
           */
          @Override
          protected void setStyle() {
              setStyle(getTableBuffer(os_2Tag));
          }

          /* TrueTypeFont can use the fsSelection fields of OS/2 table
           * to determine the style. In the unlikely case that doesn't exist,
           * can use macStyle in the 'head' table but simpler to
           * fall back to super class algorithm of looking for well known string.
           * A very few fonts don't specify this information, but I only
           * came across one: Lucida Sans Thai Typewriter Oblique in
           * /usr/openwin/lib/locale/th_TH/X11/fonts/TrueType/lucidai.ttf
           * that explicitly specified the wrong value. It says its regular.
           * I didn't find any fonts that were inconsistent (ie regular plus some
           * other value).
           */
          private static final int fsSelectionItalicBit = 0x00001;
          private static final int fsSelectionBoldBit = 0x00020;
          private static final int fsSelectionRegularBit = 0x00040;
          private void setStyle(ByteBuffer os_2Table) {
              /* fsSelection is unsigned short at buffer offset 62 */
              if (os_2Table == null || os_2Table.capacity() < 64) {
                  super.setStyle();
                  return;
              }
              int fsSelection = os_2Table.getChar(62) & 0xffff;
              int italic = fsSelection & fsSelectionItalicBit;
              int bold = fsSelection & fsSelectionBoldBit;
              int regular = fsSelection & fsSelectionRegularBit;
      // System.out.println("platname="+platName+" font="+fullName+
      // " family="+familyName+
      // " R="+regular+" I="+italic+" B="+bold);
              if (regular!=0 && ((italic|bold)!=0)) {
                  /* This is inconsistent. Try using the font name algorithm */
                  super.setStyle();
                  return;
              } else if ((regular|italic|bold) == 0) {
                  /* No style specified. Try using the font name algorithm */
                  super.setStyle();
                  return;
              }
              switch (bold|italic) {
              case fsSelectionItalicBit:
                  style = Font.ITALIC;
                  break;
              case fsSelectionBoldBit:
                  if (FontUtilities.isSolaris && platName.endsWith("HG-GothicB.ttf")) {
                      /* Workaround for Solaris's use of a JA font that's marked as
                       * being designed bold, but is used as a PLAIN font.
                       */
                      style = Font.PLAIN;
                  } else {
                      style = Font.BOLD;
                  }
                  break;
              case fsSelectionBoldBit|fsSelectionItalicBit:
                  style = Font.BOLD|Font.ITALIC;
              }
          }

          private float stSize, stPos, ulSize, ulPos;

          private void setStrikethroughMetrics(ByteBuffer os_2Table, int upem) {
              if (os_2Table == null || os_2Table.capacity() < 30 || upem < 0) {
                  stSize = .05f;
                  stPos = -.4f;
                  return;
              }
              ShortBuffer sb = os_2Table.asShortBuffer();
              stSize = sb.get(13) / (float)upem;
              stPos = -sb.get(14) / (float)upem;
          }

          private void setUnderlineMetrics(ByteBuffer postTable, int upem) {
              if (postTable == null || postTable.capacity() < 12 || upem < 0) {
                  ulSize = .05f;
                  ulPos = .1f;
                  return;
              }
              ShortBuffer sb = postTable.asShortBuffer();
              ulSize = sb.get(5) / (float)upem;
              ulPos = -sb.get(4) / (float)upem;
          }

          @Override
          public void getStyleMetrics(float pointSize, float[] metrics, int offset) {

              if (ulSize == 0f && ulPos == 0f) {

                  ByteBuffer head_Table = getTableBuffer(headTag);
                  int upem = -1;
                  if (head_Table != null && head_Table.capacity() >= 18) {
                      ShortBuffer sb = head_Table.asShortBuffer();
                      upem = sb.get(9) & 0xffff;
                  }

                  ByteBuffer os2_Table = getTableBuffer(os_2Tag);
                  setStrikethroughMetrics(os2_Table, upem);

                  ByteBuffer post_Table = getTableBuffer(postTag);
                  setUnderlineMetrics(post_Table, upem);
              }

              metrics[offset] = stPos * pointSize;
              metrics[offset+1] = stSize * pointSize;

              metrics[offset+2] = ulPos * pointSize;
              metrics[offset+3] = ulSize * pointSize;
          }

          private String makeString(byte[] bytes, int len, short encoding) {

              /* Check for fonts using encodings 2->6 is just for
               * some old DBCS fonts, apparently mostly on Solaris.
               * Some of these fonts encode ascii names as double-byte characters.
               * ie with a leading zero byte for what properly should be a
               * single byte-char.
               */
              if (encoding >=2 && encoding <= 6) {
                   byte[] oldbytes = bytes;
                   int oldlen = len;
                   bytes = new byte[oldlen];
                   len = 0;
                   for (int i=0; i<oldlen; i++) {
                       if (oldbytes[i] != 0) {
                           bytes[len++] = oldbytes[i];
                       }
                   }
               }

              String charset;
              switch (encoding) {
                  case 1: charset = "UTF-16"; break; // most common case first.
                  case 0: charset = "UTF-16"; break; // symbol uses this
                  case 2: charset = "SJIS"; break;
                  case 3: charset = "GBK"; break;
                  case 4: charset = "MS950"; break;
                  case 5: charset = "EUC_KR"; break;
                  case 6: charset = "Johab"; break;
                  default: charset = "UTF-16"; break;
              }

              try {
                  return new String(bytes, 0, len, charset);
              } catch (UnsupportedEncodingException e) {
                  if (FontUtilities.isLogging()) {
                      FontUtilities.getLogger().warning(e + " EncodingID=" + encoding);
                  }
                  return new String(bytes, 0, len);
              } catch (Throwable t) {
                  return null;
              }
          }

          protected void initNames() {

      byte[] name = new byte[256];
      ByteBuffer buffer = getTableBuffer(nameTag);

      if (buffer != null) {
      ShortBuffer sbuffer = buffer.asShortBuffer();
      sbuffer.get(); // format - not needed.
      short numRecords = sbuffer.get();
      /* The name table uses unsigned shorts. Many of these
      * are known small values that fit in a short.
      * The values that are sizes or offsets into the table could be
      * greater than 32767, so read and store those as ints
      */
      int stringPtr = sbuffer.get() & 0xffff;

      nameLocale = sun.awt.SunToolkit.getStartupLocale();
      // XXX: initialize the languageCompatibleLCIDs after the nameLocale
      languageCompatibleLCIDs = getLanguageCompatibleLCIDsFromLocale(nameLocale);

      short nameLocaleID = getLCIDFromLocale(nameLocale);

      for (int i=0; i<numRecords; i++) {
      short platformID = sbuffer.get();
      if (platformID != MS_PLATFORM_ID) {
      sbuffer.position(sbuffer.position()+5);
      continue; // skip over this record.
      }
      short encodingID = sbuffer.get();
      short langID = sbuffer.get();
      short nameID = sbuffer.get();
      int nameLen = ((int) sbuffer.get()) & 0xffff;
      int namePtr = (((int) sbuffer.get()) & 0xffff) + stringPtr;
      String tmpName = null;
      switch (nameID) {

      case FAMILY_NAME_ID:
      //XXX: set to true only if we have found a language compatible LCID
      boolean compatible = false;
      if (familyName == null || langID == ENGLISH_LOCALE_ID ||
      langID == nameLocaleID
      // XXX: see if there is a language compatible LCID as last option
      || (compatible = isLanguageCompatible(langID)))
      {
      buffer.position(namePtr);
      buffer.get(name, 0, nameLen);
      tmpName = makeString(name, nameLen, encodingID);
      if (familyName == null || langID == ENGLISH_LOCALE_ID){
      familyName = tmpName;
      }
      if (langID == nameLocaleID
      // XXX: if we have not yet found a localeFamilyName, set a compatible name if possible
      || (localeFamilyName == null && compatible)) {
      localeFamilyName = tmpName;
      }
      }
      /*
                         for (int ii=0;ii<nameLen;ii++) {
                             int val = (int)name[ii]&0xff;
                             System.err.print(Integer.toHexString(val)+ " ");
                         }
                         System.err.println();
                         System.err.println("familyName="+familyName +
                                            " nameLen="+nameLen+
                                            " langID="+langID+ " eid="+encodingID +
                                            " str len="+familyName.length());

      */
      break;

      case FULL_NAME_ID:
      //XXX: set to true only if we have found a language compatible LCID
      compatible = false;
      if (fullName == null || langID == ENGLISH_LOCALE_ID ||
      langID == nameLocaleID
      // XXX: see if there is a language compatible LCID as last option
      || (compatible = isLanguageCompatible(langID)))
      {
      buffer.position(namePtr);
      buffer.get(name, 0, nameLen);
      tmpName = makeString(name, nameLen, encodingID);

      if (fullName == null || langID == ENGLISH_LOCALE_ID) {
      fullName = tmpName;
      }
      if (langID == nameLocaleID
      // XXX: if we have not yet found a localeFamilyName, set a compatible name if possible
      || (localeFullName == null && compatible)) {
      localeFullName = tmpName;
      }
      }
      break;
      }
      }
      if (localeFamilyName == null) {
      localeFamilyName = familyName;
      }
      if (localeFullName == null) {
      localeFullName = fullName;
      }
      }
      }

          /* Return the requested name in the requested locale, for the
           * MS platform ID. If the requested locale isn't found, return US
           * English, if that isn't found, return null and let the caller
           * figure out how to handle that.
           */
          protected String lookupName(short findLocaleID, int findNameID) {
              String foundName = null;
              byte[] name = new byte[1024];

              ByteBuffer buffer = getTableBuffer(nameTag);
              if (buffer != null) {
                  ShortBuffer sbuffer = buffer.asShortBuffer();
                  sbuffer.get(); // format - not needed.
                  short numRecords = sbuffer.get();

                  /* The name table uses unsigned shorts. Many of these
                   * are known small values that fit in a short.
                   * The values that are sizes or offsets into the table could be
                   * greater than 32767, so read and store those as ints
                   */
                  int stringPtr = ((int) sbuffer.get()) & 0xffff;

                  for (int i=0; i<numRecords; i++) {
                      short platformID = sbuffer.get();
                      if (platformID != MS_PLATFORM_ID) {
                          sbuffer.position(sbuffer.position()+5);
                          continue; // skip over this record.
                      }
                      short encodingID = sbuffer.get();
                      short langID = sbuffer.get();
                      short nameID = sbuffer.get();
                      int nameLen = ((int) sbuffer.get()) & 0xffff;
                      int namePtr = (((int) sbuffer.get()) & 0xffff) + stringPtr;
                      if (nameID == findNameID &&
                          ((foundName == null && langID == ENGLISH_LOCALE_ID)
                           || langID == findLocaleID)) {
                          buffer.position(namePtr);
                          buffer.get(name, 0, nameLen);
                          foundName = makeString(name, nameLen, encodingID);
                          if (langID == findLocaleID) {
                              return foundName;
                          }
                      }
                  }
              }
              return foundName;
          }

          /**
           * @return number of logical fonts. Is "1" for all but TTC files
           */
          public int getFontCount() {
              return directoryCount;
          }

          protected synchronized FontScaler getScaler() {
              if (scaler == null) {
                  scaler = FontScaler.getScaler(this, fontIndex,
                      supportsCJK, fileSize);
              }
              return scaler;
          }


          /* Postscript name is rarely requested. Don't waste cycles locating it
           * as part of font creation, nor storage to hold it. Get it only on demand.
           */
          @Override
          public String getPostscriptName() {
              String name = lookupName(ENGLISH_LOCALE_ID, POSTSCRIPT_NAME_ID);
              if (name == null) {
                  return fullName;
              } else {
                  return name;
              }
          }

          @Override
          public String getFontName(Locale locale) {
              if (locale == null) {
                  return fullName;
              } else if (locale.equals(nameLocale) && localeFullName != null) {
                  return localeFullName;
              } else {
                  short localeID = getLCIDFromLocale(locale);
                  String name = lookupName(localeID, FULL_NAME_ID);
                  if (name == null) {
                      return fullName;
                  } else {
                      return name;
                  }
              }
          }

          // Return a Microsoft LCID from the given Locale.
          // Used when getting localized font data.

          private static void addLCIDMapEntry(Map<String, Short> map,
                                              String key, short value) {
              map.put(key, Short.valueOf(value));
          }

          private static synchronized void createLCIDMap() {
              if (lcidMap != null) {
                  return;
              }

              Map<String, Short> map = new HashMap<String, Short>(200);

              // the following statements are derived from the langIDMap
              // in src/windows/native/java/lang/java_props_md.c using the following
              // awk script:
              // $1~/\/\*/ { next}
              // $3~/\?\?/ { next }
              // $3!~/_/ { next }
              // $1~/0x0409/ { next }
              // $1~/0x0c0a/ { next }
              // $1~/0x042c/ { next }
              // $1~/0x0443/ { next }
              // $1~/0x0812/ { next }
              // $1~/0x04/ { print " addLCIDMapEntry(map, " substr($3, 0, 3) "\", (short) " substr($1, 0, 6) ");" ; next }
              // $3~/,/ { print " addLCIDMapEntry(map, " $3 " (short) " substr($1, 0, 6) ");" ; next }
              // { print " addLCIDMapEntry(map, " $3 ", (short) " substr($1, 0, 6) ");" ; next }
              // The lines of this script:
              // - eliminate comments
              // - eliminate questionable locales
              // - eliminate language-only locales
              // - eliminate the default LCID value
              // - eliminate a few other unneeded LCID values
              // - print language-only locale entries for x04* LCID values
              // (apparently Microsoft doesn't use language-only LCID values -
              // see http://www.microsoft.com/OpenType/otspec/name.htm
              // - print complete entries for all other LCID values
              // Run
              // awk -f awk-script langIDMap > statements
              addLCIDMapEntry(map, "ar", (short) 0x0401);
              addLCIDMapEntry(map, "bg", (short) 0x0402);
              addLCIDMapEntry(map, "ca", (short) 0x0403);
              addLCIDMapEntry(map, "zh", (short) 0x0404);
              addLCIDMapEntry(map, "cs", (short) 0x0405);
              addLCIDMapEntry(map, "da", (short) 0x0406);
              addLCIDMapEntry(map, "de", (short) 0x0407);
              addLCIDMapEntry(map, "el", (short) 0x0408);
              addLCIDMapEntry(map, "es", (short) 0x040a);
              addLCIDMapEntry(map, "fi", (short) 0x040b);
              addLCIDMapEntry(map, "fr", (short) 0x040c);
              addLCIDMapEntry(map, "iw", (short) 0x040d);
              addLCIDMapEntry(map, "hu", (short) 0x040e);
              addLCIDMapEntry(map, "is", (short) 0x040f);
              addLCIDMapEntry(map, "it", (short) 0x0410);
              addLCIDMapEntry(map, "ja", (short) 0x0411);
              addLCIDMapEntry(map, "ko", (short) 0x0412);
              addLCIDMapEntry(map, "nl", (short) 0x0413);
              addLCIDMapEntry(map, "no", (short) 0x0414);
              addLCIDMapEntry(map, "pl", (short) 0x0415);
              addLCIDMapEntry(map, "pt", (short) 0x0416);
              addLCIDMapEntry(map, "rm", (short) 0x0417);
              addLCIDMapEntry(map, "ro", (short) 0x0418);
              addLCIDMapEntry(map, "ru", (short) 0x0419);
              addLCIDMapEntry(map, "hr", (short) 0x041a);
              addLCIDMapEntry(map, "sk", (short) 0x041b);
              addLCIDMapEntry(map, "sq", (short) 0x041c);
              addLCIDMapEntry(map, "sv", (short) 0x041d);
              addLCIDMapEntry(map, "th", (short) 0x041e);
              addLCIDMapEntry(map, "tr", (short) 0x041f);
              addLCIDMapEntry(map, "ur", (short) 0x0420);
              addLCIDMapEntry(map, "in", (short) 0x0421);
              addLCIDMapEntry(map, "uk", (short) 0x0422);
              addLCIDMapEntry(map, "be", (short) 0x0423);
              addLCIDMapEntry(map, "sl", (short) 0x0424);
              addLCIDMapEntry(map, "et", (short) 0x0425);
              addLCIDMapEntry(map, "lv", (short) 0x0426);
              addLCIDMapEntry(map, "lt", (short) 0x0427);
              addLCIDMapEntry(map, "fa", (short) 0x0429);
              addLCIDMapEntry(map, "vi", (short) 0x042a);
              addLCIDMapEntry(map, "hy", (short) 0x042b);
              addLCIDMapEntry(map, "eu", (short) 0x042d);
              addLCIDMapEntry(map, "mk", (short) 0x042f);
              addLCIDMapEntry(map, "tn", (short) 0x0432);
              addLCIDMapEntry(map, "xh", (short) 0x0434);
              addLCIDMapEntry(map, "zu", (short) 0x0435);
              addLCIDMapEntry(map, "af", (short) 0x0436);
              addLCIDMapEntry(map, "ka", (short) 0x0437);
              addLCIDMapEntry(map, "fo", (short) 0x0438);
              addLCIDMapEntry(map, "hi", (short) 0x0439);
              addLCIDMapEntry(map, "mt", (short) 0x043a);
              addLCIDMapEntry(map, "se", (short) 0x043b);
              addLCIDMapEntry(map, "gd", (short) 0x043c);
              addLCIDMapEntry(map, "ms", (short) 0x043e);
              addLCIDMapEntry(map, "kk", (short) 0x043f);
              addLCIDMapEntry(map, "ky", (short) 0x0440);
              addLCIDMapEntry(map, "sw", (short) 0x0441);
              addLCIDMapEntry(map, "tt", (short) 0x0444);
              addLCIDMapEntry(map, "bn", (short) 0x0445);
              addLCIDMapEntry(map, "pa", (short) 0x0446);
              addLCIDMapEntry(map, "gu", (short) 0x0447);
              addLCIDMapEntry(map, "ta", (short) 0x0449);
              addLCIDMapEntry(map, "te", (short) 0x044a);
              addLCIDMapEntry(map, "kn", (short) 0x044b);
              addLCIDMapEntry(map, "ml", (short) 0x044c);
              addLCIDMapEntry(map, "mr", (short) 0x044e);
              addLCIDMapEntry(map, "sa", (short) 0x044f);
              addLCIDMapEntry(map, "mn", (short) 0x0450);
              addLCIDMapEntry(map, "cy", (short) 0x0452);
              addLCIDMapEntry(map, "gl", (short) 0x0456);
              addLCIDMapEntry(map, "dv", (short) 0x0465);
              addLCIDMapEntry(map, "qu", (short) 0x046b);
              addLCIDMapEntry(map, "mi", (short) 0x0481);
              addLCIDMapEntry(map, "ar_IQ", (short) 0x0801);
              addLCIDMapEntry(map, "zh_CN", (short) 0x0804);
              addLCIDMapEntry(map, "de_CH", (short) 0x0807);
              addLCIDMapEntry(map, "en_GB", (short) 0x0809);
              addLCIDMapEntry(map, "es_MX", (short) 0x080a);
              addLCIDMapEntry(map, "fr_BE", (short) 0x080c);
              addLCIDMapEntry(map, "it_CH", (short) 0x0810);
              addLCIDMapEntry(map, "nl_BE", (short) 0x0813);
              addLCIDMapEntry(map, "no_NO_NY", (short) 0x0814);
              addLCIDMapEntry(map, "pt_PT", (short) 0x0816);
              addLCIDMapEntry(map, "ro_MD", (short) 0x0818);
              addLCIDMapEntry(map, "ru_MD", (short) 0x0819);
              addLCIDMapEntry(map, "sr_CS", (short) 0x081a);
              addLCIDMapEntry(map, "sv_FI", (short) 0x081d);
              addLCIDMapEntry(map, "az_AZ", (short) 0x082c);
              addLCIDMapEntry(map, "se_SE", (short) 0x083b);
              addLCIDMapEntry(map, "ga_IE", (short) 0x083c);
              addLCIDMapEntry(map, "ms_BN", (short) 0x083e);
              addLCIDMapEntry(map, "uz_UZ", (short) 0x0843);
              addLCIDMapEntry(map, "qu_EC", (short) 0x086b);
              addLCIDMapEntry(map, "ar_EG", (short) 0x0c01);
              addLCIDMapEntry(map, "zh_HK", (short) 0x0c04);
              addLCIDMapEntry(map, "de_AT", (short) 0x0c07);
              addLCIDMapEntry(map, "en_AU", (short) 0x0c09);
              addLCIDMapEntry(map, "fr_CA", (short) 0x0c0c);
              addLCIDMapEntry(map, "sr_CS", (short) 0x0c1a);
              addLCIDMapEntry(map, "se_FI", (short) 0x0c3b);
              addLCIDMapEntry(map, "qu_PE", (short) 0x0c6b);
              addLCIDMapEntry(map, "ar_LY", (short) 0x1001);
              addLCIDMapEntry(map, "zh_SG", (short) 0x1004);
              addLCIDMapEntry(map, "de_LU", (short) 0x1007);
              addLCIDMapEntry(map, "en_CA", (short) 0x1009);
              addLCIDMapEntry(map, "es_GT", (short) 0x100a);
              addLCIDMapEntry(map, "fr_CH", (short) 0x100c);
              addLCIDMapEntry(map, "hr_BA", (short) 0x101a);
              addLCIDMapEntry(map, "ar_DZ", (short) 0x1401);
              addLCIDMapEntry(map, "zh_MO", (short) 0x1404);
              addLCIDMapEntry(map, "de_LI", (short) 0x1407);
              addLCIDMapEntry(map, "en_NZ", (short) 0x1409);
              addLCIDMapEntry(map, "es_CR", (short) 0x140a);
              addLCIDMapEntry(map, "fr_LU", (short) 0x140c);
              addLCIDMapEntry(map, "bs_BA", (short) 0x141a);
              addLCIDMapEntry(map, "ar_MA", (short) 0x1801);
              addLCIDMapEntry(map, "en_IE", (short) 0x1809);
              addLCIDMapEntry(map, "es_PA", (short) 0x180a);
              addLCIDMapEntry(map, "fr_MC", (short) 0x180c);
              addLCIDMapEntry(map, "sr_BA", (short) 0x181a);
              addLCIDMapEntry(map, "ar_TN", (short) 0x1c01);
              addLCIDMapEntry(map, "en_ZA", (short) 0x1c09);
              addLCIDMapEntry(map, "es_DO", (short) 0x1c0a);
              addLCIDMapEntry(map, "sr_BA", (short) 0x1c1a);
              addLCIDMapEntry(map, "ar_OM", (short) 0x2001);
              addLCIDMapEntry(map, "en_JM", (short) 0x2009);
              addLCIDMapEntry(map, "es_VE", (short) 0x200a);
              addLCIDMapEntry(map, "ar_YE", (short) 0x2401);
              addLCIDMapEntry(map, "es_CO", (short) 0x240a);
              addLCIDMapEntry(map, "ar_SY", (short) 0x2801);
              addLCIDMapEntry(map, "en_BZ", (short) 0x2809);
              addLCIDMapEntry(map, "es_PE", (short) 0x280a);
              addLCIDMapEntry(map, "ar_JO", (short) 0x2c01);
              addLCIDMapEntry(map, "en_TT", (short) 0x2c09);
              addLCIDMapEntry(map, "es_AR", (short) 0x2c0a);
              addLCIDMapEntry(map, "ar_LB", (short) 0x3001);
              addLCIDMapEntry(map, "en_ZW", (short) 0x3009);
              addLCIDMapEntry(map, "es_EC", (short) 0x300a);
              addLCIDMapEntry(map, "ar_KW", (short) 0x3401);
              addLCIDMapEntry(map, "en_PH", (short) 0x3409);
              addLCIDMapEntry(map, "es_CL", (short) 0x340a);
              addLCIDMapEntry(map, "ar_AE", (short) 0x3801);
              addLCIDMapEntry(map, "es_UY", (short) 0x380a);
              addLCIDMapEntry(map, "ar_BH", (short) 0x3c01);
              addLCIDMapEntry(map, "es_PY", (short) 0x3c0a);
              addLCIDMapEntry(map, "ar_QA", (short) 0x4001);
              addLCIDMapEntry(map, "es_BO", (short) 0x400a);
              addLCIDMapEntry(map, "es_SV", (short) 0x440a);
              addLCIDMapEntry(map, "es_HN", (short) 0x480a);
              addLCIDMapEntry(map, "es_NI", (short) 0x4c0a);
              addLCIDMapEntry(map, "es_PR", (short) 0x500a);

              lcidMap = map;
          }

          private static short getLCIDFromLocale(Locale locale) {
              // optimize for common case
              if (locale.equals(Locale.US)) {
                  return US_LCID;
              }

              if (lcidMap == null) {
                  createLCIDMap();
              }

              String key = locale.toString();
              while (!"".equals(key)) {
                  Short lcidObject = (Short) lcidMap.get(key);
                  if (lcidObject != null) {
                      return lcidObject.shortValue();
                  }
                  int pos = key.lastIndexOf('_');
                  if (pos < 1) {
                      return US_LCID;
                  }
                  key = key.substring(0, pos);
              }

              return US_LCID;
          }

          @Override
          public String getFamilyName(Locale locale) {
              if (locale == null) {
                  return familyName;
              } else if (locale.equals(nameLocale) && localeFamilyName != null) {
                  return localeFamilyName;
              } else {
                  short localeID = getLCIDFromLocale(locale);
                  String name = lookupName(localeID, FAMILY_NAME_ID);
                  if (name == null) {
                      return familyName;
                  } else {
                      return name;
                  }
              }
          }

          public CharToGlyphMapper getMapper() {
              if (mapper == null) {
                  mapper = new TrueTypeGlyphMapper(this);
              }
              return mapper;
          }

          /* This duplicates initNames() but that has to run fast as its used
           * during typical start-up and the information here is likely never
           * needed.
           */
          protected void initAllNames(int requestedID, HashSet names) {

              byte[] name = new byte[256];
              ByteBuffer buffer = getTableBuffer(nameTag);

              if (buffer != null) {
                  ShortBuffer sbuffer = buffer.asShortBuffer();
                  sbuffer.get(); // format - not needed.
                  short numRecords = sbuffer.get();

                  /* The name table uses unsigned shorts. Many of these
                   * are known small values that fit in a short.
                   * The values that are sizes or offsets into the table could be
                   * greater than 32767, so read and store those as ints
                   */
                  int stringPtr = ((int) sbuffer.get()) & 0xffff;
                  for (int i=0; i<numRecords; i++) {
                      short platformID = sbuffer.get();
                      if (platformID != MS_PLATFORM_ID) {
                          sbuffer.position(sbuffer.position()+5);
                          continue; // skip over this record.
                      }
                      short encodingID = sbuffer.get();
                      short langID = sbuffer.get();
                      short nameID = sbuffer.get();
                      int nameLen = ((int) sbuffer.get()) & 0xffff;
                      int namePtr = (((int) sbuffer.get()) & 0xffff) + stringPtr;

                      if (nameID == requestedID) {
                          buffer.position(namePtr);
                          buffer.get(name, 0, nameLen);
                          names.add(makeString(name, nameLen, encodingID));
                      }
                  }
              }
          }

          String[] getAllFamilyNames() {
              HashSet aSet = new HashSet();
              try {
                  initAllNames(FAMILY_NAME_ID, aSet);
              } catch (Exception e) {
                  /* In case of malformed font */
              }
              return (String[])aSet.toArray(new String[0]);
          }

          String[] getAllFullNames() {
              HashSet aSet = new HashSet();
              try {
                  initAllNames(FULL_NAME_ID, aSet);
              } catch (Exception e) {
                  /* In case of malformed font */
              }
              return (String[])aSet.toArray(new String[0]);
          }

          /* Used by the OpenType engine for mark positioning.
           */
          @Override
          Point2D.Float getGlyphPoint(long pScalerContext,
                                      int glyphCode, int ptNumber) {
              try {
                  return getScaler().getGlyphPoint(pScalerContext,
                                                   glyphCode, ptNumber);
              } catch(FontScalerException fe) {
                  return null;
              }
          }

          private char[] gaspTable;

          private char[] getGaspTable() {

              if (gaspTable != null) {
                  return gaspTable;
              }

              ByteBuffer buffer = getTableBuffer(gaspTag);
              if (buffer == null) {
                  return gaspTable = new char[0];
              }

              CharBuffer cbuffer = buffer.asCharBuffer();
              char format = cbuffer.get();
              /* format "1" has appeared for some Windows Vista fonts.
               * Its presently undocumented but the existing values
               * seem to be still valid so we can use it.
               */
              if (format > 1) { // unrecognised format
                  return gaspTable = new char[0];
              }

              char numRanges = cbuffer.get();
              if (4+numRanges*4 > getTableSize(gaspTag)) { // sanity check
                  return gaspTable = new char[0];
              }
              gaspTable = new char[2*numRanges];
              cbuffer.get(gaspTable);
              return gaspTable;
          }

          /* This is to obtain info from the TT 'gasp' (grid-fitting and
           * scan-conversion procedure) table which specifies three combinations:
           * Hint, Smooth (greyscale), Hint and Smooth.
           * In this simplified scheme we don't distinguish the latter two. We
           * hint even at small sizes, so as to preserve metrics consistency.
           * If the information isn't available default values are substituted.
           * The more precise defaults we'd do if we distinguished the cases are:
           * Bold (no other style) fonts :
           * 0-8 : Smooth ( do grey)
           * 9+ : Hint + smooth (gridfit + grey)
           * Plain, Italic and Bold-Italic fonts :
           * 0-8 : Smooth ( do grey)
           * 9-17 : Hint (gridfit)
           * 18+ : Hint + smooth (gridfit + grey)
           * The defaults should rarely come into play as most TT fonts provide
           * better defaults.
           * REMIND: consider unpacking the table into an array of booleans
           * for faster use.
           */
          @Override
          public boolean useAAForPtSize(int ptsize) {

              char[] gasp = getGaspTable();
              if (gasp.length > 0) {
                  for (int i=0;i<gasp.length;i+=2) {
                      if (ptsize <= gasp[i]) {
                          return ((gasp[i+1] & 0x2) != 0); // bit 2 means DO_GRAY;
                      }
                  }
                  return true;
              }

              if (style == Font.BOLD) {
                  return true;
              } else {
                  return ptsize <= 8 || ptsize >= 18;
              }
          }

          @Override
          public boolean hasSupplementaryChars() {
              return ((TrueTypeGlyphMapper)getMapper()).hasSupplementaryChars();
          }

          @Override
          public String toString() {
              return "** TrueType Font: Family="+familyName+ " Name="+fullName+
                  " style="+style+" fileName="+getPublicFileName();
          }
          
          
          // XXX: new methods and fields
          
          private static Map<String, short[]> lcidLanguageCompatibilityMap;
      private static final short[] EMPTY_COMPATIBLE_LCIDS = new short[0];

      // the language compatible LCIDs for this fonts nameLocale
      private short[] languageCompatibleLCIDs;

      /*
      * Returns true if the given lcid's language is compatible
      * to the language of the startup Locale. I.e. if
      * startupLocale.getLanguage().equals(lcidLocale.getLanguage()) would
      * return true.
      */
      private boolean isLanguageCompatible(short lcid){
      for (short s : languageCompatibleLCIDs) {
      if (s == lcid) {
      return true;
      }
      }
      return false;
      }

      /*
      * Returns an array of all the language compatible LCIDs for the
      * given Locale. This array is later used to find compatible
      * locales.
      */
      private static short[] getLanguageCompatibleLCIDsFromLocale(Locale locale){
      if (lcidLanguageCompatibilityMap == null) {
      createLCIDMap();
      createLCIDLanguageCompatibilityMap();
      }
      String language = locale.getLanguage();
      short[] result = lcidLanguageCompatibilityMap .get(language);
      return result == null ? EMPTY_COMPATIBLE_LCIDS : result;
      }

      /*
      * Initializes the map from Locale keys (e.g. "en_BZ" or "de") to language compatible LCIDs.
      */
      private static synchronized void createLCIDLanguageCompatibilityMap(){
      if (lcidLanguageCompatibilityMap != null) {
      return;
      }
      // TODO: it would be possible to hardcode the created map
      // for a possible performance increase
      HashMap<String, List<Short>> result = new HashMap<>();
      for (Entry<String, Short> e : lcidMap.entrySet()) {
      String language = e.getKey();
      int index = language.indexOf('_');
      if (index != -1) {
      language = language.substring(0, index);
      }
      List<Short> list = result.get(language);
      if (list == null) {
      list = new ArrayList<>();
      result.put(language, list);
      }
      if (index == -1) {
      // insert the main language LCID at the beginning so it is found first
      list.add(0, e.getValue());
      } else{
      list.add(e.getValue());
      }
      }
      lcidLanguageCompatibilityMap = new HashMap<>();
      for (Entry<String, List<Short>> e : result.entrySet()) {
      if (e.getValue().size() > 1) {
      List<Short> list = e.getValue();
      short[] shorts = new short[list.size()];
      for (int i = 0; i < shorts.length; i++) {
      shorts[i] = list.get(i);
      }
      lcidLanguageCompatibilityMap.put(e.getKey(), shorts);
      }
      }
      }
      }


      STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
      javac TrueTypeFontBug.java
      java TrueTypeFontBug

      EXPECTED VERSUS ACTUAL BEHAVIOR :
      EXPECTED -
      System.out should print "Verdana Fett" for both the Austrian and the German font name:

      Austrian font name: Verdana Fett
      German font name: Verdana Fett
      ACTUAL -
      System.out prints:

      Austrian font name: Verdana Bold
      German font name: Verdana Fett


      REPRODUCIBILITY :
      This bug can be reproduced always.

      ---------- BEGIN SOURCE ----------
      import java.awt.Font;
      import java.util.Locale;

      public class TrueTypeFontBug {
      public static void main(String[] args) {
      // change the startup locale for the JVM to Austria will result
      System.setProperty("user.language", "de");
      System.setProperty("user.country", "AT");
      Locale austrianLocale = new Locale("de", "AT");
      Locale.setDefault(austrianLocale);
      // assert sun.awt.SunToolkit.getStartupLocale().equals(new Locale("de", "AT"));

      // instantiate the font
      Font font = new Font("Verdana", Font.BOLD, 12);

      String fontNameForAustria = font.getFontName();
      Locale germanLocale = new Locale("de", "GE");
      String fontNameForGermany = font.getFontName(germanLocale);

      System.out.println("Austrian font name: " + fontNameForAustria);
      System.out.println("German font name: " + fontNameForGermany);
      }
      }
      ---------- END SOURCE ----------

      Attachments

        Activity

          People

            prr Philip Race
            webbuggrp Webbug Group
            Votes:
            0 Vote for this issue
            Watchers:
            4 Start watching this issue

            Dates

              Created:
              Updated:
              Resolved: