| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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. |
| */ |
| |
| /* $Id$ */ |
| |
| package org.apache.fop.fonts; |
| |
| import java.awt.Rectangle; |
| import java.io.InputStream; |
| import java.nio.CharBuffer; |
| import java.nio.IntBuffer; |
| import java.util.ArrayList; |
| import java.util.BitSet; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| |
| import org.apache.fop.apps.io.InternalResourceResolver; |
| import org.apache.fop.complexscripts.fonts.GlyphDefinitionTable; |
| import org.apache.fop.complexscripts.fonts.GlyphPositioningTable; |
| import org.apache.fop.complexscripts.fonts.GlyphSubstitutionTable; |
| import org.apache.fop.complexscripts.fonts.GlyphTable; |
| import org.apache.fop.complexscripts.fonts.Positionable; |
| import org.apache.fop.complexscripts.fonts.Substitutable; |
| import org.apache.fop.complexscripts.util.CharAssociation; |
| import org.apache.fop.complexscripts.util.CharNormalize; |
| import org.apache.fop.complexscripts.util.GlyphSequence; |
| import org.apache.fop.util.CharUtilities; |
| |
| /** |
| * Generic MultiByte (CID) font |
| */ |
| public class MultiByteFont extends CIDFont implements Substitutable, Positionable { |
| |
| /** logging instance */ |
| private static final Log log |
| = LogFactory.getLog(MultiByteFont.class); |
| |
| private String ttcName; |
| private String encoding = "Identity-H"; |
| |
| private int defaultWidth; |
| private CIDFontType cidType = CIDFontType.CIDTYPE2; |
| |
| protected final CIDSet cidSet; |
| |
| /* advanced typographic support */ |
| private GlyphDefinitionTable gdef; |
| private GlyphSubstitutionTable gsub; |
| private GlyphPositioningTable gpos; |
| |
| /* dynamic private use (character) mappings */ |
| private int numMapped; |
| private int numUnmapped; |
| private int nextPrivateUse = 0xE000; |
| private int firstPrivate; |
| private int lastPrivate; |
| private int firstUnmapped; |
| private int lastUnmapped; |
| |
| /** Contains the character bounding boxes for all characters in the font */ |
| protected Rectangle[] boundingBoxes; |
| |
| private boolean isOTFFile; |
| |
| // since for most users the most likely glyphs are in the first cmap segments we store their mapping. |
| private static final int NUM_MOST_LIKELY_GLYPHS = 256; |
| private int[] mostLikelyGlyphs = new int[NUM_MOST_LIKELY_GLYPHS]; |
| |
| //A map to store each used glyph from the CID set against the glyph name. |
| private LinkedHashMap<Integer, String> usedGlyphNames = new LinkedHashMap<Integer, String>(); |
| |
| /** |
| * Default constructor |
| */ |
| public MultiByteFont(InternalResourceResolver resourceResolver, EmbeddingMode embeddingMode) { |
| super(resourceResolver); |
| setFontType(FontType.TYPE0); |
| setEmbeddingMode(embeddingMode); |
| if (embeddingMode != EmbeddingMode.FULL) { |
| cidSet = new CIDSubset(this); |
| } else { |
| cidSet = new CIDFull(this); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public int getDefaultWidth() { |
| return defaultWidth; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public String getRegistry() { |
| return "Adobe"; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public String getOrdering() { |
| return "UCS"; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public int getSupplement() { |
| return 0; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public CIDFontType getCIDType() { |
| return cidType; |
| } |
| |
| public void setIsOTFFile(boolean isOTFFile) { |
| this.isOTFFile = isOTFFile; |
| } |
| |
| public boolean isOTFFile() { |
| return this.isOTFFile; |
| } |
| |
| /** |
| * Sets the CIDType. |
| * @param cidType The cidType to set |
| */ |
| public void setCIDType(CIDFontType cidType) { |
| this.cidType = cidType; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public String getEmbedFontName() { |
| if (isEmbeddable()) { |
| return FontUtil.stripWhiteSpace(super.getFontName()); |
| } else { |
| return super.getFontName(); |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean isEmbeddable() { |
| return !(getEmbedFileURI() == null && getEmbedResourceName() == null); |
| } |
| |
| public boolean isSubsetEmbedded() { |
| if (getEmbeddingMode() == EmbeddingMode.FULL) { |
| return false; |
| } |
| return true; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public CIDSet getCIDSet() { |
| return this.cidSet; |
| } |
| |
| public void mapUsedGlyphName(int gid, String value) { |
| usedGlyphNames.put(gid, value); |
| } |
| |
| public LinkedHashMap<Integer, String> getUsedGlyphNames() { |
| return usedGlyphNames; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public String getEncodingName() { |
| return encoding; |
| } |
| |
| /** {@inheritDoc} */ |
| public int getWidth(int i, int size) { |
| if (isEmbeddable()) { |
| int glyphIndex = cidSet.getOriginalGlyphIndex(i); |
| return size * width[glyphIndex]; |
| } else { |
| return size * width[i]; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public int[] getWidths() { |
| int[] arr = new int[width.length]; |
| System.arraycopy(width, 0, arr, 0, width.length); |
| return arr; |
| } |
| |
| public Rectangle getBoundingBox(int glyphIndex, int size) { |
| int index = isEmbeddable() ? cidSet.getOriginalGlyphIndex(glyphIndex) : glyphIndex; |
| Rectangle bbox = boundingBoxes[index]; |
| return new Rectangle(bbox.x * size, bbox.y * size, bbox.width * size, bbox.height * size); |
| } |
| |
| /** |
| * Returns the glyph index for a Unicode character. The method returns 0 if there's no |
| * such glyph in the character map. |
| * @param c the Unicode character index |
| * @return the glyph index (or 0 if the glyph is not available) |
| */ |
| // [TBD] - needs optimization, i.e., change from linear search to binary search |
| public int findGlyphIndex(int c) { |
| int idx = c; |
| int retIdx = SingleByteEncoding.NOT_FOUND_CODE_POINT; |
| |
| // for most users the most likely glyphs are in the first cmap segments (meaning the one with |
| // the lowest unicode start values) |
| if (idx < NUM_MOST_LIKELY_GLYPHS && mostLikelyGlyphs[idx] != 0) { |
| return mostLikelyGlyphs[idx]; |
| } |
| for (CMapSegment i : cmap) { |
| if (retIdx == 0 |
| && i.getUnicodeStart() <= idx |
| && i.getUnicodeEnd() >= idx) { |
| retIdx = i.getGlyphStartIndex() |
| + idx |
| - i.getUnicodeStart(); |
| if (idx < NUM_MOST_LIKELY_GLYPHS) { |
| mostLikelyGlyphs[idx] = retIdx; |
| } |
| if (retIdx != 0) { |
| break; |
| } |
| } |
| } |
| return retIdx; |
| } |
| |
| /** |
| * Add a private use mapping {PU,GI} to the existing character map. |
| * N.B. Does not insert in order, merely appends to end of existing map. |
| */ |
| protected synchronized void addPrivateUseMapping(int pu, int gi) { |
| assert findGlyphIndex(pu) == SingleByteEncoding.NOT_FOUND_CODE_POINT; |
| cmap.add(new CMapSegment(pu, pu, gi)); |
| } |
| |
| /** |
| * Given a glyph index, create a new private use mapping, augmenting the bfentries |
| * table. This is needed to accommodate the presence of an (output) glyph index in a |
| * complex script glyph substitution that does not correspond to a character in the |
| * font's CMAP. The creation of such private use mappings is deferred until an |
| * attempt is actually made to perform the reverse lookup from the glyph index. This |
| * is necessary in order to avoid exhausting the private use space on fonts containing |
| * many such non-mapped glyph indices, if these mappings had been created statically |
| * at font load time. |
| * @param gi glyph index |
| * @returns unicode scalar value |
| */ |
| private int createPrivateUseMapping(int gi) { |
| while ((nextPrivateUse < 0xF900) |
| && (findGlyphIndex(nextPrivateUse) != SingleByteEncoding.NOT_FOUND_CODE_POINT)) { |
| nextPrivateUse++; |
| } |
| if (nextPrivateUse < 0xF900) { |
| int pu = nextPrivateUse; |
| addPrivateUseMapping(pu, gi); |
| if (firstPrivate == 0) { |
| firstPrivate = pu; |
| } |
| lastPrivate = pu; |
| numMapped++; |
| if (log.isDebugEnabled()) { |
| log.debug("Create private use mapping from " |
| + CharUtilities.format(pu) |
| + " to glyph index " + gi |
| + " in font '" + getFullName() + "'"); |
| } |
| return pu; |
| } else { |
| if (firstUnmapped == 0) { |
| firstUnmapped = gi; |
| } |
| lastUnmapped = gi; |
| numUnmapped++; |
| log.warn("Exhausted private use area: unable to map " |
| + numUnmapped + " glyphs in glyph index range [" |
| + firstUnmapped + "," + lastUnmapped |
| + "] (inclusive) of font '" + getFullName() + "'"); |
| return 0; |
| } |
| } |
| |
| /** |
| * Returns the Unicode scalar value that corresponds to the glyph index. If more than |
| * one correspondence exists, then the first one is returned (ordered by bfentries[]). |
| * @param gi glyph index |
| * @return unicode scalar value |
| */ |
| // [TBD] - needs optimization, i.e., change from linear search to binary search |
| private int findCharacterFromGlyphIndex(int gi, boolean augment) { |
| int cc = 0; |
| for (CMapSegment segment : cmap) { |
| int s = segment.getGlyphStartIndex(); |
| int e = s + (segment.getUnicodeEnd() - segment.getUnicodeStart()); |
| if ((gi >= s) && (gi <= e)) { |
| cc = segment.getUnicodeStart() + (gi - s); |
| break; |
| } |
| } |
| if ((cc == 0) && augment) { |
| cc = createPrivateUseMapping(gi); |
| } |
| return cc; |
| } |
| |
| private int findCharacterFromGlyphIndex(int gi) { |
| return findCharacterFromGlyphIndex(gi, true); |
| } |
| |
| protected BitSet getGlyphIndices() { |
| BitSet bitset = new BitSet(); |
| bitset.set(0); |
| bitset.set(1); |
| bitset.set(2); |
| for (CMapSegment i : cmap) { |
| int start = i.getUnicodeStart(); |
| int end = i.getUnicodeEnd(); |
| int glyphIndex = i.getGlyphStartIndex(); |
| while (start++ < end + 1) { |
| bitset.set(glyphIndex++); |
| } |
| } |
| return bitset; |
| } |
| |
| protected char[] getChars() { |
| // the width array is set when the font is built |
| char[] chars = new char[width.length]; |
| for (CMapSegment i : cmap) { |
| int start = i.getUnicodeStart(); |
| int end = i.getUnicodeEnd(); |
| int glyphIndex = i.getGlyphStartIndex(); |
| while (start < end + 1) { |
| chars[glyphIndex++] = (char) start++; |
| } |
| } |
| return chars; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public char mapChar(char c) { |
| notifyMapOperation(); |
| int glyphIndex = findGlyphIndex(c); |
| if (glyphIndex == SingleByteEncoding.NOT_FOUND_CODE_POINT) { |
| warnMissingGlyph(c); |
| if (!isOTFFile) { |
| glyphIndex = findGlyphIndex(Typeface.NOT_FOUND); |
| } |
| } |
| if (isEmbeddable()) { |
| glyphIndex = cidSet.mapChar(glyphIndex, c); |
| } |
| if (isCID() && glyphIndex > 256) { |
| mapUnencodedChar(c); |
| } |
| return (char) glyphIndex; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public int mapCodePoint(int cp) { |
| notifyMapOperation(); |
| int glyphIndex = findGlyphIndex(cp); |
| if (glyphIndex == SingleByteEncoding.NOT_FOUND_CODE_POINT) { |
| |
| for (char ch : Character.toChars(cp)) { |
| //TODO better handling for non BMP |
| warnMissingGlyph(ch); |
| } |
| |
| if (!isOTFFile) { |
| glyphIndex = findGlyphIndex(Typeface.NOT_FOUND); |
| } |
| } |
| if (isEmbeddable()) { |
| glyphIndex = cidSet.mapCodePoint(glyphIndex, cp); |
| } |
| return (char) glyphIndex; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public boolean hasChar(char c) { |
| return hasCodePoint(c); |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public boolean hasCodePoint(int cp) { |
| return (findGlyphIndex(cp) != SingleByteEncoding.NOT_FOUND_CODE_POINT); |
| } |
| |
| /** |
| * Sets the defaultWidth. |
| * @param defaultWidth The defaultWidth to set |
| */ |
| public void setDefaultWidth(int defaultWidth) { |
| this.defaultWidth = defaultWidth; |
| } |
| |
| /** |
| * Returns the TrueType Collection Name. |
| * @return the TrueType Collection Name |
| */ |
| public String getTTCName() { |
| return ttcName; |
| } |
| |
| /** |
| * Sets the the TrueType Collection Name. |
| * @param ttcName the TrueType Collection Name |
| */ |
| public void setTTCName(String ttcName) { |
| this.ttcName = ttcName; |
| } |
| |
| /** |
| * Sets the width array. |
| * @param wds array of widths. |
| */ |
| public void setWidthArray(int[] wds) { |
| this.width = wds; |
| } |
| |
| /** |
| * Sets the bounding boxes array. |
| * @param boundingBoxes array of bounding boxes. |
| */ |
| public void setBBoxArray(Rectangle[] boundingBoxes) { |
| this.boundingBoxes = boundingBoxes; |
| } |
| |
| /** |
| * Returns a Map of used Glyphs. |
| * @return Map Map of used Glyphs |
| */ |
| public Map<Integer, Integer> getUsedGlyphs() { |
| return cidSet.getGlyphs(); |
| } |
| |
| /** |
| * Returns the character from it's original glyph index in the font |
| * @param glyphIndex The original index of the character |
| * @return The character |
| */ |
| public char getUnicodeFromGID(int glyphIndex) { |
| return cidSet.getUnicodeFromGID(glyphIndex); |
| } |
| |
| /** |
| * Gets the original glyph index in the font from a character. |
| * @param ch The character |
| * @return The glyph index in the font |
| */ |
| public int getGIDFromChar(char ch) { |
| return cidSet.getGIDFromChar(ch); |
| } |
| |
| /** |
| * Establishes the glyph definition table. |
| * @param gdef the glyph definition table to be used by this font |
| */ |
| public void setGDEF(GlyphDefinitionTable gdef) { |
| if ((this.gdef == null) || (gdef == null)) { |
| this.gdef = gdef; |
| } else { |
| throw new IllegalStateException("font already associated with GDEF table"); |
| } |
| } |
| |
| /** |
| * Obtain glyph definition table. |
| * @return glyph definition table or null if none is associated with font |
| */ |
| public GlyphDefinitionTable getGDEF() { |
| return gdef; |
| } |
| |
| /** |
| * Establishes the glyph substitution table. |
| * @param gsub the glyph substitution table to be used by this font |
| */ |
| public void setGSUB(GlyphSubstitutionTable gsub) { |
| if ((this.gsub == null) || (gsub == null)) { |
| this.gsub = gsub; |
| } else { |
| throw new IllegalStateException("font already associated with GSUB table"); |
| } |
| } |
| |
| /** |
| * Obtain glyph substitution table. |
| * @return glyph substitution table or null if none is associated with font |
| */ |
| public GlyphSubstitutionTable getGSUB() { |
| return gsub; |
| } |
| |
| /** |
| * Establishes the glyph positioning table. |
| * @param gpos the glyph positioning table to be used by this font |
| */ |
| public void setGPOS(GlyphPositioningTable gpos) { |
| if ((this.gpos == null) || (gpos == null)) { |
| this.gpos = gpos; |
| } else { |
| throw new IllegalStateException("font already associated with GPOS table"); |
| } |
| } |
| |
| /** |
| * Obtain glyph positioning table. |
| * @return glyph positioning table or null if none is associated with font |
| */ |
| public GlyphPositioningTable getGPOS() { |
| return gpos; |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean performsSubstitution() { |
| return gsub != null; |
| } |
| |
| /** {@inheritDoc} */ |
| public CharSequence performSubstitution(CharSequence charSequence, String script, String language, |
| List associations, boolean retainControls) { |
| if (gsub != null) { |
| charSequence = gsub.preProcess(charSequence, script, this, associations); |
| GlyphSequence glyphSequence = charSequenceToGlyphSequence(charSequence, associations); |
| GlyphSequence glyphSequenceSubstituted = gsub.substitute(glyphSequence, script, language); |
| if (associations != null) { |
| associations.clear(); |
| associations.addAll(glyphSequenceSubstituted.getAssociations()); |
| } |
| if (!retainControls) { |
| glyphSequenceSubstituted = elideControls(glyphSequenceSubstituted); |
| } |
| // may not contains all the characters that were in charSequence. |
| // see: #createPrivateUseMapping(int gi) |
| return mapGlyphsToChars(glyphSequenceSubstituted); |
| } else { |
| return charSequence; |
| } |
| } |
| |
| public GlyphSequence charSequenceToGlyphSequence(CharSequence charSequence, List associations) { |
| CharSequence normalizedCharSequence = normalize(charSequence, associations); |
| return mapCharsToGlyphs(normalizedCharSequence, associations); |
| } |
| |
| /** {@inheritDoc} */ |
| public CharSequence reorderCombiningMarks( |
| CharSequence cs, int[][] gpa, String script, String language, List associations) { |
| if (gdef != null) { |
| GlyphSequence igs = mapCharsToGlyphs(cs, associations); |
| GlyphSequence ogs = gdef.reorderCombiningMarks(igs, getUnscaledWidths(igs), gpa, script, language); |
| if (associations != null) { |
| associations.clear(); |
| associations.addAll(ogs.getAssociations()); |
| } |
| CharSequence ocs = mapGlyphsToChars(ogs); |
| return ocs; |
| } else { |
| return cs; |
| } |
| } |
| |
| protected int[] getUnscaledWidths(GlyphSequence gs) { |
| int[] widths = new int[gs.getGlyphCount()]; |
| for (int i = 0, n = widths.length; i < n; ++i) { |
| if (i < width.length) { |
| widths[i] = width[i]; |
| } |
| } |
| return widths; |
| } |
| |
| /** {@inheritDoc} */ |
| public boolean performsPositioning() { |
| return gpos != null; |
| } |
| |
| /** {@inheritDoc} */ |
| public int[][] |
| performPositioning(CharSequence cs, String script, String language, int fontSize) { |
| if (gpos != null) { |
| GlyphSequence gs = mapCharsToGlyphs(cs, null); |
| int[][] adjustments = new int [ gs.getGlyphCount() ] [ 4 ]; |
| if (gpos.position(gs, script, language, fontSize, this.width, adjustments)) { |
| return scaleAdjustments(adjustments, fontSize); |
| } else { |
| return null; |
| } |
| } else { |
| return null; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public int[][] performPositioning(CharSequence cs, String script, String language) { |
| throw new UnsupportedOperationException(); |
| } |
| |
| |
| private int[][] scaleAdjustments(int[][] adjustments, int fontSize) { |
| if (adjustments != null) { |
| for (int[] gpa : adjustments) { |
| for (int k = 0; k < 4; k++) { |
| gpa[k] = (gpa[k] * fontSize) / 1000; |
| } |
| } |
| return adjustments; |
| } else { |
| return null; |
| } |
| } |
| |
| /** |
| * Map sequence CS, comprising a sequence of UTF-16 encoded Unicode Code Points, to |
| * an output character sequence GS, comprising a sequence of Glyph Indices. N.B. Unlike |
| * mapChar(), this method does not make use of embedded subset encodings. |
| * @param cs a CharSequence containing UTF-16 encoded Unicode characters |
| * @returns a CharSequence containing glyph indices |
| */ |
| private GlyphSequence mapCharsToGlyphs(CharSequence cs, List associations) { |
| IntBuffer cb = IntBuffer.allocate(cs.length()); |
| IntBuffer gb = IntBuffer.allocate(cs.length()); |
| int gi; |
| int giMissing = findGlyphIndex(Typeface.NOT_FOUND); |
| for (int i = 0, n = cs.length(); i < n; i++) { |
| int cc = cs.charAt(i); |
| if ((cc >= 0xD800) && (cc < 0xDC00)) { |
| if ((i + 1) < n) { |
| int sh = cc; |
| int sl = cs.charAt(++i); |
| if ((sl >= 0xDC00) && (sl < 0xE000)) { |
| cc = 0x10000 + ((sh - 0xD800) << 10) + ((sl - 0xDC00) << 0); |
| } else { |
| throw new IllegalArgumentException( |
| "ill-formed UTF-16 sequence, " |
| + "contains isolated high surrogate at index " + i); |
| } |
| } else { |
| throw new IllegalArgumentException( |
| "ill-formed UTF-16 sequence, " |
| + "contains isolated high surrogate at end of sequence"); |
| } |
| } else if ((cc >= 0xDC00) && (cc < 0xE000)) { |
| throw new IllegalArgumentException( |
| "ill-formed UTF-16 sequence, " |
| + "contains isolated low surrogate at index " + i); |
| } |
| notifyMapOperation(); |
| gi = findGlyphIndex(cc); |
| if (gi == SingleByteEncoding.NOT_FOUND_CODE_POINT) { |
| warnMissingGlyph((char) cc); |
| gi = giMissing; |
| } |
| cb.put(cc); |
| gb.put(gi); |
| } |
| cb.flip(); |
| gb.flip(); |
| if ((associations != null) && (associations.size() == cs.length())) { |
| associations = new java.util.ArrayList(associations); |
| } else { |
| associations = null; |
| } |
| return new GlyphSequence(cb, gb, associations); |
| } |
| |
| /** |
| * Map sequence GS, comprising a sequence of Glyph Indices, to output sequence CS, |
| * comprising a sequence of UTF-16 encoded Unicode Code Points. |
| * @param gs a GlyphSequence containing glyph indices |
| * @returns a CharSequence containing UTF-16 encoded Unicode characters |
| */ |
| private CharSequence mapGlyphsToChars(GlyphSequence gs) { |
| int ng = gs.getGlyphCount(); |
| int ccMissing = Typeface.NOT_FOUND; |
| List<Character> chars = new ArrayList<Character>(gs.getUTF16CharacterCount()); |
| |
| for (int i = 0, n = ng; i < n; i++) { |
| int gi = gs.getGlyph(i); |
| int cc = findCharacterFromGlyphIndex(gi); |
| if ((cc == 0) || (cc > 0x10FFFF)) { |
| cc = ccMissing; |
| log.warn("Unable to map glyph index " + gi |
| + " to Unicode scalar in font '" |
| + getFullName() + "', substituting missing character '" |
| + (char) cc + "'"); |
| } |
| if (cc > 0x00FFFF) { |
| int sh; |
| int sl; |
| cc -= 0x10000; |
| sh = ((cc >> 10) & 0x3FF) + 0xD800; |
| sl = ((cc >> 0) & 0x3FF) + 0xDC00; |
| chars.add((char) sh); |
| chars.add((char) sl); |
| } else { |
| chars.add((char) cc); |
| } |
| } |
| |
| CharBuffer cb = CharBuffer.allocate(chars.size()); |
| |
| for (char c : chars) { |
| cb.put(c); |
| } |
| |
| cb.flip(); |
| return cb; |
| } |
| |
| private CharSequence normalize(CharSequence cs, List associations) { |
| return hasDecomposable(cs) ? decompose(cs, associations) : cs; |
| } |
| |
| private boolean hasDecomposable(CharSequence cs) { |
| for (int i = 0, n = cs.length(); i < n; i++) { |
| int cc = cs.charAt(i); |
| if (CharNormalize.isDecomposable(cc)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private CharSequence decompose(CharSequence cs, List associations) { |
| StringBuffer sb = new StringBuffer(cs.length()); |
| int[] daBuffer = new int[CharNormalize.maximumDecompositionLength()]; |
| for (int i = 0, n = cs.length(); i < n; i++) { |
| int cc = cs.charAt(i); |
| int[] da = CharNormalize.decompose(cc, daBuffer); |
| for (int aDa : da) { |
| if (aDa > 0) { |
| sb.append((char) aDa); |
| } else { |
| break; |
| } |
| } |
| } |
| return sb; |
| } |
| |
| /** |
| * Removes the glyphs associated with elidable control characters. |
| * All the characters in an association must be elidable in order |
| * to remove the corresponding glyph. |
| * |
| * @param gs GlyphSequence that may contains the elidable glyphs |
| * @return GlyphSequence without the elidable glyphs |
| */ |
| private static GlyphSequence elideControls(GlyphSequence gs) { |
| if (hasElidableControl(gs)) { |
| int[] ca = gs.getCharacterArray(false); |
| IntBuffer ngb = IntBuffer.allocate(gs.getGlyphCount()); |
| List nal = new java.util.ArrayList(gs.getGlyphCount()); |
| for (int i = 0, n = gs.getGlyphCount(); i < n; ++i) { |
| CharAssociation a = gs.getAssociation(i); |
| int s = a.getStart(); |
| int e = a.getEnd(); |
| while (s < e) { |
| int ch = ca [ s ]; |
| if (!isElidableControl(ch)) { |
| break; |
| } else { |
| ++s; |
| } |
| } |
| // If there is at least one non-elidable character in the char |
| // sequence then the glyph/association is kept. |
| if (s != e) { |
| ngb.put(gs.getGlyph(i)); |
| nal.add(a); |
| } |
| } |
| ngb.flip(); |
| return new GlyphSequence(gs.getCharacters(), ngb, nal, gs.getPredications()); |
| } else { |
| return gs; |
| } |
| } |
| |
| private static boolean hasElidableControl(GlyphSequence gs) { |
| int[] ca = gs.getCharacterArray(false); |
| for (int ch : ca) { |
| if (isElidableControl(ch)) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| private static boolean isElidableControl(int ch) { |
| if (ch < 0x0020) { |
| return true; |
| } else if ((ch >= 0x80) && (ch < 0x00A0)) { |
| return true; |
| } else if ((ch >= 0x2000) && (ch <= 0x206F)) { |
| if ((ch >= 0x200B) && (ch <= 0x200F)) { |
| return true; |
| } else if ((ch >= 0x2028) && (ch <= 0x202E)) { |
| return true; |
| } else if (ch >= 0x2066) { |
| return true; |
| } else { |
| return ch == 0x2060; |
| } |
| } else { |
| return false; |
| } |
| } |
| |
| @Override |
| public boolean hasFeature(int tableType, String script, String language, String feature) { |
| GlyphTable table; |
| if (tableType == GlyphTable.GLYPH_TABLE_TYPE_SUBSTITUTION) { |
| table = getGSUB(); |
| } else if (tableType == GlyphTable.GLYPH_TABLE_TYPE_POSITIONING) { |
| table = getGPOS(); |
| } else if (tableType == GlyphTable.GLYPH_TABLE_TYPE_DEFINITION) { |
| table = getGDEF(); |
| } else { |
| table = null; |
| } |
| return (table != null) && table.hasFeature(script, language, feature); |
| } |
| |
| public Map<Integer, Integer> getWidthsMap() { |
| return null; |
| } |
| |
| public InputStream getCmapStream() { |
| return null; |
| } |
| } |
| |