| /* |
| * 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.nio.CharBuffer; |
| import java.nio.IntBuffer; |
| import java.util.BitSet; |
| 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.Positionable; |
| import org.apache.fop.complexscripts.fonts.Substitutable; |
| 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 // CSOK: ConstantNameCheck |
| = LogFactory.getLog(MultiByteFont.class); |
| |
| private String ttcName; |
| private String encoding = "Identity-H"; |
| |
| private int defaultWidth; |
| private CIDFontType cidType = CIDFontType.CIDTYPE2; |
| |
| private 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; |
| |
| /** |
| * 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; |
| } |
| |
| /** |
| * 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; |
| } |
| |
| /** {@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; |
| } |
| |
| /** |
| * 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 |
| private int findGlyphIndex(int c) { |
| int idx = c; |
| int retIdx = SingleByteEncoding.NOT_FOUND_CODE_POINT; |
| |
| for (int i = 0; (i < cmap.length) && retIdx == 0; i++) { |
| if (cmap[i].getUnicodeStart() <= idx |
| && cmap[i].getUnicodeEnd() >= idx) { |
| |
| retIdx = cmap[i].getGlyphStartIndex() |
| + idx |
| - cmap[i].getUnicodeStart(); |
| } |
| } |
| 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. |
| */ |
| private synchronized void addPrivateUseMapping ( int pu, int gi ) { |
| assert findGlyphIndex ( pu ) == SingleByteEncoding.NOT_FOUND_CODE_POINT; |
| CMapSegment[] oldCmap = cmap; |
| int cmapLength = oldCmap.length; |
| CMapSegment[] newCmap = new CMapSegment [ cmapLength + 1 ]; |
| System.arraycopy ( oldCmap, 0, newCmap, 0, cmapLength ); |
| newCmap [ cmapLength ] = new CMapSegment ( pu, pu, gi ); |
| cmap = newCmap; |
| } |
| |
| /** |
| * 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 |
| * @returns 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 ( int i = 0, n = cmap.length; i < n; i++ ) { |
| CMapSegment segment = cmap [ i ]; |
| 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 ); |
| } |
| |
| |
| /** {@inheritDoc} */ |
| @Override |
| public char mapChar(char c) { |
| notifyMapOperation(); |
| int glyphIndex = findGlyphIndex(c); |
| if (glyphIndex == SingleByteEncoding.NOT_FOUND_CODE_POINT) { |
| warnMissingGlyph(c); |
| glyphIndex = findGlyphIndex(Typeface.NOT_FOUND); |
| } |
| if (isEmbeddable()) { |
| glyphIndex = cidSet.mapChar(glyphIndex, c); |
| } |
| return (char) glyphIndex; |
| } |
| |
| protected BitSet getGlyphIndices() { |
| BitSet bitset = new BitSet(); |
| bitset.set(0); |
| bitset.set(1); |
| bitset.set(2); |
| for (int i = 0; i < cmap.length; i++) { |
| int start = cmap[i].getUnicodeStart(); |
| int end = cmap[i].getUnicodeEnd(); |
| int glyphIndex = cmap[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 (int i = 0; i < cmap.length; i++) { |
| int start = cmap[i].getUnicodeStart(); |
| int end = cmap[i].getUnicodeEnd(); |
| int glyphIndex = cmap[i].getGlyphStartIndex(); |
| while (start < end + 1) { |
| chars[glyphIndex++] = (char) start++; |
| } |
| } |
| return chars; |
| } |
| |
| /** {@inheritDoc} */ |
| @Override |
| public boolean hasChar(char c) { |
| return (findGlyphIndex(c) != 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; |
| } |
| |
| /** |
| * Returns a Map of used Glyphs. |
| * @return Map Map of used Glyphs |
| */ |
| public Map<Integer, Integer> getUsedGlyphs() { |
| return cidSet.getGlyphs(); |
| } |
| |
| /** |
| * 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 cs, String script, String language ) { |
| if ( gsub != null ) { |
| GlyphSequence igs = mapCharsToGlyphs ( cs ); |
| GlyphSequence ogs = gsub.substitute ( igs, script, language ); |
| CharSequence ocs = mapGlyphsToChars ( ogs ); |
| return ocs; |
| } else { |
| return cs; |
| } |
| } |
| |
| /** {@inheritDoc} */ |
| public CharSequence reorderCombiningMarks |
| ( CharSequence cs, int[][] gpa, String script, String language ) { |
| if ( gdef != null ) { |
| GlyphSequence igs = mapCharsToGlyphs ( cs ); |
| GlyphSequence ogs = gdef.reorderCombiningMarks ( igs, gpa, script, language ); |
| CharSequence ocs = mapGlyphsToChars ( ogs ); |
| return ocs; |
| } else { |
| return cs; |
| } |
| } |
| |
| /** {@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 ); |
| 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 i = 0, n = adjustments.length; i < n; i++ ) { |
| int[] gpa = adjustments [ i ]; |
| 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 ) { |
| 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(); |
| return new GlyphSequence ( cb, gb, null ); |
| } |
| |
| /** |
| * 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(); |
| CharBuffer cb = CharBuffer.allocate ( ng ); |
| int ccMissing = Typeface.NOT_FOUND; |
| 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; |
| cb.put ( (char) sh ); |
| cb.put ( (char) sl ); |
| } else { |
| cb.put ( (char) cc ); |
| } |
| } |
| cb.flip(); |
| return cb; |
| } |
| |
| } |
| |