• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2010 Google Inc. All Rights Reserved.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.typography.font.sfntly.table.core;
18 
19 import com.google.typography.font.sfntly.Font.MacintoshEncodingId;
20 import com.google.typography.font.sfntly.Font.PlatformId;
21 import com.google.typography.font.sfntly.Font.WindowsEncodingId;
22 import com.google.typography.font.sfntly.data.ReadableFontData;
23 import com.google.typography.font.sfntly.data.WritableFontData;
24 import com.google.typography.font.sfntly.table.Header;
25 import com.google.typography.font.sfntly.table.SubTableContainerTable;
26 import com.google.typography.font.sfntly.table.core.CMap.CMapFormat;
27 
28 import java.io.IOException;
29 import java.util.HashMap;
30 import java.util.Iterator;
31 import java.util.Map;
32 import java.util.NoSuchElementException;
33 
34 /**
35  * A CMap table.
36  *
37  * @author Stuart Gill
38  */
39 public final class CMapTable extends SubTableContainerTable implements Iterable<CMap> {
40 
41   /**
42    * The .notdef glyph.
43    */
44   public static final int NOTDEF = 0;
45 
46   /**
47    * Offsets to specific elements in the underlying data. These offsets are relative to the
48    * start of the table or the start of sub-blocks within the table.
49    */
50   enum Offset {
51     version(0),
52     numTables(2),
53     encodingRecordStart(4),
54 
55     // offsets relative to the encoding record
56     encodingRecordPlatformId(0),
57     encodingRecordEncodingId(2),
58     encodingRecordOffset(4),
59     encodingRecordSize(8),
60 
61     format(0),
62 
63     // Format 0: Byte encoding table
64     format0Format(0),
65     format0Length(2),
66     format0Language(4),
67     format0GlyphIdArray(6),
68 
69     // Format 2: High-byte mapping through table
70     format2Format(0),
71     format2Length(2),
72     format2Language(4),
73     format2SubHeaderKeys(6),
74     format2SubHeaders(518),
75     // offset relative to the subHeader structure
76     format2SubHeader_firstCode(0),
77     format2SubHeader_entryCount(2),
78     format2SubHeader_idDelta(4),
79     format2SubHeader_idRangeOffset(6),
80     format2SubHeader_structLength(8),
81 
82     // Format 4: Segment mapping to delta values
83     format4Format(0),
84     format4Length(2),
85     format4Language(4),
86     format4SegCountX2(6),
87     format4SearchRange(8),
88     format4EntrySelector(10),
89     format4RangeShift(12),
90     format4EndCount(14),
91     format4FixedSize(16),
92 
93     // format 6: Trimmed table mapping
94     format6Format(0),
95     format6Length(2),
96     format6Language(4),
97     format6FirstCode(6),
98     format6EntryCount(8),
99     format6GlyphIdArray(10),
100 
101     // Format 8: mixed 16-bit and 32-bit coverage
102     format8Format(0),
103     format8Length(4),
104     format8Language(8),
105     format8Is32(12),
106     format8nGroups(8204),
107     format8Groups(8208),
108     // ofset relative to the group structure
109     format8Group_startCharCode(0),
110     format8Group_endCharCode(4),
111     format8Group_startGlyphId(8),
112     format8Group_structLength(12),
113 
114     // Format 10: Trimmed array
115     format10Format(0),
116     format10Length(4),
117     format10Language(8),
118     format10StartCharCode(12),
119     format10NumChars(16),
120     format10Glyphs(20),
121 
122     // Format 12: Segmented coverage
123     format12Format(0),
124     format12Length(4),
125     format12Language(8),
126     format12nGroups(12),
127     format12Groups(16),
128     format12Groups_structLength(12),
129     // offsets within the group structure
130     format12_startCharCode(0),
131     format12_endCharCode(4),
132     format12_startGlyphId(8),
133 
134     // Format 13: Last Resort Font
135     format13Format(0),
136     format13Length(4),
137     format13Language(8),
138     format13nGroups(12),
139     format13Groups(16),
140     format13Groups_structLength(12),
141     // offsets within the group structure
142     format13_startCharCode(0),
143     format13_endCharCode(4),
144     format13_glyphId(8),
145 
146     // TODO: finish support for format 14
147     // Format 14: Unicode Variation Sequences
148     format14Format(0),
149     format14Length(2);
150 
151     final int offset;
152 
Offset(int offset)153     private Offset(int offset) {
154       this.offset = offset;
155     }
156   }
157 
158   public static final class CMapId implements Comparable<CMapId> {
159 
160     public static final CMapId WINDOWS_BMP =
161         CMapId.getInstance(PlatformId.Windows.value(), WindowsEncodingId.UnicodeUCS2.value());
162     public static final CMapId WINDOWS_UCS4 =
163         CMapId.getInstance(PlatformId.Windows.value(), WindowsEncodingId.UnicodeUCS4.value());
164     public static final CMapId MAC_ROMAN =
165         CMapId.getInstance(PlatformId.Macintosh.value(), MacintoshEncodingId.Roman.value());
166 
getInstance(int platformId, int encodingId)167     public static CMapId getInstance(int platformId, int encodingId) {
168       return new CMapId(platformId, encodingId);
169     }
170 
171     private final int platformId;
172     private final int encodingId;
173 
CMapId(int platformId, int encodingId)174     private CMapId(int platformId, int encodingId) {
175       this.platformId = platformId;
176       this.encodingId = encodingId;
177     }
178 
platformId()179     public int platformId() {
180       return this.platformId;
181     }
182 
encodingId()183     public int encodingId() {
184       return this.encodingId;
185     }
186 
187     @Override
equals(Object obj)188     public boolean equals(Object obj) {
189       if (obj == this) {
190         return true;
191       }
192       if (!(obj instanceof CMapId)) {
193         return false;
194       }
195       CMapId otherKey = (CMapId) obj;
196       if ((otherKey.platformId == this.platformId) && (otherKey.encodingId == this.encodingId)) {
197         return true;
198       }
199       return false;
200     }
201 
202     @Override
hashCode()203     public int hashCode() {
204       return this.platformId << 8 | this.encodingId;
205     }
206 
207     @Override
compareTo(CMapId o)208     public int compareTo(CMapId o) {
209       return this.hashCode() - o.hashCode();
210     }
211 
212     @Override
toString()213     public String toString() {
214       StringBuilder b = new StringBuilder();
215       b.append("pid = ");
216       b.append(this.platformId);
217       b.append(", eid = ");
218       b.append(this.encodingId);
219       return b.toString();
220     }
221   }
222 
223   /**
224    * Constructor.
225    *
226    * @param header header for the table
227    * @param data data for the table
228    */
CMapTable(Header header, ReadableFontData data)229   private CMapTable(Header header, ReadableFontData data) {
230     super(header, data);
231   }
232 
233   /**
234    * Get the table version.
235    *
236    * @return table version
237    */
version()238   public int version() {
239     return this.data.readUShort(Offset.version.offset);
240   }
241 
242   /**
243    * Gets the number of cmaps within the CMap table.
244    *
245    * @return the number of cmaps
246    */
numCMaps()247   public int numCMaps() {
248     return this.data.readUShort(Offset.numTables.offset);
249   }
250 
251   /**
252    * Returns the index of the cmap with the given CMapId in the table or -1 if a cmap with the
253    * CMapId does not exist in the table.
254    *
255    * @param id the id of the cmap to get the index for; this value cannot be null
256    * @return the index of the cmap in the table or -1 if the cmap with the CMapId does not exist in
257    *         the table
258    */
259   // TODO Modify the iterator to be index-based and used here
getCmapIndex(CMapId id)260   public int getCmapIndex(CMapId id) {
261     for (int index = 0; index < numCMaps(); index++) {
262       if (id.equals(cmapId(index))) {
263         return index;
264       }
265     }
266 
267     return -1;
268   }
269 
270   /**
271    * Gets the offset in the table data for the encoding record for the cmap with
272    * the given index. The offset is from the beginning of the table.
273    *
274    * @param index the index of the cmap
275    * @return offset in the table data
276    */
offsetForEncodingRecord(int index)277   private static int offsetForEncodingRecord(int index) {
278     return Offset.encodingRecordStart.offset + index * Offset.encodingRecordSize.offset;
279   }
280 
281   /**
282    * Gets the cmap id for the cmap with the given index.
283    *
284    * @param index the index of the cmap
285    * @return the cmap id
286    */
cmapId(int index)287   public CMapId cmapId(int index) {
288     return CMapId.getInstance(platformId(index), encodingId(index));
289   }
290 
291   /**
292    * Gets the platform id for the cmap with the given index.
293    *
294    * @param index the index of the cmap
295    * @return the platform id
296    */
platformId(int index)297   public int platformId(int index) {
298     return this.data.readUShort(
299         Offset.encodingRecordPlatformId.offset + CMapTable.offsetForEncodingRecord(index));
300   }
301 
302   /**
303    * Gets the encoding id for the cmap with the given index.
304    *
305    * @param index the index of the cmap
306    * @return the encoding id
307    */
encodingId(int index)308   public int encodingId(int index) {
309     return this.data.readUShort(
310         Offset.encodingRecordEncodingId.offset + CMapTable.offsetForEncodingRecord(index));
311   }
312 
313   /**
314    * Gets the offset in the table data for the cmap table with the given index.
315    * The offset is from the beginning of the table.
316    *
317    * @param index the index of the cmap
318    * @return the offset in the table data
319    */
offset(int index)320   public int offset(int index) {
321     return this.data.readULongAsInt(
322         Offset.encodingRecordOffset.offset + CMapTable.offsetForEncodingRecord(index));
323   }
324 
325   /**
326    * Gets an iterator over all of the cmaps within this CMapTable.
327    */
328   @Override
iterator()329   public Iterator<CMap> iterator() {
330     return new CMapIterator();
331   }
332 
333   /**
334    * Gets an iterator over the cmaps within this CMap table using the provided
335    * filter to select the cmaps returned.
336    *
337    * @param filter the filter
338    * @return iterator over cmaps
339    */
iterator(CMapFilter filter)340   public Iterator<CMap> iterator(CMapFilter filter) {
341     return new CMapIterator(filter);
342   }
343 
344   @Override
toString()345   public String toString() {
346     StringBuilder sb = new StringBuilder(super.toString());
347     sb.append(" = { ");
348     for (int i = 0; i < this.numCMaps(); i++) {
349       CMap cmap;
350       try {
351         cmap = this.cmap(i);
352       } catch (IOException e) {
353         continue;
354       }
355       sb.append("[0x");
356       sb.append(Integer.toHexString(this.offset(i)));
357       sb.append(" = ");
358       sb.append(cmap);
359       if (i < this.numCMaps() - 1) {
360         sb.append("], ");
361       } else {
362         sb.append("]");
363       }
364     }
365     sb.append(" }");
366     return sb.toString();
367   }
368 
369   /**
370    * A filter on cmaps.
371    */
372   public interface CMapFilter {
373     /**
374      * Test on whether the cmap is acceptable or not.
375      *
376      * @param cmapId the id of the cmap
377      * @return true if the cmap is acceptable; false otherwise
378      */
accept(CMapId cmapId)379     boolean accept(CMapId cmapId);
380   }
381 
382   private class CMapIterator implements Iterator<CMap> {
383     private int tableIndex = 0;
384     private CMapFilter filter;
385 
CMapIterator()386     private CMapIterator() {
387       // no filter - iterate over all cmap subtables
388     }
389 
CMapIterator(CMapFilter filter)390     private CMapIterator(CMapFilter filter) {
391       this.filter = filter;
392     }
393 
394     @Override
hasNext()395     public boolean hasNext() {
396       if (this.filter == null) {
397         if (this.tableIndex < numCMaps()) {
398           return true;
399         }
400         return false;
401       }
402       for (; this.tableIndex < numCMaps(); this.tableIndex++) {
403         if (filter.accept(cmapId(this.tableIndex))) {
404           return true;
405         }
406       }
407       return false;
408     }
409 
410     @Override
next()411     public CMap next() {
412       if (!hasNext()) {
413         throw new NoSuchElementException();
414       }
415       try {
416         return cmap(this.tableIndex++);
417       } catch (IOException e) {
418         NoSuchElementException newException =
419             new NoSuchElementException("Error during the creation of the CMap.");
420         newException.initCause(e);
421         throw newException;
422       }
423     }
424 
425     @Override
remove()426     public void remove() {
427       throw new UnsupportedOperationException("Cannot remove a CMap table from an existing font.");
428     }
429   }
430 
431   /**
432    * Gets the cmap for the given index.
433    *
434    * @param index the index of the cmap
435    * @return the cmap at the index
436    * @throws IOException
437    */
cmap(int index)438   public CMap cmap(int index) throws IOException {
439     CMap.Builder<? extends CMap> builder =
440         CMapTable.Builder.cmapBuilder(this.readFontData(), index);
441     return builder.build();
442   }
443 
444   /**
445    * Gets the cmap with the given ids if it exists.
446    *
447    * @param platformId the platform id
448    * @param encodingId the encoding id
449    * @return the cmap if it exists; null otherwise
450    */
cmap(int platformId, int encodingId)451   public CMap cmap(int platformId, int encodingId) {
452     return cmap(CMapId.getInstance(platformId, encodingId));
453   }
454 
cmap(final CMapId cmapId)455   public CMap cmap(final CMapId cmapId) {
456     Iterator<CMap> cmapIter = this.iterator(new CMapFilter() {
457       @Override
458       public boolean accept(CMapId foundCMapId) {
459         if (cmapId.equals(foundCMapId)) {
460           return true;
461         }
462         return false;
463       }
464     });
465     // can only be one cmap for each set of ids
466     if (cmapIter.hasNext()) {
467       return cmapIter.next();
468     }
469     return null;
470   }
471 
472   /**
473    * CMap Table Builder.
474    *
475    */
476   public static class Builder extends SubTableContainerTable.Builder<CMapTable> {
477 
478     private int version = 0; // TODO(stuartg): make a CMapTable constant
479     private Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders;
480 
481     /**
482      * Creates a new builder using the header information and data provided.
483      *
484      * @param header the header information
485      * @param data the data holding the table
486      * @return a new builder
487      */
createBuilder(Header header, WritableFontData data)488     public static Builder createBuilder(Header header, WritableFontData data) {
489       return new Builder(header, data);
490     }
491 
492     /**
493      * Constructor.
494      *
495      * @param header the table header
496      * @param data the writable data for the table
497      */
Builder(Header header, WritableFontData data)498     protected Builder(Header header, WritableFontData data) {
499       super(header, data);
500     }
501 
502     /**
503      * Constructor. This constructor will try to maintain the data as readable
504      * but if editing operations are attempted then a writable copy will be made
505      * the readable data will be discarded.
506      *
507      * @param header the table header
508      * @param data the readable data for the table
509      */
Builder(Header header, ReadableFontData data)510     protected Builder(Header header, ReadableFontData data) {
511       super(header, data);
512     }
513 
514     /**
515      * Static factory method to create a cmap subtable builder.
516      *
517      * @param data the data for the whole cmap table
518      * @param index the index of the cmap subtable within the table
519      * @return the cmap subtable requested if it exists; null otherwise
520      */
cmapBuilder(ReadableFontData data, int index)521     protected static CMap.Builder<? extends CMap> cmapBuilder(ReadableFontData data, int index) {
522       if (index < 0 || index > numCMaps(data)) {
523         throw new IndexOutOfBoundsException(
524             "CMap table is outside the bounds of the known tables.");
525       }
526 
527       // read from encoding records
528       int platformId = data.readUShort(
529           Offset.encodingRecordPlatformId.offset + CMapTable.offsetForEncodingRecord(index));
530       int encodingId = data.readUShort(
531           Offset.encodingRecordEncodingId.offset + CMapTable.offsetForEncodingRecord(index));
532       int offset = data.readULongAsInt(
533           Offset.encodingRecordOffset.offset + CMapTable.offsetForEncodingRecord(index));
534       CMapId cmapId = CMapId.getInstance(platformId, encodingId);
535 
536       CMap.Builder<? extends CMap> builder = CMap.Builder.getBuilder(data, offset, cmapId);
537       return builder;
538     }
539 
540     @Override
subDataSet()541     protected void subDataSet() {
542       this.cmapBuilders = null;
543       super.setModelChanged(false);
544     }
545 
initialize(ReadableFontData data)546     private void initialize(ReadableFontData data) {
547       this.cmapBuilders = new /*TreeMap*/ HashMap<CMapId, CMap.Builder<? extends CMap>>();
548 
549       int numCMaps = numCMaps(data);
550       for (int i = 0; i < numCMaps; i++) {
551         CMap.Builder<? extends CMap> cmapBuilder = cmapBuilder(data, i);
552         cmapBuilders.put(cmapBuilder.cmapId(), cmapBuilder);
553       }
554     }
555 
getCMapBuilders()556     private Map<CMapId, CMap.Builder<? extends CMap>> getCMapBuilders() {
557       if (this.cmapBuilders != null) {
558         return this.cmapBuilders;
559       }
560       this.initialize(this.internalReadData());
561       this.setModelChanged();
562 
563       return this.cmapBuilders;
564     }
565 
numCMaps(ReadableFontData data)566     private static int numCMaps(ReadableFontData data) {
567       if (data == null) {
568         return 0;
569       }
570       return data.readUShort(Offset.numTables.offset);
571     }
572 
numCMaps()573     public int numCMaps() {
574       return this.getCMapBuilders().size();
575     }
576 
577     @Override
subDataSizeToSerialize()578     protected int subDataSizeToSerialize() {
579       if (this.cmapBuilders == null || this.cmapBuilders.size() == 0) {
580         return 0;
581       }
582 
583       boolean variable = false;
584       int size = CMapTable.Offset.encodingRecordStart.offset + this.cmapBuilders.size()
585       * CMapTable.Offset.encodingRecordSize.offset;
586 
587       // calculate size of each table
588       for (CMap.Builder<? extends CMap> b : this.cmapBuilders.values()) {
589         int cmapSize = b.subDataSizeToSerialize();
590         size += Math.abs(cmapSize);
591         variable |= cmapSize <= 0;
592       }
593       return variable ? -size : size;
594     }
595 
596     @Override
subReadyToSerialize()597     protected boolean subReadyToSerialize() {
598       if (this.cmapBuilders == null) {
599         return false;
600       }
601       // check each table
602       for (CMap.Builder<? extends CMap> b : this.cmapBuilders.values()) {
603         if (!b.subReadyToSerialize()) {
604           return false;
605         }
606       }
607       return true;
608     }
609 
610     @Override
subSerialize(WritableFontData newData)611     protected int subSerialize(WritableFontData newData) {
612       int size = newData.writeUShort(CMapTable.Offset.version.offset, this.version());
613       size += newData.writeUShort(CMapTable.Offset.numTables.offset, this.cmapBuilders.size());
614 
615       int indexOffset = size;
616       size += this.cmapBuilders.size() * CMapTable.Offset.encodingRecordSize.offset;
617       for (CMap.Builder<? extends CMap> b : this.cmapBuilders.values()) {
618         // header entry
619         indexOffset += newData.writeUShort(indexOffset, b.platformId());
620         indexOffset += newData.writeUShort(indexOffset, b.encodingId());
621         indexOffset += newData.writeULong(indexOffset, size);
622 
623         // cmap
624         size += b.subSerialize(newData.slice(size));
625       }
626       return size;
627     }
628 
629     @Override
subBuildTable(ReadableFontData data)630     protected CMapTable subBuildTable(ReadableFontData data) {
631       return new CMapTable(this.header(), data);
632     }
633 
634     // public building API
635 
iterator()636     public Iterator<? extends CMap.Builder<? extends CMap>> iterator() {
637       return this.getCMapBuilders().values().iterator();
638     }
639 
version()640     public int version() {
641       return this.version;
642     }
643 
setVersion(int version)644     public void setVersion(int version) {
645       this.version = version;
646     }
647 
648     /**
649      * Gets a new cmap builder for this cmap table. The new cmap builder will be
650      * for the cmap id specified and initialized with the data given. The data
651      * will be copied and the original data will not be modified.
652      *
653      * @param cmapId the id for the new cmap builder
654      * @param data the data to copy for the new cmap builder
655      * @return a new cmap builder initialized with the cmap id and a copy of the
656      *         data
657      * @throws IOException
658      */
newCMapBuilder(CMapId cmapId, ReadableFontData data)659     public CMap.Builder<? extends CMap> newCMapBuilder(CMapId cmapId, ReadableFontData data)
660         throws IOException {
661       WritableFontData wfd = WritableFontData.createWritableFontData(data.size());
662       data.copyTo(wfd);
663       CMap.Builder<? extends CMap> builder = CMap.Builder.getBuilder(wfd, 0, cmapId);
664       Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders = this.getCMapBuilders();
665       cmapBuilders.put(cmapId, builder);
666       return builder;
667     }
668 
newCMapBuilder(CMapId cmapId, CMapFormat cmapFormat)669     public CMap.Builder<? extends CMap> newCMapBuilder(CMapId cmapId, CMapFormat cmapFormat) {
670       CMap.Builder<? extends CMap> builder = CMap.Builder.getBuilder(cmapFormat, cmapId);
671       Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders = this.getCMapBuilders();
672       cmapBuilders.put(cmapId, builder);
673       return builder;
674     }
675 
cmapBuilder(CMapId cmapId)676     public CMap.Builder<? extends CMap> cmapBuilder(CMapId cmapId) {
677       Map<CMapId, CMap.Builder<? extends CMap>> cmapBuilders = this.getCMapBuilders();
678       return cmapBuilders.get(cmapId);
679     }
680 
681   }
682 }
683