1 /* 2 * Licensed to the Apache Software Foundation (ASF) under one or more 3 * contributor license agreements. See the NOTICE file distributed with 4 * this work for additional information regarding copyright ownership. 5 * The ASF licenses this file to You under the Apache License, Version 2.0 6 * (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 /* $Id: MultiByteFont.java 1885366 2021-01-11 15:00:20Z ssteiner $ */ 19 20 package org.apache.fop.fonts; 21 22 import java.awt.Rectangle; 23 import java.io.InputStream; 24 import java.nio.CharBuffer; 25 import java.nio.IntBuffer; 26 import java.util.ArrayList; 27 import java.util.BitSet; 28 import java.util.LinkedHashMap; 29 import java.util.List; 30 import java.util.Map; 31 32 import org.apache.commons.logging.Log; 33 import org.apache.commons.logging.LogFactory; 34 35 import org.apache.fop.apps.io.InternalResourceResolver; 36 import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; 37 import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; 38 import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable; 39 import org.apache.fop.complexscripts.fonts.GlyphTable; 40 import org.apache.fop.complexscripts.fonts.Positionable; 41 import org.apache.fop.complexscripts.fonts.Substitutable; 42 import org.apache.fop.complexscripts.util.CharAssociation; 43 import org.apache.fop.complexscripts.util.CharNormalize; 44 import org.apache.fop.complexscripts.util.GlyphSequence; 45 import org.apache.fop.fonts.truetype.SVGGlyphData; 46 import org.apache.fop.util.CharUtilities; 47 48 /** 49 * Generic MultiByte (CID) font 50 */ 51 public class MultiByteFont extends CIDFont implements Substitutable, Positionable { 52 53 /** logging instance */ 54 private static final Log log 55 = LogFactory.getLog(MultiByteFont.class); 56 57 private String ttcName; 58 private String encoding = "Identity-H"; 59 60 private int defaultWidth; 61 private CIDFontType cidType = CIDFontType.CIDTYPE2; 62 63 protected final CIDSet cidSet; 64 65 /* advanced typographic support */ 66 private GlyphDefinitionTable gdef; 67 private GlyphSubstitutionTable gsub; 68 private GlyphPositioningTable gpos; 69 70 /* dynamic private use (character) mappings */ 71 private int numMapped; 72 private int numUnmapped; 73 private int nextPrivateUse = 0xE000; 74 private int firstPrivate; 75 private int lastPrivate; 76 private int firstUnmapped; 77 private int lastUnmapped; 78 79 /** Contains the character bounding boxes for all characters in the font */ 80 protected Rectangle[] boundingBoxes; 81 82 private boolean isOTFFile; 83 84 // since for most users the most likely glyphs are in the first cmap segments we store their mapping. 85 private static final int NUM_MOST_LIKELY_GLYPHS = 256; 86 private int[] mostLikelyGlyphs = new int[NUM_MOST_LIKELY_GLYPHS]; 87 88 //A map to store each used glyph from the CID set against the glyph name. 89 private LinkedHashMap<Integer, String> usedGlyphNames = new LinkedHashMap<Integer, String>(); 90 91 /** 92 * Default constructor 93 */ MultiByteFont(InternalResourceResolver resourceResolver, EmbeddingMode embeddingMode)94 public MultiByteFont(InternalResourceResolver resourceResolver, EmbeddingMode embeddingMode) { 95 super(resourceResolver); 96 setFontType(FontType.TYPE0); 97 setEmbeddingMode(embeddingMode); 98 if (embeddingMode != EmbeddingMode.FULL) { 99 cidSet = new CIDSubset(this); 100 } else { 101 cidSet = new CIDFull(this); 102 } 103 } 104 105 /** {@inheritDoc} */ 106 @Override getDefaultWidth()107 public int getDefaultWidth() { 108 return defaultWidth; 109 } 110 111 /** {@inheritDoc} */ 112 @Override getRegistry()113 public String getRegistry() { 114 return "Adobe"; 115 } 116 117 /** {@inheritDoc} */ 118 @Override getOrdering()119 public String getOrdering() { 120 return "UCS"; 121 } 122 123 /** {@inheritDoc} */ 124 @Override getSupplement()125 public int getSupplement() { 126 return 0; 127 } 128 129 /** {@inheritDoc} */ 130 @Override getCIDType()131 public CIDFontType getCIDType() { 132 return cidType; 133 } 134 setIsOTFFile(boolean isOTFFile)135 public void setIsOTFFile(boolean isOTFFile) { 136 this.isOTFFile = isOTFFile; 137 } 138 isOTFFile()139 public boolean isOTFFile() { 140 return this.isOTFFile; 141 } 142 143 /** 144 * Sets the CIDType. 145 * @param cidType The cidType to set 146 */ setCIDType(CIDFontType cidType)147 public void setCIDType(CIDFontType cidType) { 148 this.cidType = cidType; 149 } 150 151 /** {@inheritDoc} */ 152 @Override getEmbedFontName()153 public String getEmbedFontName() { 154 if (isEmbeddable()) { 155 return FontUtil.stripWhiteSpace(super.getFontName()); 156 } else { 157 return super.getFontName(); 158 } 159 } 160 161 /** {@inheritDoc} */ isEmbeddable()162 public boolean isEmbeddable() { 163 return !(getEmbedFileURI() == null && getEmbedResourceName() == null); 164 } 165 isSubsetEmbedded()166 public boolean isSubsetEmbedded() { 167 if (getEmbeddingMode() == EmbeddingMode.FULL) { 168 return false; 169 } 170 return true; 171 } 172 173 /** {@inheritDoc} */ 174 @Override getCIDSet()175 public CIDSet getCIDSet() { 176 return this.cidSet; 177 } 178 mapUsedGlyphName(int gid, String value)179 public void mapUsedGlyphName(int gid, String value) { 180 usedGlyphNames.put(gid, value); 181 } 182 getUsedGlyphNames()183 public LinkedHashMap<Integer, String> getUsedGlyphNames() { 184 return usedGlyphNames; 185 } 186 187 /** {@inheritDoc} */ 188 @Override getEncodingName()189 public String getEncodingName() { 190 return encoding; 191 } 192 193 /** {@inheritDoc} */ getWidth(int i, int size)194 public int getWidth(int i, int size) { 195 if (isEmbeddable()) { 196 int glyphIndex = cidSet.getOriginalGlyphIndex(i); 197 return size * width[glyphIndex]; 198 } else { 199 return size * width[i]; 200 } 201 } 202 203 /** {@inheritDoc} */ getWidths()204 public int[] getWidths() { 205 int[] arr = new int[width.length]; 206 System.arraycopy(width, 0, arr, 0, width.length); 207 return arr; 208 } 209 getBoundingBox(int glyphIndex, int size)210 public Rectangle getBoundingBox(int glyphIndex, int size) { 211 int index = isEmbeddable() ? cidSet.getOriginalGlyphIndex(glyphIndex) : glyphIndex; 212 Rectangle bbox = boundingBoxes[index]; 213 return new Rectangle(bbox.x * size, bbox.y * size, bbox.width * size, bbox.height * size); 214 } 215 216 /** 217 * Returns the glyph index for a Unicode character. The method returns 0 if there's no 218 * such glyph in the character map. 219 * @param c the Unicode character index 220 * @return the glyph index (or 0 if the glyph is not available) 221 */ 222 // [TBD] - needs optimization, i.e., change from linear search to binary search findGlyphIndex(int c)223 public int findGlyphIndex(int c) { 224 int idx = c; 225 int retIdx = SingleByteEncoding.NOT_FOUND_CODE_POINT; 226 227 // for most users the most likely glyphs are in the first cmap segments (meaning the one with 228 // the lowest unicode start values) 229 if (idx < NUM_MOST_LIKELY_GLYPHS && mostLikelyGlyphs[idx] != 0) { 230 return mostLikelyGlyphs[idx]; 231 } 232 for (CMapSegment i : cmap) { 233 if (retIdx == 0 234 && i.getUnicodeStart() <= idx 235 && i.getUnicodeEnd() >= idx) { 236 retIdx = i.getGlyphStartIndex() 237 + idx 238 - i.getUnicodeStart(); 239 if (idx < NUM_MOST_LIKELY_GLYPHS) { 240 mostLikelyGlyphs[idx] = retIdx; 241 } 242 if (retIdx != 0) { 243 break; 244 } 245 } 246 } 247 return retIdx; 248 } 249 250 /** 251 * Add a private use mapping {PU,GI} to the existing character map. 252 * N.B. Does not insert in order, merely appends to end of existing map. 253 */ addPrivateUseMapping(int pu, int gi)254 protected synchronized void addPrivateUseMapping(int pu, int gi) { 255 assert findGlyphIndex(pu) == SingleByteEncoding.NOT_FOUND_CODE_POINT; 256 cmap.add(new CMapSegment(pu, pu, gi)); 257 } 258 259 /** 260 * Given a glyph index, create a new private use mapping, augmenting the bfentries 261 * table. This is needed to accommodate the presence of an (output) glyph index in a 262 * complex script glyph substitution that does not correspond to a character in the 263 * font's CMAP. The creation of such private use mappings is deferred until an 264 * attempt is actually made to perform the reverse lookup from the glyph index. This 265 * is necessary in order to avoid exhausting the private use space on fonts containing 266 * many such non-mapped glyph indices, if these mappings had been created statically 267 * at font load time. 268 * @param gi glyph index 269 * @returns unicode scalar value 270 */ createPrivateUseMapping(int gi)271 private int createPrivateUseMapping(int gi) { 272 while ((nextPrivateUse < 0xF900) 273 && (findGlyphIndex(nextPrivateUse) != SingleByteEncoding.NOT_FOUND_CODE_POINT)) { 274 nextPrivateUse++; 275 } 276 if (nextPrivateUse < 0xF900) { 277 int pu = nextPrivateUse; 278 addPrivateUseMapping(pu, gi); 279 if (firstPrivate == 0) { 280 firstPrivate = pu; 281 } 282 lastPrivate = pu; 283 numMapped++; 284 if (log.isDebugEnabled()) { 285 log.debug("Create private use mapping from " 286 + CharUtilities.format(pu) 287 + " to glyph index " + gi 288 + " in font '" + getFullName() + "'"); 289 } 290 return pu; 291 } else { 292 if (firstUnmapped == 0) { 293 firstUnmapped = gi; 294 } 295 lastUnmapped = gi; 296 numUnmapped++; 297 log.warn("Exhausted private use area: unable to map " 298 + numUnmapped + " glyphs in glyph index range [" 299 + firstUnmapped + "," + lastUnmapped 300 + "] (inclusive) of font '" + getFullName() + "'"); 301 return 0; 302 } 303 } 304 305 /** 306 * Returns the Unicode scalar value that corresponds to the glyph index. If more than 307 * one correspondence exists, then the first one is returned (ordered by bfentries[]). 308 * @param gi glyph index 309 * @return unicode scalar value 310 */ 311 // [TBD] - needs optimization, i.e., change from linear search to binary search findCharacterFromGlyphIndex(int gi, boolean augment)312 private int findCharacterFromGlyphIndex(int gi, boolean augment) { 313 int cc = 0; 314 for (CMapSegment segment : cmap) { 315 int s = segment.getGlyphStartIndex(); 316 int e = s + (segment.getUnicodeEnd() - segment.getUnicodeStart()); 317 if ((gi >= s) && (gi <= e)) { 318 cc = segment.getUnicodeStart() + (gi - s); 319 break; 320 } 321 } 322 if ((cc == 0) && augment) { 323 cc = createPrivateUseMapping(gi); 324 } 325 return cc; 326 } 327 findCharacterFromGlyphIndex(int gi)328 private int findCharacterFromGlyphIndex(int gi) { 329 return findCharacterFromGlyphIndex(gi, true); 330 } 331 getGlyphIndices()332 protected BitSet getGlyphIndices() { 333 BitSet bitset = new BitSet(); 334 bitset.set(0); 335 bitset.set(1); 336 bitset.set(2); 337 for (CMapSegment i : cmap) { 338 int start = i.getUnicodeStart(); 339 int end = i.getUnicodeEnd(); 340 int glyphIndex = i.getGlyphStartIndex(); 341 while (start++ < end + 1) { 342 bitset.set(glyphIndex++); 343 } 344 } 345 return bitset; 346 } 347 getChars()348 protected char[] getChars() { 349 // the width array is set when the font is built 350 char[] chars = new char[width.length]; 351 for (CMapSegment i : cmap) { 352 int start = i.getUnicodeStart(); 353 int end = i.getUnicodeEnd(); 354 int glyphIndex = i.getGlyphStartIndex(); 355 while (start < end + 1) { 356 chars[glyphIndex++] = (char) start++; 357 } 358 } 359 return chars; 360 } 361 362 /** {@inheritDoc} */ 363 @Override mapChar(char c)364 public char mapChar(char c) { 365 notifyMapOperation(); 366 int glyphIndex = findGlyphIndex(c); 367 if (glyphIndex == SingleByteEncoding.NOT_FOUND_CODE_POINT) { 368 warnMissingGlyph(c); 369 if (!isOTFFile) { 370 glyphIndex = findGlyphIndex(Typeface.NOT_FOUND); 371 } 372 } 373 if (isEmbeddable()) { 374 glyphIndex = cidSet.mapChar(glyphIndex, c); 375 } 376 if (isCID() && glyphIndex > 256) { 377 mapUnencodedChar(c); 378 } 379 return (char) glyphIndex; 380 } 381 382 /** {@inheritDoc} */ 383 @Override mapCodePoint(int cp)384 public int mapCodePoint(int cp) { 385 notifyMapOperation(); 386 int glyphIndex = findGlyphIndex(cp); 387 if (glyphIndex == SingleByteEncoding.NOT_FOUND_CODE_POINT) { 388 389 for (char ch : Character.toChars(cp)) { 390 //TODO better handling for non BMP 391 warnMissingGlyph(ch); 392 } 393 394 if (!isOTFFile) { 395 glyphIndex = findGlyphIndex(Typeface.NOT_FOUND); 396 } 397 } 398 if (isEmbeddable()) { 399 glyphIndex = cidSet.mapCodePoint(glyphIndex, cp); 400 } 401 return (char) glyphIndex; 402 } 403 404 /** {@inheritDoc} */ 405 @Override hasChar(char c)406 public boolean hasChar(char c) { 407 return hasCodePoint(c); 408 } 409 410 /** {@inheritDoc} */ 411 @Override hasCodePoint(int cp)412 public boolean hasCodePoint(int cp) { 413 return (findGlyphIndex(cp) != SingleByteEncoding.NOT_FOUND_CODE_POINT); 414 } 415 416 /** 417 * Sets the defaultWidth. 418 * @param defaultWidth The defaultWidth to set 419 */ setDefaultWidth(int defaultWidth)420 public void setDefaultWidth(int defaultWidth) { 421 this.defaultWidth = defaultWidth; 422 } 423 424 /** 425 * Returns the TrueType Collection Name. 426 * @return the TrueType Collection Name 427 */ getTTCName()428 public String getTTCName() { 429 return ttcName; 430 } 431 432 /** 433 * Sets the the TrueType Collection Name. 434 * @param ttcName the TrueType Collection Name 435 */ setTTCName(String ttcName)436 public void setTTCName(String ttcName) { 437 this.ttcName = ttcName; 438 } 439 440 /** 441 * Sets the width array. 442 * @param wds array of widths. 443 */ setWidthArray(int[] wds)444 public void setWidthArray(int[] wds) { 445 this.width = wds; 446 } 447 448 /** 449 * Sets the bounding boxes array. 450 * @param boundingBoxes array of bounding boxes. 451 */ setBBoxArray(Rectangle[] boundingBoxes)452 public void setBBoxArray(Rectangle[] boundingBoxes) { 453 this.boundingBoxes = boundingBoxes; 454 } 455 456 /** 457 * Returns a Map of used Glyphs. 458 * @return Map Map of used Glyphs 459 */ getUsedGlyphs()460 public Map<Integer, Integer> getUsedGlyphs() { 461 return cidSet.getGlyphs(); 462 } 463 464 /** 465 * Returns the character from it's original glyph index in the font 466 * @param glyphIndex The original index of the character 467 * @return The character 468 */ getUnicodeFromGID(int glyphIndex)469 public char getUnicodeFromGID(int glyphIndex) { 470 return cidSet.getUnicodeFromGID(glyphIndex); 471 } 472 473 /** 474 * Gets the original glyph index in the font from a character. 475 * @param ch The character 476 * @return The glyph index in the font 477 */ getGIDFromChar(char ch)478 public int getGIDFromChar(char ch) { 479 return cidSet.getGIDFromChar(ch); 480 } 481 482 /** 483 * Establishes the glyph definition table. 484 * @param gdef the glyph definition table to be used by this font 485 */ setGDEF(GlyphDefinitionTable gdef)486 public void setGDEF(GlyphDefinitionTable gdef) { 487 if ((this.gdef == null) || (gdef == null)) { 488 this.gdef = gdef; 489 } else { 490 throw new IllegalStateException("font already associated with GDEF table"); 491 } 492 } 493 494 /** 495 * Obtain glyph definition table. 496 * @return glyph definition table or null if none is associated with font 497 */ getGDEF()498 public GlyphDefinitionTable getGDEF() { 499 return gdef; 500 } 501 502 /** 503 * Establishes the glyph substitution table. 504 * @param gsub the glyph substitution table to be used by this font 505 */ setGSUB(GlyphSubstitutionTable gsub)506 public void setGSUB(GlyphSubstitutionTable gsub) { 507 if ((this.gsub == null) || (gsub == null)) { 508 this.gsub = gsub; 509 } else { 510 throw new IllegalStateException("font already associated with GSUB table"); 511 } 512 } 513 514 /** 515 * Obtain glyph substitution table. 516 * @return glyph substitution table or null if none is associated with font 517 */ getGSUB()518 public GlyphSubstitutionTable getGSUB() { 519 return gsub; 520 } 521 522 /** 523 * Establishes the glyph positioning table. 524 * @param gpos the glyph positioning table to be used by this font 525 */ setGPOS(GlyphPositioningTable gpos)526 public void setGPOS(GlyphPositioningTable gpos) { 527 if ((this.gpos == null) || (gpos == null)) { 528 this.gpos = gpos; 529 } else { 530 throw new IllegalStateException("font already associated with GPOS table"); 531 } 532 } 533 534 /** 535 * Obtain glyph positioning table. 536 * @return glyph positioning table or null if none is associated with font 537 */ getGPOS()538 public GlyphPositioningTable getGPOS() { 539 return gpos; 540 } 541 542 /** {@inheritDoc} */ performsSubstitution()543 public boolean performsSubstitution() { 544 return gsub != null; 545 } 546 547 /** {@inheritDoc} */ performSubstitution(CharSequence charSequence, String script, String language, List associations, boolean retainControls)548 public CharSequence performSubstitution(CharSequence charSequence, String script, String language, 549 List associations, boolean retainControls) { 550 if (gsub != null) { 551 charSequence = gsub.preProcess(charSequence, script, this, associations); 552 GlyphSequence glyphSequence = charSequenceToGlyphSequence(charSequence, associations); 553 GlyphSequence glyphSequenceSubstituted = gsub.substitute(glyphSequence, script, language); 554 if (associations != null) { 555 associations.clear(); 556 associations.addAll(glyphSequenceSubstituted.getAssociations()); 557 } 558 if (!retainControls) { 559 glyphSequenceSubstituted = elideControls(glyphSequenceSubstituted); 560 } 561 // may not contains all the characters that were in charSequence. 562 // see: #createPrivateUseMapping(int gi) 563 return mapGlyphsToChars(glyphSequenceSubstituted); 564 } else { 565 return charSequence; 566 } 567 } 568 charSequenceToGlyphSequence(CharSequence charSequence, List associations)569 public GlyphSequence charSequenceToGlyphSequence(CharSequence charSequence, List associations) { 570 CharSequence normalizedCharSequence = normalize(charSequence, associations); 571 return mapCharsToGlyphs(normalizedCharSequence, associations); 572 } 573 574 /** {@inheritDoc} */ reorderCombiningMarks( CharSequence cs, int[][] gpa, String script, String language, List associations)575 public CharSequence reorderCombiningMarks( 576 CharSequence cs, int[][] gpa, String script, String language, List associations) { 577 if (gdef != null) { 578 GlyphSequence igs = mapCharsToGlyphs(cs, associations); 579 GlyphSequence ogs = gdef.reorderCombiningMarks(igs, getUnscaledWidths(igs), gpa, script, language); 580 if (associations != null) { 581 associations.clear(); 582 associations.addAll(ogs.getAssociations()); 583 } 584 CharSequence ocs = mapGlyphsToChars(ogs); 585 return ocs; 586 } else { 587 return cs; 588 } 589 } 590 getUnscaledWidths(GlyphSequence gs)591 protected int[] getUnscaledWidths(GlyphSequence gs) { 592 int[] widths = new int[gs.getGlyphCount()]; 593 for (int i = 0, n = widths.length; i < n; ++i) { 594 if (i < width.length) { 595 widths[i] = width[i]; 596 } 597 } 598 return widths; 599 } 600 601 /** {@inheritDoc} */ performsPositioning()602 public boolean performsPositioning() { 603 return gpos != null; 604 } 605 606 /** {@inheritDoc} */ 607 public int[][] performPositioning(CharSequence cs, String script, String language, int fontSize)608 performPositioning(CharSequence cs, String script, String language, int fontSize) { 609 if (gpos != null) { 610 GlyphSequence gs = mapCharsToGlyphs(cs, null); 611 int[][] adjustments = new int [ gs.getGlyphCount() ] [ 4 ]; 612 if (gpos.position(gs, script, language, fontSize, this.width, adjustments)) { 613 return scaleAdjustments(adjustments, fontSize); 614 } else { 615 return null; 616 } 617 } else { 618 return null; 619 } 620 } 621 622 /** {@inheritDoc} */ performPositioning(CharSequence cs, String script, String language)623 public int[][] performPositioning(CharSequence cs, String script, String language) { 624 throw new UnsupportedOperationException(); 625 } 626 627 scaleAdjustments(int[][] adjustments, int fontSize)628 private int[][] scaleAdjustments(int[][] adjustments, int fontSize) { 629 if (adjustments != null) { 630 for (int[] gpa : adjustments) { 631 for (int k = 0; k < 4; k++) { 632 gpa[k] = (gpa[k] * fontSize) / 1000; 633 } 634 } 635 return adjustments; 636 } else { 637 return null; 638 } 639 } 640 641 /** 642 * Map sequence CS, comprising a sequence of UTF-16 encoded Unicode Code Points, to 643 * an output character sequence GS, comprising a sequence of Glyph Indices. N.B. Unlike 644 * mapChar(), this method does not make use of embedded subset encodings. 645 * @param cs a CharSequence containing UTF-16 encoded Unicode characters 646 * @returns a CharSequence containing glyph indices 647 */ mapCharsToGlyphs(CharSequence cs, List associations)648 private GlyphSequence mapCharsToGlyphs(CharSequence cs, List associations) { 649 IntBuffer cb = IntBuffer.allocate(cs.length()); 650 IntBuffer gb = IntBuffer.allocate(cs.length()); 651 int gi; 652 int giMissing = findGlyphIndex(Typeface.NOT_FOUND); 653 for (int i = 0, n = cs.length(); i < n; i++) { 654 int cc = cs.charAt(i); 655 if ((cc >= 0xD800) && (cc < 0xDC00)) { 656 if ((i + 1) < n) { 657 int sh = cc; 658 int sl = cs.charAt(++i); 659 if ((sl >= 0xDC00) && (sl < 0xE000)) { 660 cc = 0x10000 + ((sh - 0xD800) << 10) + ((sl - 0xDC00) << 0); 661 } else { 662 throw new IllegalArgumentException( 663 "ill-formed UTF-16 sequence, " 664 + "contains isolated high surrogate at index " + i); 665 } 666 } else { 667 throw new IllegalArgumentException( 668 "ill-formed UTF-16 sequence, " 669 + "contains isolated high surrogate at end of sequence"); 670 } 671 } else if ((cc >= 0xDC00) && (cc < 0xE000)) { 672 throw new IllegalArgumentException( 673 "ill-formed UTF-16 sequence, " 674 + "contains isolated low surrogate at index " + i); 675 } 676 notifyMapOperation(); 677 gi = findGlyphIndex(cc); 678 if (gi == SingleByteEncoding.NOT_FOUND_CODE_POINT) { 679 warnMissingGlyph((char) cc); 680 gi = giMissing; 681 } 682 cb.put(cc); 683 gb.put(gi); 684 } 685 cb.flip(); 686 gb.flip(); 687 if ((associations != null) && (associations.size() == cs.length())) { 688 associations = new java.util.ArrayList(associations); 689 } else { 690 associations = null; 691 } 692 return new GlyphSequence(cb, gb, associations); 693 } 694 695 /** 696 * Map sequence GS, comprising a sequence of Glyph Indices, to output sequence CS, 697 * comprising a sequence of UTF-16 encoded Unicode Code Points. 698 * @param gs a GlyphSequence containing glyph indices 699 * @returns a CharSequence containing UTF-16 encoded Unicode characters 700 */ mapGlyphsToChars(GlyphSequence gs)701 private CharSequence mapGlyphsToChars(GlyphSequence gs) { 702 int ng = gs.getGlyphCount(); 703 int ccMissing = Typeface.NOT_FOUND; 704 List<Character> chars = new ArrayList<Character>(gs.getUTF16CharacterCount()); 705 706 for (int i = 0, n = ng; i < n; i++) { 707 int gi = gs.getGlyph(i); 708 int cc = findCharacterFromGlyphIndex(gi); 709 if ((cc == 0) || (cc > 0x10FFFF)) { 710 cc = ccMissing; 711 log.warn("Unable to map glyph index " + gi 712 + " to Unicode scalar in font '" 713 + getFullName() + "', substituting missing character '" 714 + (char) cc + "'"); 715 } 716 if (cc > 0x00FFFF) { 717 int sh; 718 int sl; 719 cc -= 0x10000; 720 sh = ((cc >> 10) & 0x3FF) + 0xD800; 721 sl = ((cc >> 0) & 0x3FF) + 0xDC00; 722 chars.add((char) sh); 723 chars.add((char) sl); 724 } else { 725 chars.add((char) cc); 726 } 727 } 728 729 CharBuffer cb = CharBuffer.allocate(chars.size()); 730 731 for (char c : chars) { 732 cb.put(c); 733 } 734 735 cb.flip(); 736 return cb; 737 } 738 normalize(CharSequence cs, List associations)739 private CharSequence normalize(CharSequence cs, List associations) { 740 return hasDecomposable(cs) ? decompose(cs, associations) : cs; 741 } 742 hasDecomposable(CharSequence cs)743 private boolean hasDecomposable(CharSequence cs) { 744 for (int i = 0, n = cs.length(); i < n; i++) { 745 int cc = cs.charAt(i); 746 if (CharNormalize.isDecomposable(cc)) { 747 return true; 748 } 749 } 750 return false; 751 } 752 decompose(CharSequence cs, List associations)753 private CharSequence decompose(CharSequence cs, List associations) { 754 StringBuffer sb = new StringBuffer(cs.length()); 755 int[] daBuffer = new int[CharNormalize.maximumDecompositionLength()]; 756 for (int i = 0, n = cs.length(); i < n; i++) { 757 int cc = cs.charAt(i); 758 int[] da = CharNormalize.decompose(cc, daBuffer); 759 for (int aDa : da) { 760 if (aDa > 0) { 761 sb.append((char) aDa); 762 } else { 763 break; 764 } 765 } 766 } 767 return sb; 768 } 769 770 /** 771 * Removes the glyphs associated with elidable control characters. 772 * All the characters in an association must be elidable in order 773 * to remove the corresponding glyph. 774 * 775 * @param gs GlyphSequence that may contains the elidable glyphs 776 * @return GlyphSequence without the elidable glyphs 777 */ elideControls(GlyphSequence gs)778 private static GlyphSequence elideControls(GlyphSequence gs) { 779 if (hasElidableControl(gs)) { 780 int[] ca = gs.getCharacterArray(false); 781 IntBuffer ngb = IntBuffer.allocate(gs.getGlyphCount()); 782 List nal = new java.util.ArrayList(gs.getGlyphCount()); 783 for (int i = 0, n = gs.getGlyphCount(); i < n; ++i) { 784 CharAssociation a = gs.getAssociation(i); 785 int s = a.getStart(); 786 int e = a.getEnd(); 787 while (s < e) { 788 int ch = ca [ s ]; 789 if (!isElidableControl(ch)) { 790 break; 791 } else { 792 ++s; 793 } 794 } 795 // If there is at least one non-elidable character in the char 796 // sequence then the glyph/association is kept. 797 if (s != e) { 798 ngb.put(gs.getGlyph(i)); 799 nal.add(a); 800 } 801 } 802 ngb.flip(); 803 return new GlyphSequence(gs.getCharacters(), ngb, nal, gs.getPredications()); 804 } else { 805 return gs; 806 } 807 } 808 hasElidableControl(GlyphSequence gs)809 private static boolean hasElidableControl(GlyphSequence gs) { 810 int[] ca = gs.getCharacterArray(false); 811 for (int ch : ca) { 812 if (isElidableControl(ch)) { 813 return true; 814 } 815 } 816 return false; 817 } 818 isElidableControl(int ch)819 private static boolean isElidableControl(int ch) { 820 if (ch < 0x0020) { 821 return true; 822 } else if ((ch >= 0x80) && (ch < 0x00A0)) { 823 return true; 824 } else if ((ch >= 0x2000) && (ch <= 0x206F)) { 825 if ((ch >= 0x200B) && (ch <= 0x200F)) { 826 return true; 827 } else if ((ch >= 0x2028) && (ch <= 0x202E)) { 828 return true; 829 } else if (ch >= 0x2066) { 830 return true; 831 } else { 832 return ch == 0x2060; 833 } 834 } else { 835 return false; 836 } 837 } 838 839 @Override hasFeature(int tableType, String script, String language, String feature)840 public boolean hasFeature(int tableType, String script, String language, String feature) { 841 GlyphTable table; 842 if (tableType == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION) { 843 table = getGSUB(); 844 } else if (tableType == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING) { 845 table = getGPOS(); 846 } else if (tableType == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION) { 847 table = getGDEF(); 848 } else { 849 table = null; 850 } 851 return (table != null) && table.hasFeature(script, language, feature); 852 } 853 getWidthsMap()854 public Map<Integer, Integer> getWidthsMap() { 855 return null; 856 } 857 getCmapStream()858 public InputStream getCmapStream() { 859 return null; 860 } 861 getSVG(char c)862 public SVGGlyphData getSVG(char c) { 863 int gid = findGlyphIndex(c); 864 return svgs.get(gid); 865 } 866 } 867 868