/*
* Copyright 2010 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.typography.font.sfntly.table.core;
import com.google.typography.font.sfntly.Font.MacintoshEncodingId;
import com.google.typography.font.sfntly.Font.PlatformId;
import com.google.typography.font.sfntly.Font.UnicodeEncodingId;
import com.google.typography.font.sfntly.Font.WindowsEncodingId;
import com.google.typography.font.sfntly.data.ReadableFontData;
import com.google.typography.font.sfntly.data.WritableFontData;
import com.google.typography.font.sfntly.table.Header;
import com.google.typography.font.sfntly.table.SubTableContainerTable;
import com.ibm.icu.charset.CharsetICU;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.TreeMap;
// TODO(stuartg): support format 1 name tables
/**
* A Name table.
*
* @author Stuart Gill
*/
public final class NameTable extends SubTableContainerTable implements Iterable<
NameTable.NameEntry> {
/**
* Offsets to specific elements in the underlying data. These offsets are relative to the
* start of the table or the start of sub-blocks within the table.
*/
public enum Offset {
format(0),
count(2),
stringOffset(4),
nameRecordStart(6),
// format 1 - offset from the end of the name records
langTagCount(0),
langTagRecord(2),
nameRecordSize(12),
// Name Records
nameRecordPlatformId(0),
nameRecordEncodingId(2),
nameRecordLanguageId(4),
nameRecordNameId(6),
nameRecordStringLength(8),
nameRecordStringOffset(10);
private final int offset;
private Offset(int offset) {
this.offset = offset;
}
}
public enum NameId {
Unknown(-1),
CopyrightNotice(0),
FontFamilyName(1),
FontSubfamilyName(2),
UniqueFontIdentifier(3),
FullFontName(4),
VersionString(5),
PostscriptName(6),
Trademark(7),
ManufacturerName(8),
Designer(9),
Description(10),
VendorURL(11),
DesignerURL(12),
LicenseDescription(13),
LicenseInfoURL(14),
Reserved15(15),
PreferredFamily(16),
PreferredSubfamily(17),
CompatibleFullName(18),
SampleText(19),
PostscriptCID(20),
WWSFamilyName(21),
WWSSubfamilyName(22);
private final int value;
private NameId(int value) {
this.value = value;
}
public int value() {
return this.value;
}
public boolean equals(int value) {
return value == this.value;
}
public static NameId valueOf(int value) {
for (NameId name : NameId.values()) {
if (name.equals(value)) {
return name;
}
}
return Unknown;
}
}
public enum UnicodeLanguageId {
// Unicode Language IDs (platform ID = 0)
Unknown(-1), All(0);
private final int value;
private UnicodeLanguageId(int value) {
this.value = value;
}
public int value() {
return this.value;
}
public boolean equals(int value) {
return value == this.value;
}
public static UnicodeLanguageId valueOf(int value) {
for (UnicodeLanguageId language : UnicodeLanguageId.values()) {
if (language.equals(value)) {
return language;
}
}
return Unknown;
}
}
/**
* Macinstosh Language IDs (platform ID = 1)
*
*/
public enum MacintoshLanguageId {
Unknown(-1),
English(0),
French(1),
German(2),
Italian(3),
Dutch(4),
Swedish(5),
Spanish(6),
Danish(7),
Portuguese(8),
Norwegian(9),
Hebrew(10),
Japanese(11),
Arabic(12),
Finnish(13),
Greek(14),
Icelandic(15),
Maltese(16),
Turkish(17),
Croatian(18),
Chinese_Traditional(19),
Urdu(20),
Hindi(21),
Thai(22),
Korean(23),
Lithuanian(24),
Polish(25),
Hungarian(26),
Estonian(27),
Latvian(28),
Sami(29),
Faroese(30),
FarsiPersian(31),
Russian(32),
Chinese_Simplified(33),
Flemish(34),
IrishGaelic(35),
Albanian(36),
Romanian(37),
Czech(38),
Slovak(39),
Slovenian(40),
Yiddish(41),
Serbian(42),
Macedonian(43),
Bulgarian(44),
Ukrainian(45),
Byelorussian(46),
Uzbek(47),
Kazakh(48),
Azerbaijani_Cyrillic(49),
Azerbaijani_Arabic(50),
Armenian(51),
Georgian(52),
Moldavian(53),
Kirghiz(54),
Tajiki(55),
Turkmen(56),
Mongolian_Mongolian(57),
Mongolian_Cyrillic(58),
Pashto(59),
Kurdish(60),
Kashmiri(61),
Sindhi(62),
Tibetan(63),
Nepali(64),
Sanskrit(65),
Marathi(66),
Bengali(67),
Assamese(68),
Gujarati(69),
Punjabi(70),
Oriya(71),
Malayalam(72),
Kannada(73),
Tamil(74),
Telugu(75),
Sinhalese(76),
Burmese(77),
Khmer(78),
Lao(79),
Vietnamese(80),
Indonesian(81),
Tagalong(82),
Malay_Roman(83),
Malay_Arabic(84),
Amharic(85),
Tigrinya(86),
Galla(87),
Somali(88),
Swahili(89),
KinyarwandaRuanda(90),
Rundi(91),
NyanjaChewa(92),
Malagasy(93),
Esperanto(94),
Welsh(128),
Basque(129),
Catalan(130),
Latin(131),
Quenchua(132),
Guarani(133),
Aymara(134),
Tatar(135),
Uighur(136),
Dzongkha(137),
Javanese_Roman(138),
Sundanese_Roman(139),
Galician(140),
Afrikaans(141),
Breton(142),
Inuktitut(143),
ScottishGaelic(144),
ManxGaelic(145),
IrishGaelic_WithDotAbove(146),
Tongan(147),
Greek_Polytonic(148),
Greenlandic(149),
Azerbaijani_Roman(150);
private final int value;
private MacintoshLanguageId(int value) {
this.value = value;
}
public int value() {
return this.value;
}
public boolean equals(int value) {
return value == this.value;
}
public static MacintoshLanguageId valueOf(int value) {
for (MacintoshLanguageId language : MacintoshLanguageId.values()) {
if (language.equals(value)) {
return language;
}
}
return Unknown;
}
}
/**
* Windows Language IDs (platform ID = 3)
*/
public enum WindowsLanguageId {
Unknown(-1),
Afrikaans_SouthAfrica(0x0436),
Albanian_Albania(0x041C),
Alsatian_France(0x0484),
Amharic_Ethiopia(0x045E),
Arabic_Algeria(0x1401),
Arabic_Bahrain(0x3C01),
Arabic_Egypt(0x0C01),
Arabic_Iraq(0x0801),
Arabic_Jordan(0x2C01),
Arabic_Kuwait(0x3401),
Arabic_Lebanon(0x3001),
Arabic_Libya(0x1001),
Arabic_Morocco(0x1801),
Arabic_Oman(0x2001),
Arabic_Qatar(0x4001),
Arabic_SaudiArabia(0x0401),
Arabic_Syria(0x2801),
Arabic_Tunisia(0x1C01),
Arabic_UAE(0x3801),
Arabic_Yemen(0x2401),
Armenian_Armenia(0x042B),
Assamese_India(0x044D),
Azeri_Cyrillic_Azerbaijan(0x082C),
Azeri_Latin_Azerbaijan(0x042C),
Bashkir_Russia(0x046D),
Basque_Basque(0x042D),
Belarusian_Belarus(0x0423),
Bengali_Bangladesh(0x0845),
Bengali_India(0x0445),
Bosnian_Cyrillic_BosniaAndHerzegovina(0x201A),
Bosnian_Latin_BosniaAndHerzegovina(0x141A),
Breton_France(0x047E),
Bulgarian_Bulgaria(0x0402),
Catalan_Catalan(0x0403),
Chinese_HongKongSAR(0x0C04),
Chinese_MacaoSAR(0x1404),
Chinese_PeoplesRepublicOfChina(0x0804),
Chinese_Singapore(0x1004),
Chinese_Taiwan(0x0404),
Corsican_France(0x0483),
Croatian_Croatia(0x041A),
Croatian_Latin_BosniaAndHerzegovina(0x101A),
Czech_CzechRepublic(0x0405),
Danish_Denmark(0x0406),
Dari_Afghanistan(0x048C),
Divehi_Maldives(0x0465),
Dutch_Belgium(0x0813),
Dutch_Netherlands(0x0413),
English_Australia(0x0C09),
English_Belize(0x2809),
English_Canada(0x1009),
English_Caribbean(0x2409),
English_India(0x4009),
English_Ireland(0x1809),
English_Jamaica(0x2009),
English_Malaysia(0x4409),
English_NewZealand(0x1409),
English_RepublicOfThePhilippines(0x3409),
English_Singapore(0x4809),
English_SouthAfrica(0x1C09),
English_TrinidadAndTobago(0x2C09),
English_UnitedKingdom(0x0809),
English_UnitedStates(0x0409),
English_Zimbabwe(0x3009),
Estonian_Estonia(0x0425),
Faroese_FaroeIslands(0x0438),
Filipino_Philippines(0x0464),
Finnish_Finland(0x040B),
French_Belgium(0x080C),
French_Canada(0x0C0C),
French_France(0x040C),
French_Luxembourg(0x140c),
French_PrincipalityOfMonoco(0x180C),
French_Switzerland(0x100C),
Frisian_Netherlands(0x0462),
Galician_Galician(0x0456),
Georgian_Georgia(0x0437),
German_Austria(0x0C07),
German_Germany(0x0407),
German_Liechtenstein(0x1407),
German_Luxembourg(0x1007),
German_Switzerland(0x0807),
Greek_Greece(0x0408),
Greenlandic_Greenland(0x046F),
Gujarati_India(0x0447),
Hausa_Latin_Nigeria(0x0468),
Hebrew_Israel(0x040D),
Hindi_India(0x0439),
Hungarian_Hungary(0x040E),
Icelandic_Iceland(0x040F),
Igbo_Nigeria(0x0470),
Indonesian_Indonesia(0x0421),
Inuktitut_Canada(0x045D),
Inuktitut_Latin_Canada(0x085D),
Irish_Ireland(0x083C),
isiXhosa_SouthAfrica(0x0434),
isiZulu_SouthAfrica(0x0435),
Italian_Italy(0x0410),
Italian_Switzerland(0x0810),
Japanese_Japan(0x0411),
Kannada_India(0x044B),
Kazakh_Kazakhstan(0x043F),
Khmer_Cambodia(0x0453),
Kiche_Guatemala(0x0486),
Kinyarwanda_Rwanda(0x0487),
Kiswahili_Kenya(0x0441),
Konkani_India(0x0457),
Korean_Korea(0x0412),
Kyrgyz_Kyrgyzstan(0x0440),
Lao_LaoPDR(0x0454),
Latvian_Latvia(0x0426),
Lithuanian_Lithuania(0x0427),
LowerSorbian_Germany(0x082E),
Luxembourgish_Luxembourg(0x046E),
Macedonian_FYROM_FormerYugoslavRepublicOfMacedonia(0x042F),
Malay_BruneiDarussalam(0x083E),
Malay_Malaysia(0x043E),
Malayalam_India(0x044C),
Maltese_Malta(0x043A),
Maori_NewZealand(0x0481),
Mapudungun_Chile(0x047A),
Marathi_India(0x044E),
Mohawk_Mohawk(0x047C),
Mongolian_Cyrillic_Mongolia(0x0450),
Mongolian_Traditional_PeoplesRepublicOfChina(0x0850),
Nepali_Nepal(0x0461),
Norwegian_Bokmal_Norway(0x0414),
Norwegian_Nynorsk_Norway(0x0814),
Occitan_France(0x0482),
Oriya_India(0x0448),
Pashto_Afghanistan(0x0463),
Polish_Poland(0x0415),
Portuguese_Brazil(0x0416),
Portuguese_Portugal(0x0816),
Punjabi_India(0x0446),
Quechua_Bolivia(0x046B),
Quechua_Ecuador(0x086B),
Quechua_Peru(0x0C6B),
Romanian_Romania(0x0418),
Romansh_Switzerland(0x0417),
Russian_Russia(0x0419),
Sami_Inari_Finland(0x243B),
Sami_Lule_Norway(0x103B),
Sami_Lule_Sweden(0x143B),
Sami_Northern_Finland(0x0C3B),
Sami_Northern_Norway(0x043B),
Sami_Northern_Sweden(0x083B),
Sami_Skolt_Finland(0x203B),
Sami_Southern_Norway(0x183B),
Sami_Southern_Sweden(0x1C3B),
Sanskrit_India(0x044F),
Serbian_Cyrillic_BosniaAndHerzegovina(0x1C1A),
Serbian_Cyrillic_Serbia(0x0C1A),
Serbian_Latin_BosniaAndHerzegovina(0x181A),
Serbian_Latin_Serbia(0x081A),
SesothoSaLeboa_SouthAfrica(0x046C),
Setswana_SouthAfrica(0x0432),
Sinhala_SriLanka(0x045B),
Slovak_Slovakia(0x041B),
Slovenian_Slovenia(0x0424),
Spanish_Argentina(0x2C0A),
Spanish_Bolivia(0x400A),
Spanish_Chile(0x340A),
Spanish_Colombia(0x240A),
Spanish_CostaRica(0x140A),
Spanish_DominicanRepublic(0x1C0A),
Spanish_Ecuador(0x300A),
Spanish_ElSalvador(0x440A),
Spanish_Guatemala(0x100A),
Spanish_Honduras(0x480A),
Spanish_Mexico(0x080A),
Spanish_Nicaragua(0x4C0A),
Spanish_Panama(0x180A),
Spanish_Paraguay(0x3C0A),
Spanish_Peru(0x280A),
Spanish_PuertoRico(0x500A),
Spanish_ModernSort_Spain(0x0C0A),
Spanish_TraditionalSort_Spain(0x040A),
Spanish_UnitedStates(0x540A),
Spanish_Uruguay(0x380A),
Spanish_Venezuela(0x200A),
Sweden_Finland(0x081D),
Swedish_Sweden(0x041D),
Syriac_Syria(0x045A),
Tajik_Cyrillic_Tajikistan(0x0428),
Tamazight_Latin_Algeria(0x085F),
Tamil_India(0x0449),
Tatar_Russia(0x0444),
Telugu_India(0x044A),
Thai_Thailand(0x041E),
Tibetan_PRC(0x0451),
Turkish_Turkey(0x041F),
Turkmen_Turkmenistan(0x0442),
Uighur_PRC(0x0480),
Ukrainian_Ukraine(0x0422),
UpperSorbian_Germany(0x042E),
Urdu_IslamicRepublicOfPakistan(0x0420),
Uzbek_Cyrillic_Uzbekistan(0x0843),
Uzbek_Latin_Uzbekistan(0x0443),
Vietnamese_Vietnam(0x042A),
Welsh_UnitedKingdom(0x0452),
Wolof_Senegal(0x0448),
Yakut_Russia(0x0485),
Yi_PRC(0x0478),
Yoruba_Nigeria(0x046A);
private final int value;
private WindowsLanguageId(int value) {
this.value = value;
}
public int value() {
return this.value;
}
public boolean equals(int value) {
return value == this.value;
}
public static WindowsLanguageId valueOf(int value) {
for (WindowsLanguageId language : WindowsLanguageId.values()) {
if (language.equals(value)) {
return language;
}
}
return Unknown;
}
}
private NameTable(Header header, ReadableFontData data) {
super(header, data);
}
public int format() {
return this.data.readUShort(Offset.format.offset);
}
/**
* Get the number of names in the name table.
* @return the number of names
*/
public int nameCount() {
return this.data.readUShort(Offset.count.offset);
}
/**
* Get the offset to the string data in the name table.
* @return the string offset
*/
private int stringOffset() {
return this.data.readUShort(Offset.stringOffset.offset);
}
/**
* Get the offset for the given name record.
* @param index the index of the name record
* @return the offset of the name record
*/
private int offsetForNameRecord(int index) {
return Offset.nameRecordStart.offset + index * Offset.nameRecordSize.offset;
}
/**
* Get the platform id for the given name record.
*
* @param index the index of the name record
* @return the platform id
* @see PlatformId
*/
public int platformId(int index) {
return this.data.readUShort(
Offset.nameRecordPlatformId.offset + this.offsetForNameRecord(index));
}
/**
* Get the encoding id for the given name record.
*
* @param index the index of the name record
* @return the encoding id
*
* @see MacintoshEncodingId
* @see WindowsEncodingId
* @see UnicodeEncodingId
*/
public int encodingId(int index) {
return this.data.readUShort(
Offset.nameRecordEncodingId.offset + this.offsetForNameRecord(index));
}
/**
* Get the language id for the given name record.
* @param index the index of the name record
* @return the language id
*/
public int languageId(int index) {
return this.data.readUShort(
Offset.nameRecordLanguageId.offset + this.offsetForNameRecord(index));
}
/**
* Get the name id for given name record.
* @param index the index of the name record
* @return the name id
*/
public int nameId(int index) {
return this.data.readUShort(
Offset.nameRecordNameId.offset + this.offsetForNameRecord(index));
}
/**
* Get the length of the string data for the given name record.
* @param index the index of the name record
* @return the length of the string data in bytes
*/
private int nameLength(int index) {
return this.data.readUShort(
Offset.nameRecordStringLength.offset + this.offsetForNameRecord(index));
}
/**
* Get the offset of the string data for the given name record.
* @param index the index of the name record
* @return the offset of the string data from the start of the table
*/
private int nameOffset(int index) {
return this.data.readUShort(
Offset.nameRecordStringOffset.offset +
this.offsetForNameRecord(index)) + this.stringOffset();
}
/**
* Get the name as bytes for the given name record.
* @param index the index of the name record
* @return the bytes for the name
*/
public byte[] nameAsBytes(int index) {
int length = this.nameLength(index);
byte[] b = new byte[length];
this.data.readBytes(this.nameOffset(index), b, 0, length);
return b;
}
/**
* Get the name as bytes for the specified name. If there is no entry for the requested name
* then null
is returned.
* @param platformId the platform id
* @param encodingId the encoding id
* @param languageId the language id
* @param nameId the name id
* @return the bytes for the name
*/
public byte[] nameAsBytes(int platformId, int encodingId, int languageId, int nameId) {
NameEntry entry = this.nameEntry(platformId, encodingId, languageId, nameId);
if (entry != null) {
return entry.nameAsBytes();
}
return null;
}
/**
* Get the name as a String for the given name record. If there is no encoding conversion
* available for the name record then a best attempt String will be returned.
* @param index the index of the name record
* @return the name
*/
public String name(int index) {
return convertFromNameBytes(
this.nameAsBytes(index), this.platformId(index), this.encodingId(index));
}
/**
* Get the name as a String for the specified name. If there is no entry for the requested name
* then null
is returned. If there is no encoding conversion
* available for the name then a best attempt String will be returned.
* @param platformId the platform id
* @param encodingId the encoding id
* @param languageId the language id
* @param nameId the name id
* @return the name
*/
public String name(int platformId, int encodingId, int languageId, int nameId) {
NameEntry entry = this.nameEntry(platformId, encodingId, languageId, nameId);
if (entry != null) {
return entry.name();
}
return null;
}
/**
* Get the name entry record for the given name entry.
* @param index the index of the name record
* @return the name entry
*/
public NameEntry nameEntry(int index) {
return new NameEntry(
this.platformId(index), this.encodingId(index), this.languageId(index),
this.nameId(index), this.nameAsBytes(index));
}
/**
* Get the name entry record for the specified name. If there is no entry for the requested name
* then null
is returned.
* @param platformId the platform id
* @param encodingId the encoding id
* @param languageId the language id
* @param nameId the name id
* @return the name entry
*/
public NameEntry nameEntry(
final int platformId, final int encodingId, final int languageId, final int nameId) {
Iterator nameEntryIter = this.iterator(new NameEntryFilter() {
@Override
public boolean accept(int pid, int eid, int lid, int nid) {
if (pid == platformId && eid == encodingId && lid == languageId && nid == nameId) {
return true;
}
return false;
}
});
// can only be one name for each set of ids
if (nameEntryIter.hasNext()) {
return nameEntryIter.next();
}
return null;
}
/**
* Get all the name entry records.
* @return the set of all name entry records
*/
public Set names() {
Set nameSet = new HashSet(this.nameCount());
for (NameEntry entry : this) {
nameSet.add(entry);
}
return nameSet;
}
private static class NameEntryId implements Comparable {
/* @see Font.PlatformId
*/
protected int platformId;
/* @see Font.UnicodeEncodingId
* @see Font.MacintoshEncodingId
* @see Font.WindowsEncodingId
*/
protected int encodingId;
/* @see NameTable.UnicodeLanguageId
* @see NameTable.MacintoshLanguageId
* @see NameTable.WindowsLanguageId
*/
protected int languageId;
/* @see NameTable.NameId
*/
protected int nameId;
/**
* @param platformId
* @param encodingId
* @param languageId
* @param nameId
*/
protected NameEntryId(int platformId, int encodingId, int languageId, int nameId) {
this.platformId = platformId;
this.encodingId = encodingId;
this.languageId = languageId;
this.nameId = nameId;
}
/**
* Get the platform id.
*
* @return the platform id
*/
protected int getPlatformId() {
return this.platformId;
}
/**
* Get the encoding id.
*
* @return the encoding id
*/
protected int getEncodingId() {
return this.encodingId;
}
/**
* Get the language id.
*
* @return the language id
*/
protected int getLanguageId() {
return this.languageId;
}
/**
* Get the name id.
*
* @return the name id
*/
protected int getNameId() {
return this.nameId;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof NameEntryId)) {
return false;
}
NameEntryId other = (NameEntryId) obj;
return (this.encodingId == other.encodingId) && (this.languageId == other.languageId)
&& (this.platformId == other.platformId) && (this.nameId == other.nameId);
}
@Override
public int hashCode() {
/*
* - this takes advantage of the sizes of the various entries and the fact
* that the ranges of their values have an almost zero probability of ever
* changing - this allows us to generate a unique hash at low cost - if
* the ranges do change then we will potentially generate non-unique hash
* values which is a common result
*/
return ((this.encodingId & 0x3f) << 26) | ((this.nameId & 0x3f) << 16)
| ((this.platformId & 0x0f) << 12) | (this.languageId & 0xff);
}
/**
* Name entries are sorted by platform id, encoding id, language id, and
* name id in order of decreasing importance.
*
* @return less than zero if this entry is less than the other; greater than
* zero if this entry is greater than the other; and zero if they
* are equal
*
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
@Override
public int compareTo(NameEntryId o) {
if (this.platformId != o.platformId) {
return this.platformId - o.platformId;
}
if (this.encodingId != o.encodingId) {
return this.encodingId - o.encodingId;
}
if (this.languageId != o.languageId) {
return this.languageId - o.languageId;
}
return this.nameId - o.nameId;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("P=");
sb.append(PlatformId.valueOf(this.platformId));
sb.append(", E=0x");
sb.append(Integer.toHexString(this.encodingId));
sb.append(", L=0x");
sb.append(Integer.toHexString(this.languageId));
sb.append(", N=");
NameId nameId = NameId.valueOf(this.nameId);
if (nameId != null) {
sb.append(NameId.valueOf(this.nameId));
} else {
sb.append("0x");
sb.append(Integer.toHexString(this.nameId));
}
return sb.toString();
}
}
/**
* Class to represent a name entry in the name table.
*
*/
public static class NameEntry {
NameEntryId nameEntryId;
protected int length;
protected byte[] nameBytes;
protected NameEntry() {
}
protected NameEntry(NameEntryId nameEntryId, byte[] nameBytes) {
this.nameEntryId = nameEntryId;
this.nameBytes = nameBytes;
}
protected NameEntry(
int platformId, int encodingId, int languageId, int nameId, byte[] nameBytes) {
this(new NameEntryId(platformId, encodingId, languageId, nameId), nameBytes);
}
protected NameEntryId getNameEntryId() {
return this.nameEntryId;
}
/**
* Get the platform id.
* @return the platform id
*/
public int platformId() {
return this.nameEntryId.getPlatformId();
}
/**
* Get the encoding id.
* @return the encoding id
*/
public int encodingId() {
return this.nameEntryId.getEncodingId();
}
/**
* Get the language id.
* @return the language id
*/
public int languageId() {
return this.nameEntryId.getLanguageId();
}
/**
* Get the name id.
* @return the name id
*/
public int nameId() {
return this.nameEntryId.getNameId();
}
/**
* Get the bytes for name.
* @return the name bytes
*/
public byte[] nameAsBytes() {
return this.nameBytes;
}
/**
* Get the name as a String. If there is no encoding conversion
* available for the name bytes then a best attempt String will be returned.
* @return the name
*/
public String name() {
return NameTable.convertFromNameBytes(this.nameBytes, this.platformId(), this.encodingId());
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(this.nameEntryId);
sb.append(", \"");
String name = this.name();
sb.append(this.name());
sb.append("\"]");
return sb.toString();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof NameEntry)) {
return false;
}
NameEntry other = (NameEntry) obj;
if (!this.nameEntryId.equals(other.nameEntryId)) {
return false;
}
if (this.nameBytes.length != other.nameBytes.length) {
return false;
}
for (int i = 0; i < this.nameBytes.length; i++) {
if (this.nameBytes[i] != other.nameBytes[i]) {
return false;
}
}
return true;
}
@Override
public int hashCode() {
int hash = this.nameEntryId.hashCode();
for (int i = 0; i < this.nameBytes.length; i+=4) {
for (int j = 0; j < 4 && j + i < this.nameBytes.length; j++) {
hash |= this.nameBytes[j] << j * 8;
}
}
return hash;
}
}
public static class NameEntryBuilder extends NameEntry {
/**
* Constructor.
*/
protected NameEntryBuilder() {
super();
}
protected NameEntryBuilder(NameEntryId nameEntryId, byte[] nameBytes) {
super(nameEntryId, nameBytes);
}
protected NameEntryBuilder(NameEntryId nameEntryId) {
this(nameEntryId, null);
}
protected NameEntryBuilder(NameEntry nameEntry) {
this(nameEntry.getNameEntryId(), nameEntry.nameAsBytes());
}
public void setName(String name) {
if (name == null) {
this.nameBytes = new byte[0];
return;
}
this.nameBytes = NameTable.convertToNameBytes(
name, this.nameEntryId.getPlatformId(), this.nameEntryId.getEncodingId());
}
public void setName(byte[] nameBytes) {
this.nameBytes = Arrays.copyOf(nameBytes, nameBytes.length);
}
public void setName(byte[] nameBytes, int offset, int length) {
this.nameBytes = Arrays.copyOfRange(nameBytes, offset, offset + length);
}
}
/**
* An interface for a filter to use with the name entry iterator. This allows
* name entries to be iterated and only those acceptable to the filter will be returned.
*/
public interface NameEntryFilter {
/**
* Callback to determine if a name entry is acceptable.
* @param platformId platform id
* @param encodingId encoding id
* @param languageId language id
* @param nameId name id
* @return true if the name entry is acceptable; false otherwise
*/
boolean accept(int platformId, int encodingId, int languageId, int nameId);
}
protected class NameEntryIterator implements Iterator {
private int nameIndex = 0;
private NameEntryFilter filter = null;
private NameEntryIterator() {
// no filter - iterate all name entries
}
private NameEntryIterator(NameEntryFilter filter) {
this.filter = filter;
}
@Override
public boolean hasNext() {
if (this.filter == null) {
if (this.nameIndex < nameCount()) {
return true;
}
return false;
}
for (; this.nameIndex < nameCount(); this.nameIndex++) {
if (filter.accept(
platformId(this.nameIndex), encodingId(this.nameIndex),
languageId(this.nameIndex), nameId(this.nameIndex))) {
return true;
}
}
return false;
}
@Override
public NameEntry next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
return nameEntry(this.nameIndex++);
}
@Override
public void remove() {
throw new UnsupportedOperationException("Cannot remove a CMap table from an existing font.");
}
}
@Override
public Iterator iterator() {
return new NameEntryIterator();
}
/**
* Get an iterator over name entries in the name table.
* @param filter a filter to select acceptable name entries
* @return an iterator over name entries
*/
public Iterator iterator(NameEntryFilter filter) {
return new NameEntryIterator(filter);
}
// TODO(stuartg): do this in the encoding enums
private static String getEncodingName(int platformId, int encodingId) {
String encodingName = null;
switch (PlatformId.valueOf(platformId)) {
case Unicode:
encodingName = "UTF-16BE";
break;
case Macintosh:
switch (MacintoshEncodingId.valueOf(encodingId)) {
case Roman:
encodingName = "MacRoman";
break;
case Japanese:
encodingName = "Shift_JIS";
break;
case ChineseTraditional:
encodingName = "Big5";
break;
case Korean:
encodingName = "EUC-KR";
break;
case Arabic:
encodingName = "MacArabic";
break;
case Hebrew:
encodingName = "MacHebrew";
break;
case Greek:
encodingName = "MacGreek";
break;
case Russian:
encodingName = "MacCyrillic";
break;
case RSymbol:
encodingName = "MacSymbol";
break;
case Devanagari:
break;
case Gurmukhi:
break;
case Gujarati:
break;
case Oriya:
break;
case Bengali:
break;
case Tamil:
break;
case Telugu:
break;
case Kannada:
break;
case Malayalam:
break;
case Sinhalese:
break;
case Burmese:
break;
case Khmer:
break;
case Thai:
encodingName = "MacThai";
break;
case Laotian:
break;
case Georgian:
// TODO: ??? is it?
encodingName = "MacCyrillic";
break;
case Armenian:
break;
case ChineseSimplified:
encodingName = "EUC-CN";
break;
case Tibetan:
break;
case Mongolian:
// TODO: ??? is it?
encodingName = "MacCyrillic";
break;
case Geez:
break;
case Slavic:
// TODO: ??? is it?
encodingName = "MacCyrillic";
break;
case Vietnamese:
break;
case Sindhi:
break;
case Uninterpreted:
break;
}
break;
case ISO:
break;
case Windows:
switch (WindowsEncodingId.valueOf(encodingId)) {
case Symbol:
encodingName = "UTF-16BE";
break;
case UnicodeUCS2:
encodingName = "UTF-16BE";
break;
case ShiftJIS:
encodingName = "windows-933";
break;
case PRC:
encodingName = "windows-936";
break;
case Big5:
encodingName = "windows-950";
break;
case Wansung:
encodingName = "windows-949";
break;
case Johab:
encodingName = "ms1361";
break;
case UnicodeUCS4:
encodingName = "UCS-4";
break;
}
break;
case Custom:
break;
default:
break;
}
return encodingName;
}
// TODO: caching of charsets?
private static Charset getCharset(int platformId, int encodingId) {
String encodingName = NameTable.getEncodingName(platformId, encodingId);
if (encodingName == null) {
return null;
}
Charset charset = null;
try {
charset = CharsetICU.forNameICU(encodingName);
} catch (UnsupportedCharsetException e) {
return null;
}
return charset;
}
// TODO(stuartg):
// do the conversion by hand to detect conversion failures (i.e. no character in the encoding)
private static byte[] convertToNameBytes(String name, int platformId, int encodingId) {
Charset cs = NameTable.getCharset(platformId, encodingId);
if (cs == null) {
return null;
}
ByteBuffer bb = cs.encode(name);
return bb.array();
}
private static String convertFromNameBytes(byte[] nameBytes, int platformId, int encodingId) {
return NameTable.convertFromNameBytes(ByteBuffer.wrap(nameBytes), platformId, encodingId);
}
private static String convertFromNameBytes(ByteBuffer nameBytes, int platformId, int encodingId) {
Charset cs = NameTable.getCharset(platformId, encodingId);
if (cs == null) {
return Integer.toHexString(platformId);
}
CharBuffer cb = cs.decode(nameBytes);
return cb.toString();
}
public static class Builder extends SubTableContainerTable.Builder {
private Map nameEntryMap;
/**
* Create a new builder using the header information and data provided.
*
* @param header the header information
* @param data the data holding the table
* @return a new builder
*/
public static Builder createBuilder(Header header, WritableFontData data) {
return new Builder(header, data);
}
protected Builder(Header header, WritableFontData data) {
super(header, data);
}
protected Builder(Header header, ReadableFontData data) {
super(header, data);
}
private void initialize(ReadableFontData data) {
this.nameEntryMap = new TreeMap();
if (data != null) {
NameTable table = new NameTable(this.header(), data);
Iterator nameIter = table.iterator();
while (nameIter.hasNext()) {
NameEntry nameEntry = nameIter.next();
NameEntryBuilder nameEntryBuilder = new NameEntryBuilder(nameEntry);
this.nameEntryMap.put(nameEntryBuilder.getNameEntryId(), nameEntryBuilder);
}
}
}
private Map getNameBuilders() {
if (this.nameEntryMap == null) {
this.initialize(super.internalReadData());
}
super.setModelChanged();
return this.nameEntryMap;
}
/**
* Revert the name builders for the name table to the last version that came
* from data.
*/
public void revertNames() {
this.nameEntryMap = null;
this.setModelChanged(false);
}
public int builderCount() {
return this.getNameBuilders().size();
}
/**
* Clear the name builders for the name table.
*/
public void clear() {
this.getNameBuilders().clear();
}
public boolean has(int platformId, int encodingId, int languageId, int nameId) {
NameEntryId probe = new NameEntryId(platformId, encodingId, languageId, nameId);
return this.getNameBuilders().containsKey(probe);
}
public NameEntryBuilder nameBuilder(
int platformId, int encodingId, int languageId, int nameId) {
NameEntryId probe = new NameEntryId(platformId, encodingId, languageId, nameId);
NameEntryBuilder builder = this.getNameBuilders().get(probe);
if (builder == null) {
builder = new NameEntryBuilder(probe);
this.getNameBuilders().put(probe, builder);
}
return builder;
}
public boolean remove(int platformId, int encodingId, int languageId, int nameId) {
NameEntryId probe = new NameEntryId(platformId, encodingId, languageId, nameId);
return (this.getNameBuilders().remove(probe) != null);
}
// subclass API implementation
@Override
protected NameTable subBuildTable(ReadableFontData data) {
return new NameTable(this.header(), data);
}
@Override
protected void subDataSet() {
this.nameEntryMap = null;
super.setModelChanged(false);
}
@Override
protected int subDataSizeToSerialize() {
if (this.nameEntryMap == null || this.nameEntryMap.size() == 0) {
return 0;
}
int size = NameTable.Offset.nameRecordStart.offset + this.nameEntryMap.size()
* NameTable.Offset.nameRecordSize.offset;
for (Map.Entry entry : this.nameEntryMap.entrySet()) {
size += entry.getValue().nameAsBytes().length;
}
return size;
}
@Override
protected boolean subReadyToSerialize() {
if (this.nameEntryMap == null || this.nameEntryMap.size() == 0) {
return false;
}
return true;
}
@Override
protected int subSerialize(WritableFontData newData) {
int stringTableStartOffset =
NameTable.Offset.nameRecordStart.offset + this.nameEntryMap.size()
* NameTable.Offset.nameRecordSize.offset;
// header
newData.writeUShort(NameTable.Offset.format.offset, 0);
newData.writeUShort(NameTable.Offset.count.offset, this.nameEntryMap.size());
newData.writeUShort(NameTable.Offset.stringOffset.offset, stringTableStartOffset);
int nameRecordOffset = NameTable.Offset.nameRecordStart.offset;
int stringOffset = 0;
for (Map.Entry entry : this.nameEntryMap.entrySet()) {
// lookup table
newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordPlatformId.offset,
entry.getKey().getPlatformId());
newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordEncodingId.offset,
entry.getKey().getEncodingId());
newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordLanguageId.offset,
entry.getKey().getLanguageId());
newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordNameId.offset,
entry.getKey().getNameId());
newData.writeUShort(nameRecordOffset + NameTable.Offset.nameRecordStringLength.offset,
entry.getValue().nameAsBytes().length);
newData.writeUShort(
nameRecordOffset + NameTable.Offset.nameRecordStringOffset.offset, stringOffset);
nameRecordOffset += NameTable.Offset.nameRecordSize.offset;
// string table
byte[] nameBytes = entry.getValue().nameAsBytes();
if (nameBytes.length > 0) {
stringOffset += newData.writeBytes(
stringOffset + stringTableStartOffset, entry.getValue().nameAsBytes());
}
}
return stringOffset + stringTableStartOffset;
}
}
}