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