Open Source Repository

Home /itextpdf/itextpdf-5.1.2 | Repository Home


com/itextpdf/text/pdf/CJKFont.java
/*
 * $Id: CJKFont.java 4784 2011-03-15 08:33:00Z blowagie $
 *
 * This file is part of the iText (R) project.
 * Copyright (c) 1998-2011 1T3XT BVBA
 * Authors: Bruno Lowagie, Paulo Soares, et al.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License version 3
 * as published by the Free Software Foundation with the addition of the
 * following permission added to Section 15 as permitted in Section 7(a):
 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY 1T3XT,
 * 1T3XT DISCLAIMS THE WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.
 * See the GNU Affero General Public License for more details.
 * You should have received a copy of the GNU Affero General Public License
 * along with this program; if not, see http://www.gnu.org/licenses or write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA, 02110-1301 USA, or download the license from the following URL:
 * http://itextpdf.com/terms-of-use/
 *
 * The interactive user interfaces in modified source and object code versions
 * of this program must display Appropriate Legal Notices, as required under
 * Section 5 of the GNU Affero General Public License.
 *
 * In accordance with Section 7(b) of the GNU Affero General Public License,
 * a covered work must retain the producer line in every PDF that is created
 * or manipulated using iText.
 *
 * You can be released from the requirements of the license by purchasing
 * a commercial license. Buying such a license is mandatory as soon as you
 * develop commercial activities involving the iText software without
 * disclosing the source code of your own applications.
 * These activities include: offering paid services to customers as an ASP,
 * serving PDFs on the fly in a web application, shipping iText with a closed
 * source product.
 *
 * For more information, please contact iText Software Corp. at this
 * address: [email protected]
 */
package com.itextpdf.text.pdf;

import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Properties;
import java.util.StringTokenizer;

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.error_messages.MessageLocalization;

/**
 * Creates a CJK font compatible with the fonts in the Adobe Asian font Pack.
 *
 @author  Paulo Soares
 */

class CJKFont extends BaseFont {
    /** The encoding used in the PDF document for CJK fonts
     */
    static final String CJK_ENCODING = "UnicodeBigUnmarked";
    private static final int FIRST = 0;
    private static final int BRACKET = 1;
    private static final int SERIAL = 2;
    private static final int V1Y = 880;

    static Properties cjkFonts = new Properties();
    static Properties cjkEncodings = new Properties();
    static Hashtable<String, char[]> allCMaps = new Hashtable<String, char[]>();
    static Hashtable<String, HashMap<String, Object>> allFonts = new Hashtable<String, HashMap<String, Object>>();
    private static boolean propertiesLoaded = false;

    /** The font name */
    private String fontName;
    /** The style modifier */
    private String style = "";
    /** The CMap name associated with this font */
    private String CMap;

    private boolean cidDirect = false;

    private char[] translationMap;
    private IntHashtable vMetrics;
    private IntHashtable hMetrics;
    private HashMap<String, Object> fontDesc;
    private boolean vertical = false;

    private static void loadProperties() {
        if (propertiesLoaded)
            return;
        synchronized (allFonts) {
            if (propertiesLoaded)
                return;
            try {
                InputStream is = getResourceStream(RESOURCE_PATH + "cjkfonts.properties");
                cjkFonts.load(is);
                is.close();
                is = getResourceStream(RESOURCE_PATH + "cjkencodings.properties");
                cjkEncodings.load(is);
                is.close();
            }
            catch (Exception e) {
                cjkFonts = new Properties();
                cjkEncodings = new Properties();
            }
            propertiesLoaded = true;
        }
    }

    /** Creates a CJK font.
     @param fontName the name of the font
     @param enc the encoding of the font
     @param emb always <CODE>false</CODE>. CJK font and not embedded
     @throws DocumentException on error
     */
    CJKFont(String fontName, String enc, boolean embthrows DocumentException {
        loadProperties();
        fontType = FONT_TYPE_CJK;
        String nameBase = getBaseName(fontName);
        if (!isCJKFont(nameBase, enc))
            throw new DocumentException(MessageLocalization.getComposedMessage("font.1.with.2.encoding.is.not.a.cjk.font", fontName, enc));
        if (nameBase.length() < fontName.length()) {
            style = fontName.substring(nameBase.length());
            fontName = nameBase;
        }
        this.fontName = fontName;
        encoding = CJK_ENCODING;
        vertical = enc.endsWith("V");
        CMap = enc;
        if (enc.startsWith("Identity-")) {
            cidDirect = true;
            String s = cjkFonts.getProperty(fontName);
            s = s.substring(0, s.indexOf('_'));
            char c[] = allCMaps.get(s);
            if (c == null) {
                c = readCMap(s);
                if (c == null)
                    throw new DocumentException(MessageLocalization.getComposedMessage("the.cmap.1.does.not.exist.as.a.resource", s));
                c[CID_NEWLINE'\n';
                allCMaps.put(s, c);
            }
            translationMap = c;
        }
        else {
            char c[] = allCMaps.get(enc);
            if (c == null) {
                String s = cjkEncodings.getProperty(enc);
                if (s == null)
                    throw new DocumentException(MessageLocalization.getComposedMessage("the.resource.cjkencodings.properties.does.not.contain.the.encoding.1", enc));
                StringTokenizer tk = new StringTokenizer(s);
                String nt = tk.nextToken();
                c = allCMaps.get(nt);
                if (c == null) {
                    c = readCMap(nt);
                    allCMaps.put(nt, c);
                }
                if (tk.hasMoreTokens()) {
                    String nt2 = tk.nextToken();
                    char m2[] = readCMap(nt2);
                    for (int k = 0; k < 0x10000; ++k) {
                        if (m2[k== 0)
                            m2[k= c[k];
                    }
                    allCMaps.put(enc, m2);
                    c = m2;
                }
            }
            translationMap = c;
        }
        fontDesc = allFonts.get(fontName);
        if (fontDesc == null) {
            fontDesc = readFontProperties(fontName);
            allFonts.put(fontName, fontDesc);
        }
        hMetrics = (IntHashtable)fontDesc.get("W");
        vMetrics = (IntHashtable)fontDesc.get("W2");
    }

    /** Checks if its a valid CJK font.
     @param fontName the font name
     @param enc the encoding
     @return <CODE>true</CODE> if it is CJK font
     */
    public static boolean isCJKFont(String fontName, String enc) {
        loadProperties();
        String encodings = cjkFonts.getProperty(fontName);
        return encodings != null && (enc.equals("Identity-H"|| enc.equals("Identity-V"|| encodings.indexOf("_" + enc + "_">= 0);
    }

    /**
     * Gets the width of a <CODE>char</CODE> in normalized 1000 units.
     @param char1 the unicode <CODE>char</CODE> to get the width of
     @return the width in normalized 1000 units
     */
    @Override
    public int getWidth(int char1) {
        int c = char1;
        if (!cidDirect)
            c = translationMap[c];
        int v;
        if (vertical)
            v = vMetrics.get(c);
        else
            v = hMetrics.get(c);
        if (v > 0)
            return v;
        else
            return 1000;
    }

    @Override
    public int getWidth(String text) {
        int total = 0;
        for (int k = 0; k < text.length(); ++k) {
            int c = text.charAt(k);
            if (!cidDirect)
                c = translationMap[c];
            int v;
            if (vertical)
                v = vMetrics.get(c);
            else
                v = hMetrics.get(c);
            if (v > 0)
                total += v;
            else
                total += 1000;
        }
        return total;
    }

    @Override
    int getRawWidth(int c, String name) {
        return 0;
    }

    @Override
    public int getKerning(int char1, int char2) {
        return 0;
    }

    private PdfDictionary getFontDescriptor() {
        PdfDictionary dic = new PdfDictionary(PdfName.FONTDESCRIPTOR);
        dic.put(PdfName.ASCENT, new PdfLiteral((String)fontDesc.get("Ascent")));
        dic.put(PdfName.CAPHEIGHT, new PdfLiteral((String)fontDesc.get("CapHeight")));
        dic.put(PdfName.DESCENT, new PdfLiteral((String)fontDesc.get("Descent")));
        dic.put(PdfName.FLAGS, new PdfLiteral((String)fontDesc.get("Flags")));
        dic.put(PdfName.FONTBBOX, new PdfLiteral((String)fontDesc.get("FontBBox")));
        dic.put(PdfName.FONTNAME, new PdfName(fontName + style));
        dic.put(PdfName.ITALICANGLE, new PdfLiteral((String)fontDesc.get("ItalicAngle")));
        dic.put(PdfName.STEMV, new PdfLiteral((String)fontDesc.get("StemV")));
        PdfDictionary pdic = new PdfDictionary();
        pdic.put(PdfName.PANOSE, new PdfString((String)fontDesc.get("Panose")null));
        dic.put(PdfName.STYLE, pdic);
        return dic;
    }

    private PdfDictionary getCIDFont(PdfIndirectReference fontDescriptor, IntHashtable cjkTag) {
        PdfDictionary dic = new PdfDictionary(PdfName.FONT);
        dic.put(PdfName.SUBTYPE, PdfName.CIDFONTTYPE0);
        dic.put(PdfName.BASEFONT, new PdfName(fontName + style));
        dic.put(PdfName.FONTDESCRIPTOR, fontDescriptor);
        int keys[] = cjkTag.toOrderedKeys();
        String w = convertToHCIDMetrics(keys, hMetrics);
        if (w != null)
            dic.put(PdfName.W, new PdfLiteral(w));
        if (vertical) {
            w = convertToVCIDMetrics(keys, vMetrics, hMetrics);
            if (w != null)
                dic.put(PdfName.W2, new PdfLiteral(w));
        }
        else
            dic.put(PdfName.DW, new PdfNumber(1000));
        PdfDictionary cdic = new PdfDictionary();
        cdic.put(PdfName.REGISTRY, new PdfString((String)fontDesc.get("Registry")null));
        cdic.put(PdfName.ORDERING, new PdfString((String)fontDesc.get("Ordering")null));
        cdic.put(PdfName.SUPPLEMENT, new PdfLiteral((String)fontDesc.get("Supplement")));
        dic.put(PdfName.CIDSYSTEMINFO, cdic);
        return dic;
    }

    private PdfDictionary getFontBaseType(PdfIndirectReference CIDFont) {
        PdfDictionary dic = new PdfDictionary(PdfName.FONT);
        dic.put(PdfName.SUBTYPE, PdfName.TYPE0);
        String name = fontName;
        if (style.length() 0)
            name += "-" + style.substring(1);
        name += "-" + CMap;
        dic.put(PdfName.BASEFONT, new PdfName(name));
        dic.put(PdfName.ENCODING, new PdfName(CMap));
        dic.put(PdfName.DESCENDANTFONTS, new PdfArray(CIDFont));
        return dic;
    }

    @Override
    void writeFont(PdfWriter writer, PdfIndirectReference ref, Object params[]) throws DocumentException, IOException {
        IntHashtable cjkTag = (IntHashtable)params[0];
        PdfIndirectReference ind_font = null;
        PdfObject pobj = null;
        PdfIndirectObject obj = null;
        pobj = getFontDescriptor();
        if (pobj != null){
            obj = writer.addToBody(pobj);
            ind_font = obj.getIndirectReference();
        }
        pobj = getCIDFont(ind_font, cjkTag);
        if (pobj != null){
            obj = writer.addToBody(pobj);
            ind_font = obj.getIndirectReference();
        }
        pobj = getFontBaseType(ind_font);
        writer.addToBody(pobj, ref);
    }

    /**
     * You can't get the FontStream of a CJK font (CJK fonts are never embedded),
     * so this method always returns null.
      @return  null
     @since  2.1.3
     */
    @Override
    public PdfStream getFullFontStream() {
      return null;
    }

    private float getDescNumber(String name) {
        return Integer.parseInt((String)fontDesc.get(name));
    }

    private float getBBox(int idx) {
        String s = (String)fontDesc.get("FontBBox");
        StringTokenizer tk = new StringTokenizer(s, " []\r\n\t\f");
        String ret = tk.nextToken();
        for (int k = 0; k < idx; ++k)
            ret = tk.nextToken();
        return Integer.parseInt(ret);
    }

    /** Gets the font parameter identified by <CODE>key</CODE>. Valid values
     * for <CODE>key</CODE> are <CODE>ASCENT</CODE><CODE>CAPHEIGHT</CODE><CODE>DESCENT</CODE>
     * and <CODE>ITALICANGLE</CODE>.
     @param key the parameter to be extracted
     @param fontSize the font size in points
     @return the parameter in points
     */
    @Override
    public float getFontDescriptor(int key, float fontSize) {
        switch (key) {
            case AWT_ASCENT:
            case ASCENT:
                return getDescNumber("Ascent"* fontSize / 1000;
            case CAPHEIGHT:
                return getDescNumber("CapHeight"* fontSize / 1000;
            case AWT_DESCENT:
            case DESCENT:
                return getDescNumber("Descent"* fontSize / 1000;
            case ITALICANGLE:
                return getDescNumber("ItalicAngle");
            case BBOXLLX:
                return fontSize * getBBox(01000;
            case BBOXLLY:
                return fontSize * getBBox(11000;
            case BBOXURX:
                return fontSize * getBBox(21000;
            case BBOXURY:
                return fontSize * getBBox(31000;
            case AWT_LEADING:
                return 0;
            case AWT_MAXADVANCE:
                return fontSize * (getBBox(2- getBBox(0)) 1000;
        }
        return 0;
    }

    @Override
    public String getPostscriptFontName() {
        return fontName;
    }

    /** Gets the full name of the font. If it is a True Type font
     * each array element will have {Platform ID, Platform Encoding ID,
     * Language ID, font name}. The interpretation of this values can be
     * found in the Open Type specification, chapter 2, in the 'name' table.<br>
     * For the other fonts the array has a single element with {"", "", "",
     * font name}.
     @return the full name of the font
     */
    @Override
    public String[][] getFullFontName() {
        return new String[][]{{"""""", fontName}};
    }

    /** Gets all the entries of the names-table. If it is a True Type font
     * each array element will have {Name ID, Platform ID, Platform Encoding ID,
     * Language ID, font name}. The interpretation of this values can be
     * found in the Open Type specification, chapter 2, in the 'name' table.<br>
     * For the other fonts the array has a single element with {"4", "", "", "",
     * font name}.
     @return the full name of the font
     */
    @Override
    public String[][] getAllNameEntries() {
        return new String[][]{{"4""""""", fontName}};
    }

    /** Gets the family name of the font. If it is a True Type font
     * each array element will have {Platform ID, Platform Encoding ID,
     * Language ID, font name}. The interpretation of this values can be
     * found in the Open Type specification, chapter 2, in the 'name' table.<br>
     * For the other fonts the array has a single element with {"", "", "",
     * font name}.
     @return the family name of the font
     */
    @Override
    public String[][] getFamilyFontName() {
        return getFullFontName();
    }

    static char[] readCMap(String name) {
        try {
            name = name + ".cmap";
            InputStream is = getResourceStream(RESOURCE_PATH + name);
            char c[] new char[0x10000];
            for (int k = 0; k < 0x10000; ++k)
                c[k(char)((is.read() << 8+ is.read());
            is.close();
            return c;
        }
        catch (Exception e) {
            // empty on purpose
        }
        return null;
    }

    static IntHashtable createMetric(String s) {
        IntHashtable h = new IntHashtable();
        StringTokenizer tk = new StringTokenizer(s);
        while (tk.hasMoreTokens()) {
            int n1 = Integer.parseInt(tk.nextToken());
            h.put(n1, Integer.parseInt(tk.nextToken()));
        }
        return h;
    }

    static String convertToHCIDMetrics(int keys[], IntHashtable h) {
        if (keys.length == 0)
            return null;
        int lastCid = 0;
        int lastValue = 0;
        int start;
        for (start = 0; start < keys.length; ++start) {
            lastCid = keys[start];
            lastValue = h.get(lastCid);
            if (lastValue != 0) {
                ++start;
                break;
            }
        }
        if (lastValue == 0)
            return null;
        StringBuffer buf = new StringBuffer();
        buf.append('[');
        buf.append(lastCid);
        int state = FIRST;
        for (int k = start; k < keys.length; ++k) {
            int cid = keys[k];
            int value = h.get(cid);
            if (value == 0)
                continue;
            switch (state) {
                case FIRST: {
                    if (cid == lastCid + && value == lastValue) {
                        state = SERIAL;
                    }
                    else if (cid == lastCid + 1) {
                        state = BRACKET;
                        buf.append('[').append(lastValue);
                    }
                    else {
                        buf.append('[').append(lastValue).append(']').append(cid);
                    }
                    break;
                }
                case BRACKET: {
                    if (cid == lastCid + && value == lastValue) {
                        state = SERIAL;
                        buf.append(']').append(lastCid);
                    }
                    else if (cid == lastCid + 1) {
                        buf.append(' ').append(lastValue);
                    }
                    else {
                        state = FIRST;
                        buf.append(' ').append(lastValue).append(']').append(cid);
                    }
                    break;
                }
                case SERIAL: {
                    if (cid != lastCid + || value != lastValue) {
                        buf.append(' ').append(lastCid).append(' ').append(lastValue).append(' ').append(cid);
                        state = FIRST;
                    }
                    break;
                }
            }
            lastValue = value;
            lastCid = cid;
        }
        switch (state) {
            case FIRST: {
                buf.append('[').append(lastValue).append("]]");
                break;
            }
            case BRACKET: {
                buf.append(' ').append(lastValue).append("]]");
                break;
            }
            case SERIAL: {
                buf.append(' ').append(lastCid).append(' ').append(lastValue).append(']');
                break;
            }
        }
        return buf.toString();
    }

    static String convertToVCIDMetrics(int keys[], IntHashtable v, IntHashtable h) {
        if (keys.length == 0)
            return null;
        int lastCid = 0;
        int lastValue = 0;
        int lastHValue = 0;
        int start;
        for (start = 0; start < keys.length; ++start) {
            lastCid = keys[start];
            lastValue = v.get(lastCid);
            if (lastValue != 0) {
                ++start;
                break;
            }
            else
                lastHValue = h.get(lastCid);
        }
        if (lastValue == 0)
            return null;
        if (lastHValue == 0)
            lastHValue = 1000;
        StringBuffer buf = new StringBuffer();
        buf.append('[');
        buf.append(lastCid);
        int state = FIRST;
        for (int k = start; k < keys.length; ++k) {
            int cid = keys[k];
            int value = v.get(cid);
            if (value == 0)
                continue;
            int hValue = h.get(lastCid);
            if (hValue == 0)
                hValue = 1000;
            switch (state) {
                case FIRST: {
                    if (cid == lastCid + && value == lastValue && hValue == lastHValue) {
                        state = SERIAL;
                    }
                    else {
                        buf.append(' ').append(lastCid).append(' ').append(-lastValue).append(' ').append(lastHValue / 2).append(' ').append(V1Y).append(' ').append(cid);
                    }
                    break;
                }
                case SERIAL: {
                    if (cid != lastCid + || value != lastValue || hValue != lastHValue) {
                        buf.append(' ').append(lastCid).append(' ').append(-lastValue).append(' ').append(lastHValue / 2).append(' ').append(V1Y).append(' ').append(cid);
                        state = FIRST;
                    }
                    break;
                }
            }
            lastValue = value;
            lastCid = cid;
            lastHValue = hValue;
        }
        buf.append(' ').append(lastCid).append(' ').append(-lastValue).append(' ').append(lastHValue / 2).append(' ').append(V1Y).append(" ]");
        return buf.toString();
    }

    static HashMap<String, Object> readFontProperties(String name) {
        try {
            name += ".properties";
            InputStream is = getResourceStream(RESOURCE_PATH + name);
            Properties p = new Properties();
            p.load(is);
            is.close();
            IntHashtable W = createMetric(p.getProperty("W"));
            p.remove("W");
            IntHashtable W2 = createMetric(p.getProperty("W2"));
            p.remove("W2");
            HashMap<String, Object> map = new HashMap<String, Object>();
            for (Enumeration<Object> e = p.keys(); e.hasMoreElements();) {
                Object obj = e.nextElement();
                map.put((String)obj, p.getProperty((String)obj));
            }
            map.put("W", W);
            map.put("W2", W2);
            return map;
        }
        catch (Exception e) {
            // empty on purpose
        }
        return null;
    }

    @Override
    public int getUnicodeEquivalent(int c) {
        if (cidDirect)
            return translationMap[c];
        return c;
    }

    @Override
    public int getCidCode(int c) {
        if (cidDirect)
            return c;
        return translationMap[c];
    }

    /** Checks if the font has any kerning pairs.
     @return always <CODE>false</CODE>
     */
    @Override
    public boolean hasKernPairs() {
        return false;
    }

    /**
     * Checks if a character exists in this font.
     @param c the character to check
     @return <CODE>true</CODE> if the character has a glyph,
     <CODE>false</CODE> otherwise
     */
    @Override
    public boolean charExists(int c) {
        return translationMap[c!= 0;
    }

    /**
     * Sets the character advance.
     @param c the character
     @param advance the character advance normalized to 1000 units
     @return <CODE>true</CODE> if the advance was set,
     <CODE>false</CODE> otherwise. Will always return <CODE>false</CODE>
     */
    @Override
    public boolean setCharAdvance(int c, int advance) {
        return false;
    }

    /**
     * Sets the font name that will appear in the pdf font dictionary.
     * Use with care as it can easily make a font unreadable if not embedded.
     @param name the new font name
     */
    @Override
    public void setPostscriptFontName(String name) {
        fontName = name;
    }

    @Override
    public boolean setKerning(int char1, int char2, int kern) {
        return false;
    }

    @Override
    public int[] getCharBBox(int c) {
        return null;
    }

    @Override
    protected int[] getRawCharBBox(int c, String name) {
        return null;
    }
}