1 /*
2  * $Id: CJKFont.java 3595 2008-10-08 13:46:32Z psoares33 $
3  *
4  * Copyright 2000, 2001, 2002 by Paulo Soares.
5  *
6  * The contents of this file are subject to the Mozilla Public License Version 1.1
7  * (the "License"); you may not use this file except in compliance with the License.
8  * You may obtain a copy of the License at http://www.mozilla.org/MPL/
9  *
10  * Software distributed under the License is distributed on an "AS IS" basis,
11  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12  * for the specific language governing rights and limitations under the License.
13  *
14  * The Original Code is 'iText, a free JAVA-PDF library'.
15  *
16  * The Initial Developer of the Original Code is Bruno Lowagie. Portions created by
17  * the Initial Developer are Copyright (C) 1999, 2000, 2001, 2002 by Bruno Lowagie.
18  * All Rights Reserved.
19  * Co-Developer of the code is Paulo Soares. Portions created by the Co-Developer
20  * are Copyright (C) 2000, 2001, 2002 by Paulo Soares. All Rights Reserved.
21  *
22  * Contributor(s): all the names of the contributors are added in the source code
23  * where applicable.
24  *
25  * Alternatively, the contents of this file may be used under the terms of the
26  * LGPL license (the "GNU LIBRARY GENERAL PUBLIC LICENSE"), in which case the
27  * provisions of LGPL are applicable instead of those above.  If you wish to
28  * allow use of your version of this file only under the terms of the LGPL
29  * License and not to allow others to use your version of this file under
30  * the MPL, indicate your decision by deleting the provisions above and
31  * replace them with the notice and other provisions required by the LGPL.
32  * If you do not delete the provisions above, a recipient may use your version
33  * of this file under either the MPL or the GNU LIBRARY GENERAL PUBLIC LICENSE.
34  *
35  * This library is free software; you can redistribute it and/or modify it
36  * under the terms of the MPL as stated above or under the terms of the GNU
37  * Library General Public License as published by the Free Software Foundation;
38  * either version 2 of the License, or any later version.
39  *
40  * This library is distributed in the hope that it will be useful, but WITHOUT
41  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
42  * FOR A PARTICULAR PURPOSE. See the GNU Library general Public License for more
43  * details.
44  *
45  * If you didn't download this code from the following link, you should check if
46  * you aren't using an obsolete version:
47  * http://www.lowagie.com/iText/
48  */

49
50 package com.lowagie.text.pdf;
51
52 import java.io.IOException;
53 import java.io.InputStream;
54 import java.util.Enumeration;
55 import java.util.HashMap;
56 import java.util.Hashtable;
57 import java.util.Properties;
58 import java.util.StringTokenizer;
59
60 import com.lowagie.text.DocumentException;
61
62 /**
63  * Creates a CJK font compatible with the fonts in the Adobe Asian font Pack.
64  *
65  * @author  Paulo Soares (psoares@consiste.pt)
66  */

67
68 class CJKFont extends BaseFont {
69     /** The encoding used in the PDF document for CJK fonts
70      */

71     static final String CJK_ENCODING = "UnicodeBigUnmarked";
72     private static final int FIRST = 0;
73     private static final int BRACKET = 1;
74     private static final int SERIAL = 2;
75     private static final int V1Y = 880;
76         
77     static Properties cjkFonts = new Properties();
78     static Properties cjkEncodings = new Properties();
79     static Hashtable allCMaps = new Hashtable();
80     static Hashtable allFonts = new Hashtable();
81     private static boolean propertiesLoaded = false;
82     
83     /** The font name */
84     private String fontName;
85     /** The style modifier */
86     private String style = "";
87     /** The CMap name associated with this font */
88     private String CMap;
89     
90     private boolean cidDirect = false;
91     
92     private char[] translationMap;
93     private IntHashtable vMetrics;
94     private IntHashtable hMetrics;
95     private HashMap fontDesc;
96     private boolean vertical = false;
97     
98     private static void loadProperties() {
99         if (propertiesLoaded)
100             return;
101         synchronized (allFonts) {
102             if (propertiesLoaded)
103                 return;
104             try {
105                 InputStream is = getResourceStream(RESOURCE_PATH + "cjkfonts.properties");
106                 cjkFonts.load(is);
107                 is.close();
108                 is = getResourceStream(RESOURCE_PATH + "cjkencodings.properties");
109                 cjkEncodings.load(is);
110                 is.close();
111             }
112             catch (Exception e) {
113                 cjkFonts = new Properties();
114                 cjkEncodings = new Properties();
115             }
116             propertiesLoaded = true;
117         }
118     }
119     
120     /** Creates a CJK font.
121      * @param fontName the name of the font
122      * @param enc the encoding of the font
123      * @param emb always <CODE>false</CODE>. CJK font and not embedded
124      * @throws DocumentException on error
125      */

126     CJKFont(String fontName, String enc, boolean emb) throws DocumentException {
127         loadProperties();
128         fontType = FONT_TYPE_CJK;
129         String nameBase = getBaseName(fontName);
130         if (!isCJKFont(nameBase, enc))
131             throw new DocumentException("Font '" + fontName + "' with '" + enc + "' encoding is not a CJK font.");
132         if (nameBase.length() < fontName.length()) {
133             style = fontName.substring(nameBase.length());
134             fontName = nameBase;
135         }
136         this.fontName = fontName;
137         encoding = CJK_ENCODING;
138         vertical = enc.endsWith("V");
139         CMap = enc;
140         if (enc.startsWith("Identity-")) {
141             cidDirect = true;
142             String s = cjkFonts.getProperty(fontName);
143             s = s.substring(0, s.indexOf('_'));
144             char c[] = (char[])allCMaps.get(s);
145             if (c == null) {
146                 c = readCMap(s);
147                 if (c == null)
148                     throw new DocumentException("The cmap " + s + " does not exist as a resource.");
149                 c[CID_NEWLINE] = '\n';
150                 allCMaps.put(s, c);
151             }
152             translationMap = c;
153         }
154         else {
155             char c[] = (char[])allCMaps.get(enc);
156             if (c == null) {
157                 String s = cjkEncodings.getProperty(enc);
158                 if (s == null)
159                     throw new DocumentException("The resource cjkencodings.properties does not contain the encoding " + enc);
160                 StringTokenizer tk = new StringTokenizer(s);
161                 String nt = tk.nextToken();
162                 c = (char[])allCMaps.get(nt);
163                 if (c == null) {
164                     c = readCMap(nt);
165                     allCMaps.put(nt, c);
166                 }
167                 if (tk.hasMoreTokens()) {
168                     String nt2 = tk.nextToken();
169                     char m2[] = readCMap(nt2);
170                     for (int k = 0; k < 0x10000; ++k) {
171                         if (m2[k] == 0)
172                             m2[k] = c[k];
173                     }
174                     allCMaps.put(enc, m2);
175                     c = m2;
176                 }
177             }
178             translationMap = c;
179         }
180         fontDesc = (HashMap)allFonts.get(fontName);
181         if (fontDesc == null) {
182             fontDesc = readFontProperties(fontName);
183             allFonts.put(fontName, fontDesc);
184         }
185         hMetrics = (IntHashtable)fontDesc.get("W");
186         vMetrics = (IntHashtable)fontDesc.get("W2");
187     }
188     
189     /** Checks if its a valid CJK font.
190      * @param fontName the font name
191      * @param enc the encoding
192      * @return <CODE>true</CODE> if it is CJK font
193      */

194     public static boolean isCJKFont(String fontName, String enc) {
195         loadProperties();
196         String encodings = cjkFonts.getProperty(fontName);
197         return (encodings != null && (enc.equals("Identity-H") || enc.equals("Identity-V") || encodings.indexOf("_" + enc + "_") >= 0));
198     }
199         
200     /**
201      * Gets the width of a <CODE>char</CODE> in normalized 1000 units.
202      * @param char1 the unicode <CODE>char</CODE> to get the width of
203      * @return the width in normalized 1000 units
204      */

205     public int getWidth(int char1) {
206         int c = char1;
207         if (!cidDirect)
208             c = translationMap[c];
209         int v;
210         if (vertical)
211             v = vMetrics.get(c);
212         else
213             v = hMetrics.get(c);
214         if (v > 0)
215             return v;
216         else
217             return 1000;
218     }
219     
220     public int getWidth(String text) {
221         int total = 0;
222         for (int k = 0; k < text.length(); ++k) {
223             int c = text.charAt(k);
224             if (!cidDirect)
225                 c = translationMap[c];
226             int v;
227             if (vertical)
228                 v = vMetrics.get(c);
229             else
230                 v = hMetrics.get(c);
231             if (v > 0)
232                 total += v;
233             else
234                 total += 1000;
235         }
236         return total;
237     }
238     
239     int getRawWidth(int c, String name) {
240         return 0;
241     }
242   
243     public int getKerning(int char1, int char2) {
244         return 0;
245     }
246
247     private PdfDictionary getFontDescriptor() {
248         PdfDictionary dic = new PdfDictionary(PdfName.FONTDESCRIPTOR);
249         dic.put(PdfName.ASCENT, new PdfLiteral((String)fontDesc.get("Ascent")));
250         dic.put(PdfName.CAPHEIGHT, new PdfLiteral((String)fontDesc.get("CapHeight")));
251         dic.put(PdfName.DESCENT, new PdfLiteral((String)fontDesc.get("Descent")));
252         dic.put(PdfName.FLAGS, new PdfLiteral((String)fontDesc.get("Flags")));
253         dic.put(PdfName.FONTBBOX, new PdfLiteral((String)fontDesc.get("FontBBox")));
254         dic.put(PdfName.FONTNAME, new PdfName(fontName + style));
255         dic.put(PdfName.ITALICANGLE, new PdfLiteral((String)fontDesc.get("ItalicAngle")));
256         dic.put(PdfName.STEMV, new PdfLiteral((String)fontDesc.get("StemV")));
257         PdfDictionary pdic = new PdfDictionary();
258         pdic.put(PdfName.PANOSE, new PdfString((String)fontDesc.get("Panose"), null));
259         dic.put(PdfName.STYLE, pdic);
260         return dic;
261     }
262     
263     private PdfDictionary getCIDFont(PdfIndirectReference fontDescriptor, IntHashtable cjkTag) {
264         PdfDictionary dic = new PdfDictionary(PdfName.FONT);
265         dic.put(PdfName.SUBTYPE, PdfName.CIDFONTTYPE0);
266         dic.put(PdfName.BASEFONT, new PdfName(fontName + style));
267         dic.put(PdfName.FONTDESCRIPTOR, fontDescriptor);
268         int keys[] = cjkTag.toOrderedKeys();
269         String w = convertToHCIDMetrics(keys, hMetrics);
270         if (w != null)
271             dic.put(PdfName.W, new PdfLiteral(w));
272         if (vertical) {
273             w = convertToVCIDMetrics(keys, vMetrics, hMetrics);
274             if (w != null)
275                 dic.put(PdfName.W2, new PdfLiteral(w));
276         }
277         else
278             dic.put(PdfName.DW, new PdfNumber(1000));
279         PdfDictionary cdic = new PdfDictionary();
280         cdic.put(PdfName.REGISTRY, new PdfString((String)fontDesc.get("Registry"), null));
281         cdic.put(PdfName.ORDERING, new PdfString((String)fontDesc.get("Ordering"), null));
282         cdic.put(PdfName.SUPPLEMENT, new PdfLiteral((String)fontDesc.get("Supplement")));
283         dic.put(PdfName.CIDSYSTEMINFO, cdic);
284         return dic;
285     }
286     
287     private PdfDictionary getFontBaseType(PdfIndirectReference CIDFont) {
288         PdfDictionary dic = new PdfDictionary(PdfName.FONT);
289         dic.put(PdfName.SUBTYPE, PdfName.TYPE0);
290         String name = fontName;
291         if (style.length() > 0)
292             name += "-" + style.substring(1);
293         name += "-" + CMap;
294         dic.put(PdfName.BASEFONT, new PdfName(name));
295         dic.put(PdfName.ENCODING, new PdfName(CMap));
296         dic.put(PdfName.DESCENDANTFONTS, new PdfArray(CIDFont));
297         return dic;
298     }
299     
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     public PdfStream getFullFontStream() {
326         return null;
327     }
328     
329     private float getDescNumber(String name) {
330         return Integer.parseInt((String)fontDesc.get(name));
331     }
332     
333     private float getBBox(int idx) {
334         String s = (String)fontDesc.get("FontBBox");
335         StringTokenizer tk = new StringTokenizer(s, " []\r\n\t\f");
336         String ret = tk.nextToken();
337         for (int k = 0; k < idx; ++k)
338             ret = tk.nextToken();
339         return Integer.parseInt(ret);
340     }
341     
342     /** Gets the font parameter identified by <CODE>key</CODE>. Valid values
343      * for <CODE>key</CODE> are <CODE>ASCENT</CODE>, <CODE>CAPHEIGHT</CODE>, <CODE>DESCENT</CODE>
344      * and <CODE>ITALICANGLE</CODE>.
345      * @param key the parameter to be extracted
346      * @param fontSize the font size in points
347      * @return the parameter in points
348      */

349     public float getFontDescriptor(int key, float fontSize) {
350         switch (key) {
351             case AWT_ASCENT:
352             case ASCENT:
353                 return getDescNumber("Ascent") * fontSize / 1000;
354             case CAPHEIGHT:
355                 return getDescNumber("CapHeight") * fontSize / 1000;
356             case AWT_DESCENT:
357             case DESCENT:
358                 return getDescNumber("Descent") * fontSize / 1000;
359             case ITALICANGLE:
360                 return getDescNumber("ItalicAngle");
361             case BBOXLLX:
362                 return fontSize * getBBox(0) / 1000;
363             case BBOXLLY:
364                 return fontSize * getBBox(1) / 1000;
365             case BBOXURX:
366                 return fontSize * getBBox(2) / 1000;
367             case BBOXURY:
368                 return fontSize * getBBox(3) / 1000;
369             case AWT_LEADING:
370                 return 0;
371             case AWT_MAXADVANCE:
372                 return fontSize * (getBBox(2) - getBBox(0)) / 1000;
373         }
374         return 0;
375     }
376     
377     public String getPostscriptFontName() {
378         return fontName;
379     }
380     
381     /** Gets the full name of the font. If it is a True Type font
382      * each array element will have {Platform ID, Platform Encoding ID,
383      * Language ID, font name}. The interpretation of this values can be
384      * found in the Open Type specification, chapter 2, in the 'name' table.<br>
385      * For the other fonts the array has a single element with {"""""",
386      * font name}.
387      * @return the full name of the font
388      */

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

401     public String[][] getAllNameEntries() {
402         return new String[][]{{"4""""""", fontName}};
403     }
404     
405     /** Gets the family name of the font. If it is a True Type font
406      * each array element will have {Platform ID, Platform Encoding ID,
407      * Language ID, font name}. The interpretation of this values can be
408      * found in the Open Type specification, chapter 2, in the 'name' table.<br>
409      * For the other fonts the array has a single element with {"""""",
410      * font name}.
411      * @return the family name of the font
412      */

413     public String[][] getFamilyFontName() {
414         return getFullFontName();
415     }
416     
417     static char[] readCMap(String name) {
418         try {
419             name = name + ".cmap";
420             InputStream is = getResourceStream(RESOURCE_PATH + name);
421             char c[] = new char[0x10000];
422             for (int k = 0; k < 0x10000; ++k)
423                 c[k] = (char)((is.read() << 8) + is.read());
424             is.close();
425             return c;
426         }
427         catch (Exception e) {
428             // empty on purpose
429         }
430         return null;
431     }
432     
433     static IntHashtable createMetric(String s) {
434         IntHashtable h = new IntHashtable();
435         StringTokenizer tk = new StringTokenizer(s);
436         while (tk.hasMoreTokens()) {
437             int n1 = Integer.parseInt(tk.nextToken());
438             h.put(n1, Integer.parseInt(tk.nextToken()));
439         }
440         return h;
441     }
442     
443     static String convertToHCIDMetrics(int keys[], IntHashtable h) {
444         if (keys.length == 0)
445             return null;
446         int lastCid = 0;
447         int lastValue = 0;
448         int start;
449         for (start = 0; start < keys.length; ++start) {
450             lastCid = keys[start];
451             lastValue = h.get(lastCid);
452             if (lastValue != 0) {
453                 ++start;
454                 break;
455             }
456         }
457         if (lastValue == 0)
458             return null;
459         StringBuffer buf = new StringBuffer();
460         buf.append('[');
461         buf.append(lastCid);
462         int state = FIRST;
463         for (int k = start; k < keys.length; ++k) {
464             int cid = keys[k];
465             int value = h.get(cid);
466             if (value == 0)
467                 continue;
468             switch (state) {
469                 case FIRST: {
470                     if (cid == lastCid + 1 && value == lastValue) {
471                         state = SERIAL;
472                     }
473                     else if (cid == lastCid + 1) {
474                         state = BRACKET;
475                         buf.append('[').append(lastValue);
476                     }
477                     else {
478                         buf.append('[').append(lastValue).append(']').append(cid);
479                     }
480                     break;
481                 }
482                 case BRACKET: {
483                     if (cid == lastCid + 1 && value == lastValue) {
484                         state = SERIAL;
485                         buf.append(']').append(lastCid);
486                     }
487                     else if (cid == lastCid + 1) {
488                         buf.append(' ').append(lastValue);
489                     }
490                     else {
491                         state = FIRST;
492                         buf.append(' ').append(lastValue).append(']').append(cid);
493                     }
494                     break;
495                 }
496                 case SERIAL: {
497                     if (cid != lastCid + 1 || value != lastValue) {
498                         buf.append(' ').append(lastCid).append(' ').append(lastValue).append(' ').append(cid);
499                         state = FIRST;
500                     }
501                     break;
502                 }
503             }
504             lastValue = value;
505             lastCid = cid;
506         }
507         switch (state) {
508             case FIRST: {
509                 buf.append('[').append(lastValue).append("]]");
510                 break;
511             }
512             case BRACKET: {
513                 buf.append(' ').append(lastValue).append("]]");
514                 break;
515             }
516             case SERIAL: {
517                 buf.append(' ').append(lastCid).append(' ').append(lastValue).append(']');
518                 break;
519             }
520         }
521         return buf.toString();
522     }
523     
524     static String convertToVCIDMetrics(int keys[], IntHashtable v, IntHashtable h) {
525         if (keys.length == 0)
526             return null;
527         int lastCid = 0;
528         int lastValue = 0;
529         int lastHValue = 0;
530         int start;
531         for (start = 0; start < keys.length; ++start) {
532             lastCid = keys[start];
533             lastValue = v.get(lastCid);
534             if (lastValue != 0) {
535                 ++start;
536                 break;
537             }
538             else
539                 lastHValue = h.get(lastCid);
540         }
541         if (lastValue == 0)
542             return null;
543         if (lastHValue == 0)
544             lastHValue = 1000;
545         StringBuffer buf = new StringBuffer();
546         buf.append('[');
547         buf.append(lastCid);
548         int state = FIRST;
549         for (int k = start; k < keys.length; ++k) {
550             int cid = keys[k];
551             int value = v.get(cid);
552             if (value == 0)
553                 continue;
554             int hValue = h.get(lastCid);
555             if (hValue == 0)
556                 hValue = 1000;
557             switch (state) {
558                 case FIRST: {
559                     if (cid == lastCid + 1 && value == lastValue && hValue == lastHValue) {
560                         state = SERIAL;
561                     }
562                     else {
563                         buf.append(' ').append(lastCid).append(' ').append(-lastValue).append(' ').append(lastHValue / 2).append(' ').append(V1Y).append(' ').append(cid);
564                     }
565                     break;
566                 }
567                 case SERIAL: {
568                     if (cid != lastCid + 1 || value != lastValue || hValue != lastHValue) {
569                         buf.append(' ').append(lastCid).append(' ').append(-lastValue).append(' ').append(lastHValue / 2).append(' ').append(V1Y).append(' ').append(cid);
570                         state = FIRST;
571                     }
572                     break;
573                 }
574             }
575             lastValue = value;
576             lastCid = cid;
577             lastHValue = hValue;
578         }
579         buf.append(' ').append(lastCid).append(' ').append(-lastValue).append(' ').append(lastHValue / 2).append(' ').append(V1Y).append(" ]");
580         return buf.toString();
581     }
582     
583     static HashMap readFontProperties(String name) {
584         try {
585             name += ".properties";
586             InputStream is = getResourceStream(RESOURCE_PATH + name);
587             Properties p = new Properties();
588             p.load(is);
589             is.close();
590             IntHashtable W = createMetric(p.getProperty("W"));
591             p.remove("W");
592             IntHashtable W2 = createMetric(p.getProperty("W2"));
593             p.remove("W2");
594             HashMap map = new HashMap();
595             for (Enumeration e = p.keys(); e.hasMoreElements();) {
596                 Object obj = e.nextElement();
597                 map.put(obj, p.getProperty((String)obj));
598             }
599             map.put("W", W);
600             map.put("W2", W2);
601             return map;
602         }
603         catch (Exception e) {
604             // empty on purpose
605         }
606         return null;
607     }
608
609     public int getUnicodeEquivalent(int c) {
610         if (cidDirect)
611             return translationMap[c];
612         return c;
613     }
614     
615     public int getCidCode(int c) {
616         if (cidDirect)
617             return c;
618         return translationMap[c];
619     }
620     
621     /** Checks if the font has any kerning pairs.
622      * @return always <CODE>false</CODE>
623      */
    
624     public boolean hasKernPairs() {
625         return false;
626     }
627     
628     /**
629      * Checks if a character exists in this font.
630      * @param c the character to check
631      * @return <CODE>true</CODE> if the character has a glyph,
632      * <CODE>false</CODE> otherwise
633      */

634     public boolean charExists(int c) {
635         return translationMap[c] != 0;
636     }
637     
638     /**
639      * Sets the character advance.
640      * @param c the character
641      * @param advance the character advance normalized to 1000 units
642      * @return <CODE>true</CODE> if the advance was set,
643      * <CODE>false</CODE> otherwise. Will always return <CODE>false</CODE>
644      */

645     public boolean setCharAdvance(int c, int advance) {
646         return false;
647     }
648     
649     /**
650      * Sets the font name that will appear in the pdf font dictionary.
651      * Use with care as it can easily make a font unreadable if not embedded.
652      * @param name the new font name
653      */
    
654     public void setPostscriptFontName(String name) {
655         fontName = name;
656     }   
657     
658     public boolean setKerning(int char1, int char2, int kern) {
659         return false;
660     }
661     
662     public int[] getCharBBox(int c) {
663         return null;
664     }
665     
666     protected int[] getRawCharBBox(int c, String name) {
667         return null;
668     }
669 }