001/*
002 * $Id: CJKFont.java 4784 2011-03-15 08:33:00Z blowagie $
003 *
004 * This file is part of the iText (R) project.
005 * Copyright (c) 1998-2011 1T3XT BVBA
006 * Authors: Bruno Lowagie, Paulo Soares, et al.
007 *
008 * This program is free software; you can redistribute it and/or modify
009 * it under the terms of the GNU Affero General Public License version 3
010 * as published by the Free Software Foundation with the addition of the
011 * following permission added to Section 15 as permitted in Section 7(a):
012 * FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY 1T3XT,
013 * 1T3XT DISCLAIMS THE WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
014 *
015 * This program is distributed in the hope that it will be useful, but
016 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
017 * or FITNESS FOR A PARTICULAR PURPOSE.
018 * See the GNU Affero General Public License for more details.
019 * You should have received a copy of the GNU Affero General Public License
020 * along with this program; if not, see http://www.gnu.org/licenses or write to
021 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
022 * Boston, MA, 02110-1301 USA, or download the license from the following URL:
023 * http://itextpdf.com/terms-of-use/
024 *
025 * The interactive user interfaces in modified source and object code versions
026 * of this program must display Appropriate Legal Notices, as required under
027 * Section 5 of the GNU Affero General Public License.
028 *
029 * In accordance with Section 7(b) of the GNU Affero General Public License,
030 * a covered work must retain the producer line in every PDF that is created
031 * or manipulated using iText.
032 *
033 * You can be released from the requirements of the license by purchasing
034 * a commercial license. Buying such a license is mandatory as soon as you
035 * develop commercial activities involving the iText software without
036 * disclosing the source code of your own applications.
037 * These activities include: offering paid services to customers as an ASP,
038 * serving PDFs on the fly in a web application, shipping iText with a closed
039 * source product.
040 *
041 * For more information, please contact iText Software Corp. at this
042 * address: sales@itextpdf.com
043 */
044package com.itextpdf.text.pdf;
045
046import java.io.IOException;
047import java.io.InputStream;
048import java.util.Enumeration;
049import java.util.HashMap;
050import java.util.Hashtable;
051import java.util.Properties;
052import java.util.StringTokenizer;
053
054import com.itextpdf.text.DocumentException;
055import com.itextpdf.text.error_messages.MessageLocalization;
056
057/**
058 * Creates a CJK font compatible with the fonts in the Adobe Asian font Pack.
059 *
060 * @author  Paulo Soares
061 */
062
063class CJKFont extends BaseFont {
064    /** The encoding used in the PDF document for CJK fonts
065     */
066    static final String CJK_ENCODING = "UnicodeBigUnmarked";
067    private static final int FIRST = 0;
068    private static final int BRACKET = 1;
069    private static final int SERIAL = 2;
070    private static final int V1Y = 880;
071
072    static Properties cjkFonts = new Properties();
073    static Properties cjkEncodings = new Properties();
074    static Hashtable<String, char[]> allCMaps = new Hashtable<String, char[]>();
075    static Hashtable<String, HashMap<String, Object>> allFonts = new Hashtable<String, HashMap<String, Object>>();
076    private static boolean propertiesLoaded = false;
077
078    /** The font name */
079    private String fontName;
080    /** The style modifier */
081    private String style = "";
082    /** The CMap name associated with this font */
083    private String CMap;
084
085    private boolean cidDirect = false;
086
087    private char[] translationMap;
088    private IntHashtable vMetrics;
089    private IntHashtable hMetrics;
090    private HashMap<String, Object> fontDesc;
091    private boolean vertical = false;
092
093    private static void loadProperties() {
094        if (propertiesLoaded)
095            return;
096        synchronized (allFonts) {
097            if (propertiesLoaded)
098                return;
099            try {
100                InputStream is = getResourceStream(RESOURCE_PATH + "cjkfonts.properties");
101                cjkFonts.load(is);
102                is.close();
103                is = getResourceStream(RESOURCE_PATH + "cjkencodings.properties");
104                cjkEncodings.load(is);
105                is.close();
106            }
107            catch (Exception e) {
108                cjkFonts = new Properties();
109                cjkEncodings = new Properties();
110            }
111            propertiesLoaded = true;
112        }
113    }
114
115    /** Creates a CJK font.
116     * @param fontName the name of the font
117     * @param enc the encoding of the font
118     * @param emb always <CODE>false</CODE>. CJK font and not embedded
119     * @throws DocumentException on error
120     */
121    CJKFont(String fontName, String enc, boolean emb) throws DocumentException {
122        loadProperties();
123        fontType = FONT_TYPE_CJK;
124        String nameBase = getBaseName(fontName);
125        if (!isCJKFont(nameBase, enc))
126            throw new DocumentException(MessageLocalization.getComposedMessage("font.1.with.2.encoding.is.not.a.cjk.font", fontName, enc));
127        if (nameBase.length() < fontName.length()) {
128            style = fontName.substring(nameBase.length());
129            fontName = nameBase;
130        }
131        this.fontName = fontName;
132        encoding = CJK_ENCODING;
133        vertical = enc.endsWith("V");
134        CMap = enc;
135        if (enc.startsWith("Identity-")) {
136            cidDirect = true;
137            String s = cjkFonts.getProperty(fontName);
138            s = s.substring(0, s.indexOf('_'));
139            char c[] = allCMaps.get(s);
140            if (c == null) {
141                c = readCMap(s);
142                if (c == null)
143                    throw new DocumentException(MessageLocalization.getComposedMessage("the.cmap.1.does.not.exist.as.a.resource", s));
144                c[CID_NEWLINE] = '\n';
145                allCMaps.put(s, c);
146            }
147            translationMap = c;
148        }
149        else {
150            char c[] = allCMaps.get(enc);
151            if (c == null) {
152                String s = cjkEncodings.getProperty(enc);
153                if (s == null)
154                    throw new DocumentException(MessageLocalization.getComposedMessage("the.resource.cjkencodings.properties.does.not.contain.the.encoding.1", enc));
155                StringTokenizer tk = new StringTokenizer(s);
156                String nt = tk.nextToken();
157                c = allCMaps.get(nt);
158                if (c == null) {
159                    c = readCMap(nt);
160                    allCMaps.put(nt, c);
161                }
162                if (tk.hasMoreTokens()) {
163                    String nt2 = tk.nextToken();
164                    char m2[] = readCMap(nt2);
165                    for (int k = 0; k < 0x10000; ++k) {
166                        if (m2[k] == 0)
167                            m2[k] = c[k];
168                    }
169                    allCMaps.put(enc, m2);
170                    c = m2;
171                }
172            }
173            translationMap = c;
174        }
175        fontDesc = allFonts.get(fontName);
176        if (fontDesc == null) {
177            fontDesc = readFontProperties(fontName);
178            allFonts.put(fontName, fontDesc);
179        }
180        hMetrics = (IntHashtable)fontDesc.get("W");
181        vMetrics = (IntHashtable)fontDesc.get("W2");
182    }
183
184    /** Checks if its a valid CJK font.
185     * @param fontName the font name
186     * @param enc the encoding
187     * @return <CODE>true</CODE> if it is CJK font
188     */
189    public static boolean isCJKFont(String fontName, String enc) {
190        loadProperties();
191        String encodings = cjkFonts.getProperty(fontName);
192        return encodings != null && (enc.equals("Identity-H") || enc.equals("Identity-V") || encodings.indexOf("_" + enc + "_") >= 0);
193    }
194
195    /**
196     * Gets the width of a <CODE>char</CODE> in normalized 1000 units.
197     * @param char1 the unicode <CODE>char</CODE> to get the width of
198     * @return the width in normalized 1000 units
199     */
200    @Override
201    public int getWidth(int char1) {
202        int c = char1;
203        if (!cidDirect)
204            c = translationMap[c];
205        int v;
206        if (vertical)
207            v = vMetrics.get(c);
208        else
209            v = hMetrics.get(c);
210        if (v > 0)
211            return v;
212        else
213            return 1000;
214    }
215
216    @Override
217    public int getWidth(String text) {
218        int total = 0;
219        for (int k = 0; k < text.length(); ++k) {
220            int c = text.charAt(k);
221            if (!cidDirect)
222                c = translationMap[c];
223            int v;
224            if (vertical)
225                v = vMetrics.get(c);
226            else
227                v = hMetrics.get(c);
228            if (v > 0)
229                total += v;
230            else
231                total += 1000;
232        }
233        return total;
234    }
235
236    @Override
237    int getRawWidth(int c, String name) {
238        return 0;
239    }
240
241    @Override
242    public int getKerning(int char1, int char2) {
243        return 0;
244    }
245
246    private PdfDictionary getFontDescriptor() {
247        PdfDictionary dic = new PdfDictionary(PdfName.FONTDESCRIPTOR);
248        dic.put(PdfName.ASCENT, new PdfLiteral((String)fontDesc.get("Ascent")));
249        dic.put(PdfName.CAPHEIGHT, new PdfLiteral((String)fontDesc.get("CapHeight")));
250        dic.put(PdfName.DESCENT, new PdfLiteral((String)fontDesc.get("Descent")));
251        dic.put(PdfName.FLAGS, new PdfLiteral((String)fontDesc.get("Flags")));
252        dic.put(PdfName.FONTBBOX, new PdfLiteral((String)fontDesc.get("FontBBox")));
253        dic.put(PdfName.FONTNAME, new PdfName(fontName + style));
254        dic.put(PdfName.ITALICANGLE, new PdfLiteral((String)fontDesc.get("ItalicAngle")));
255        dic.put(PdfName.STEMV, new PdfLiteral((String)fontDesc.get("StemV")));
256        PdfDictionary pdic = new PdfDictionary();
257        pdic.put(PdfName.PANOSE, new PdfString((String)fontDesc.get("Panose"), null));
258        dic.put(PdfName.STYLE, pdic);
259        return dic;
260    }
261
262    private PdfDictionary getCIDFont(PdfIndirectReference fontDescriptor, IntHashtable cjkTag) {
263        PdfDictionary dic = new PdfDictionary(PdfName.FONT);
264        dic.put(PdfName.SUBTYPE, PdfName.CIDFONTTYPE0);
265        dic.put(PdfName.BASEFONT, new PdfName(fontName + style));
266        dic.put(PdfName.FONTDESCRIPTOR, fontDescriptor);
267        int keys[] = cjkTag.toOrderedKeys();
268        String w = convertToHCIDMetrics(keys, hMetrics);
269        if (w != null)
270            dic.put(PdfName.W, new PdfLiteral(w));
271        if (vertical) {
272            w = convertToVCIDMetrics(keys, vMetrics, hMetrics);
273            if (w != null)
274                dic.put(PdfName.W2, new PdfLiteral(w));
275        }
276        else
277            dic.put(PdfName.DW, new PdfNumber(1000));
278        PdfDictionary cdic = new PdfDictionary();
279        cdic.put(PdfName.REGISTRY, new PdfString((String)fontDesc.get("Registry"), null));
280        cdic.put(PdfName.ORDERING, new PdfString((String)fontDesc.get("Ordering"), null));
281        cdic.put(PdfName.SUPPLEMENT, new PdfLiteral((String)fontDesc.get("Supplement")));
282        dic.put(PdfName.CIDSYSTEMINFO, cdic);
283        return dic;
284    }
285
286    private PdfDictionary getFontBaseType(PdfIndirectReference CIDFont) {
287        PdfDictionary dic = new PdfDictionary(PdfName.FONT);
288        dic.put(PdfName.SUBTYPE, PdfName.TYPE0);
289        String name = fontName;
290        if (style.length() > 0)
291            name += "-" + style.substring(1);
292        name += "-" + CMap;
293        dic.put(PdfName.BASEFONT, new PdfName(name));
294        dic.put(PdfName.ENCODING, new PdfName(CMap));
295        dic.put(PdfName.DESCENDANTFONTS, new PdfArray(CIDFont));
296        return dic;
297    }
298
299    @Override
300    void writeFont(PdfWriter writer, PdfIndirectReference ref, Object params[]) throws DocumentException, IOException {
301        IntHashtable cjkTag = (IntHashtable)params[0];
302        PdfIndirectReference ind_font = null;
303        PdfObject pobj = null;
304        PdfIndirectObject obj = null;
305        pobj = getFontDescriptor();
306        if (pobj != null){
307            obj = writer.addToBody(pobj);
308            ind_font = obj.getIndirectReference();
309        }
310        pobj = getCIDFont(ind_font, cjkTag);
311        if (pobj != null){
312            obj = writer.addToBody(pobj);
313            ind_font = obj.getIndirectReference();
314        }
315        pobj = getFontBaseType(ind_font);
316        writer.addToBody(pobj, ref);
317    }
318
319    /**
320     * You can't get the FontStream of a CJK font (CJK fonts are never embedded),
321     * so this method always returns null.
322         * @return      null
323     * @since   2.1.3
324     */
325    @Override
326    public PdfStream getFullFontStream() {
327        return null;
328    }
329
330    private float getDescNumber(String name) {
331        return Integer.parseInt((String)fontDesc.get(name));
332    }
333
334    private float getBBox(int idx) {
335        String s = (String)fontDesc.get("FontBBox");
336        StringTokenizer tk = new StringTokenizer(s, " []\r\n\t\f");
337        String ret = tk.nextToken();
338        for (int k = 0; k < idx; ++k)
339            ret = tk.nextToken();
340        return Integer.parseInt(ret);
341    }
342
343    /** Gets the font parameter identified by <CODE>key</CODE>. Valid values
344     * for <CODE>key</CODE> are <CODE>ASCENT</CODE>, <CODE>CAPHEIGHT</CODE>, <CODE>DESCENT</CODE>
345     * and <CODE>ITALICANGLE</CODE>.
346     * @param key the parameter to be extracted
347     * @param fontSize the font size in points
348     * @return the parameter in points
349     */
350    @Override
351    public float getFontDescriptor(int key, float fontSize) {
352        switch (key) {
353            case AWT_ASCENT:
354            case ASCENT:
355                return getDescNumber("Ascent") * fontSize / 1000;
356            case CAPHEIGHT:
357                return getDescNumber("CapHeight") * fontSize / 1000;
358            case AWT_DESCENT:
359            case DESCENT:
360                return getDescNumber("Descent") * fontSize / 1000;
361            case ITALICANGLE:
362                return getDescNumber("ItalicAngle");
363            case BBOXLLX:
364                return fontSize * getBBox(0) / 1000;
365            case BBOXLLY:
366                return fontSize * getBBox(1) / 1000;
367            case BBOXURX:
368                return fontSize * getBBox(2) / 1000;
369            case BBOXURY:
370                return fontSize * getBBox(3) / 1000;
371            case AWT_LEADING:
372                return 0;
373            case AWT_MAXADVANCE:
374                return fontSize * (getBBox(2) - getBBox(0)) / 1000;
375        }
376        return 0;
377    }
378
379    @Override
380    public String getPostscriptFontName() {
381        return fontName;
382    }
383
384    /** Gets the full name of the font. If it is a True Type font
385     * each array element will have {Platform ID, Platform Encoding ID,
386     * Language ID, font name}. The interpretation of this values can be
387     * found in the Open Type specification, chapter 2, in the 'name' table.<br>
388     * For the other fonts the array has a single element with {"", "", "",
389     * font name}.
390     * @return the full name of the font
391     */
392    @Override
393    public String[][] getFullFontName() {
394        return new String[][]{{"", "", "", fontName}};
395    }
396
397    /** Gets all the entries of the names-table. If it is a True Type font
398     * each array element will have {Name ID, Platform ID, Platform Encoding ID,
399     * Language ID, font name}. The interpretation of this values can be
400     * found in the Open Type specification, chapter 2, in the 'name' table.<br>
401     * For the other fonts the array has a single element with {"4", "", "", "",
402     * font name}.
403     * @return the full name of the font
404     */
405    @Override
406    public String[][] getAllNameEntries() {
407        return new String[][]{{"4", "", "", "", fontName}};
408    }
409
410    /** Gets the family name of the font. If it is a True Type font
411     * each array element will have {Platform ID, Platform Encoding ID,
412     * Language ID, font name}. The interpretation of this values can be
413     * found in the Open Type specification, chapter 2, in the 'name' table.<br>
414     * For the other fonts the array has a single element with {"", "", "",
415     * font name}.
416     * @return the family name of the font
417     */
418    @Override
419    public String[][] getFamilyFontName() {
420        return getFullFontName();
421    }
422
423    static char[] readCMap(String name) {
424        try {
425            name = name + ".cmap";
426            InputStream is = getResourceStream(RESOURCE_PATH + name);
427            char c[] = new char[0x10000];
428            for (int k = 0; k < 0x10000; ++k)
429                c[k] = (char)((is.read() << 8) + is.read());
430            is.close();
431            return c;
432        }
433        catch (Exception e) {
434            // empty on purpose
435        }
436        return null;
437    }
438
439    static IntHashtable createMetric(String s) {
440        IntHashtable h = new IntHashtable();
441        StringTokenizer tk = new StringTokenizer(s);
442        while (tk.hasMoreTokens()) {
443            int n1 = Integer.parseInt(tk.nextToken());
444            h.put(n1, Integer.parseInt(tk.nextToken()));
445        }
446        return h;
447    }
448
449    static String convertToHCIDMetrics(int keys[], IntHashtable h) {
450        if (keys.length == 0)
451            return null;
452        int lastCid = 0;
453        int lastValue = 0;
454        int start;
455        for (start = 0; start < keys.length; ++start) {
456            lastCid = keys[start];
457            lastValue = h.get(lastCid);
458            if (lastValue != 0) {
459                ++start;
460                break;
461            }
462        }
463        if (lastValue == 0)
464            return null;
465        StringBuffer buf = new StringBuffer();
466        buf.append('[');
467        buf.append(lastCid);
468        int state = FIRST;
469        for (int k = start; k < keys.length; ++k) {
470            int cid = keys[k];
471            int value = h.get(cid);
472            if (value == 0)
473                continue;
474            switch (state) {
475                case FIRST: {
476                    if (cid == lastCid + 1 && value == lastValue) {
477                        state = SERIAL;
478                    }
479                    else if (cid == lastCid + 1) {
480                        state = BRACKET;
481                        buf.append('[').append(lastValue);
482                    }
483                    else {
484                        buf.append('[').append(lastValue).append(']').append(cid);
485                    }
486                    break;
487                }
488                case BRACKET: {
489                    if (cid == lastCid + 1 && value == lastValue) {
490                        state = SERIAL;
491                        buf.append(']').append(lastCid);
492                    }
493                    else if (cid == lastCid + 1) {
494                        buf.append(' ').append(lastValue);
495                    }
496                    else {
497                        state = FIRST;
498                        buf.append(' ').append(lastValue).append(']').append(cid);
499                    }
500                    break;
501                }
502                case SERIAL: {
503                    if (cid != lastCid + 1 || value != lastValue) {
504                        buf.append(' ').append(lastCid).append(' ').append(lastValue).append(' ').append(cid);
505                        state = FIRST;
506                    }
507                    break;
508                }
509            }
510            lastValue = value;
511            lastCid = cid;
512        }
513        switch (state) {
514            case FIRST: {
515                buf.append('[').append(lastValue).append("]]");
516                break;
517            }
518            case BRACKET: {
519                buf.append(' ').append(lastValue).append("]]");
520                break;
521            }
522            case SERIAL: {
523                buf.append(' ').append(lastCid).append(' ').append(lastValue).append(']');
524                break;
525            }
526        }
527        return buf.toString();
528    }
529
530    static String convertToVCIDMetrics(int keys[], IntHashtable v, IntHashtable h) {
531        if (keys.length == 0)
532            return null;
533        int lastCid = 0;
534        int lastValue = 0;
535        int lastHValue = 0;
536        int start;
537        for (start = 0; start < keys.length; ++start) {
538            lastCid = keys[start];
539            lastValue = v.get(lastCid);
540            if (lastValue != 0) {
541                ++start;
542                break;
543            }
544            else
545                lastHValue = h.get(lastCid);
546        }
547        if (lastValue == 0)
548            return null;
549        if (lastHValue == 0)
550            lastHValue = 1000;
551        StringBuffer buf = new StringBuffer();
552        buf.append('[');
553        buf.append(lastCid);
554        int state = FIRST;
555        for (int k = start; k < keys.length; ++k) {
556            int cid = keys[k];
557            int value = v.get(cid);
558            if (value == 0)
559                continue;
560            int hValue = h.get(lastCid);
561            if (hValue == 0)
562                hValue = 1000;
563            switch (state) {
564                case FIRST: {
565                    if (cid == lastCid + 1 && value == lastValue && hValue == lastHValue) {
566                        state = SERIAL;
567                    }
568                    else {
569                        buf.append(' ').append(lastCid).append(' ').append(-lastValue).append(' ').append(lastHValue / 2).append(' ').append(V1Y).append(' ').append(cid);
570                    }
571                    break;
572                }
573                case SERIAL: {
574                    if (cid != lastCid + 1 || value != lastValue || hValue != lastHValue) {
575                        buf.append(' ').append(lastCid).append(' ').append(-lastValue).append(' ').append(lastHValue / 2).append(' ').append(V1Y).append(' ').append(cid);
576                        state = FIRST;
577                    }
578                    break;
579                }
580            }
581            lastValue = value;
582            lastCid = cid;
583            lastHValue = hValue;
584        }
585        buf.append(' ').append(lastCid).append(' ').append(-lastValue).append(' ').append(lastHValue / 2).append(' ').append(V1Y).append(" ]");
586        return buf.toString();
587    }
588
589    static HashMap<String, Object> readFontProperties(String name) {
590        try {
591            name += ".properties";
592            InputStream is = getResourceStream(RESOURCE_PATH + name);
593            Properties p = new Properties();
594            p.load(is);
595            is.close();
596            IntHashtable W = createMetric(p.getProperty("W"));
597            p.remove("W");
598            IntHashtable W2 = createMetric(p.getProperty("W2"));
599            p.remove("W2");
600            HashMap<String, Object> map = new HashMap<String, Object>();
601            for (Enumeration<Object> e = p.keys(); e.hasMoreElements();) {
602                Object obj = e.nextElement();
603                map.put((String)obj, p.getProperty((String)obj));
604            }
605            map.put("W", W);
606            map.put("W2", W2);
607            return map;
608        }
609        catch (Exception e) {
610            // empty on purpose
611        }
612        return null;
613    }
614
615    @Override
616    public int getUnicodeEquivalent(int c) {
617        if (cidDirect)
618            return translationMap[c];
619        return c;
620    }
621
622    @Override
623    public int getCidCode(int c) {
624        if (cidDirect)
625            return c;
626        return translationMap[c];
627    }
628
629    /** Checks if the font has any kerning pairs.
630     * @return always <CODE>false</CODE>
631     */
632    @Override
633    public boolean hasKernPairs() {
634        return false;
635    }
636
637    /**
638     * Checks if a character exists in this font.
639     * @param c the character to check
640     * @return <CODE>true</CODE> if the character has a glyph,
641     * <CODE>false</CODE> otherwise
642     */
643    @Override
644    public boolean charExists(int c) {
645        return translationMap[c] != 0;
646    }
647
648    /**
649     * Sets the character advance.
650     * @param c the character
651     * @param advance the character advance normalized to 1000 units
652     * @return <CODE>true</CODE> if the advance was set,
653     * <CODE>false</CODE> otherwise. Will always return <CODE>false</CODE>
654     */
655    @Override
656    public boolean setCharAdvance(int c, int advance) {
657        return false;
658    }
659
660    /**
661     * Sets the font name that will appear in the pdf font dictionary.
662     * Use with care as it can easily make a font unreadable if not embedded.
663     * @param name the new font name
664     */
665    @Override
666    public void setPostscriptFontName(String name) {
667        fontName = name;
668    }
669
670    @Override
671    public boolean setKerning(int char1, int char2, int kern) {
672        return false;
673    }
674
675    @Override
676    public int[] getCharBBox(int c) {
677        return null;
678    }
679
680    @Override
681    protected int[] getRawCharBBox(int c, String name) {
682        return null;
683    }
684}