View Javadoc

1   /* ========================================================================
2    * JFiglet, a free open source java implementation of figlet and the
3    * figfont specification (see http://www.figlet.org)
4    * Copyright (C) 2004 Sebastien Brunot
5    *
6    * This library is free software; you can redistribute it and/or
7    * modify it under the terms of the GNU Lesser General Public
8    * License as published by the Free Software Foundation; either
9    * version 2.1 of the License, or (at your option) any later version.
10   *
11   * This library is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14   * Lesser General Public License for more details.
15   *
16   * You should have received a copy of the GNU Lesser General Public
17   * License along with this library; if not, write to the Free Software
18   * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19   * ========================================================================
20   */
21  package org.gnu.jfiglet.core;
22  
23  import java.io.BufferedReader;
24  import java.io.IOException;
25  import java.io.InputStream;
26  import java.io.InputStreamReader;
27  import java.net.URL;
28  import java.util.NoSuchElementException;
29  import java.util.StringTokenizer;
30  import java.util.zip.ZipEntry;
31  import java.util.zip.ZipException;
32  import java.util.zip.ZipInputStream;
33  
34  /***
35   * A FIGfont, which file format is specified by
36   * <A HREF="doc-files/figfont.txt">this document</A>.
37   *
38   * @TODO : READ THE EXTRA NON ASCII CHARACTERS IN FIGFONT FILES
39   *
40   * @version $Id: FIGFont.java,v 1.3 2004/04/27 20:09:28 sbrunot Exp $
41   *
42   * @author <a href="mailto:sebastien.brunot@club-internet.fr">
43   *         Sebastien Brunot</a>
44   */
45  public class FIGFont
46  {
47      /////////////////////////////////////
48      // Constants
49      /////////////////////////////////////
50  
51      /***
52       * The maximum valid code for a character of the FIGFont
53       * 8-bit characterset
54       */
55      public static final int MAX_8BIT_CHARSET_CHAR_CODE = 255;
56  
57      /***
58       * The separator between tokens in a FIGFont file header
59       */
60      private static final String FIG_FONT_HEADER_TOKEN_SEPARATOR = " ";
61  
62      /***
63       * The signature of a FIGFont file (the 5 first characters of the
64       * (unziped) file)
65       */
66      private static final String FIGFONT_FILE_SIGNATURE = "flf2a";
67  
68      /***
69       * The extension of a FIGFont file (which name is FIGFontName.extension)
70       */
71      private static final String FIGFONT_EXTENSION = "flf";
72  
73      /***
74       * The name of the fonts directory in the classpath
75       */
76      private static final String FONT_DIRECTORY_NAME = "fonts";
77  
78      /////////////////////////////////////
79      // Attributes
80      /////////////////////////////////////
81  
82      /***
83       * Informations about the FIGFont
84       */
85      private FIGFontInfo info = null;
86  
87      /***
88       * An array that holds the FIGCharacters of the font. The index
89       * of a FIGCharacter in this array is the code of the
90       * subcharacter it stands for in its characterset.
91       */
92      private FIGCharacter[] eightBitCharacterSet =
93          new FIGCharacter[MAX_8BIT_CHARSET_CHAR_CODE + 1];
94  
95      /////////////////////////////////////
96      // Constructor
97      /////////////////////////////////////
98  
99      /***
100      * Create an empty FIGFont whith a provided name.
101      * @param theName the name of the font
102      */
103     public FIGFont(String theName)
104     {
105         this.info = new FIGFontInfo();
106         this.info.setName(theName);
107     }
108 
109     /////////////////////////////////////
110     // Public methods
111     /////////////////////////////////////
112 
113     /***
114      * Load the content of the FIGFont from the <em>fontname</em>.flf file
115      * provided in the fonts directory in the classpath.
116      * @throws IOException if such an exception occured while accessing the
117      * <em>fontname</em>.flf file.
118      * @throws ZipException if the <em>fontname</em>.flf file is a zip file
119      * and such an exception occurs reading its content.
120      * @throws IllegalFIGFontFileException if the <em>fontname</em>.flf file
121      * provided in the classpath is not a valid FIGFont file or if there is
122      * no <em>fontname</em>.flf file provided in the fonts directory in the
123      * classpath.
124      */
125     public void loadFromFile()
126         throws IOException, ZipException, IllegalFIGFontFileException
127     {
128         // the path of the file that should holds the FIGFont data
129         String fontFilePath =
130             FONT_DIRECTORY_NAME
131                 + "/"
132                 + this.info.getName()
133                 + "."
134                 + FIGFONT_EXTENSION;
135         // get an URL to this file using system classloader
136         URL fontFileURL = ClassLoader.getSystemResource(fontFilePath);
137         if (fontFileURL == null)
138         {
139             // No such file has been found
140             throw new IllegalFIGFontFileException(
141                 "Cannot find " + fontFilePath + " in the classpath.");
142         }
143         // Open an input stream to the font file
144         InputStream fontFileInputStream = fontFileURL.openStream();
145         // the font file can either be a zip file or a "normal" file
146         // Create a zip input stream from the fontFileInputStream
147         ZipInputStream fontFileZipInputStream =
148             new ZipInputStream(fontFileInputStream);
149         // Move the input stream cursor to the beginning of the
150         // first zip file entry.
151         ZipEntry entry = fontFileZipInputStream.getNextEntry();
152         if (entry == null)
153         {
154             // the font file is not a zip file !
155             // As the getNextEntry move the input stream cursor
156             // to the next line, we have to "hard reset" the input
157             // stream
158             fontFileZipInputStream.close();
159             fontFileInputStream.close();
160             fontFileInputStream = fontFileURL.openStream();
161         }
162         else
163         {
164             // The font file is a zip : fontFileZipInputStream
165             // is an input stream to the unziped font file
166             fontFileInputStream = fontFileZipInputStream;
167         }
168         readFontFile(fontFileInputStream);
169         fontFileInputStream.close();
170     }
171 
172     /***
173      * Get one of the FIGFont FIGCharacters.
174      * @param theCharacterCode the code of the FIGCharacter in
175      * the FIGFont characterset. An IllegalArgumentException is thrown if
176      * this code is lower than 0 or greater than MAX_8BIT_CHARSET_CHAR_CODE
177      * @return the FIGCharacter which code is theCharacterCode in
178      * the FIGFont characterset. If no FIGCharacter is defined for this code
179      * in the FIGFont, the FIGCharacter which code is 0 is returned.
180      */
181     public FIGCharacter getFIGCharacter(int theCharacterCode)
182     {
183         if (theCharacterCode < 0
184             || theCharacterCode > MAX_8BIT_CHARSET_CHAR_CODE)
185         {
186             throw new IllegalArgumentException(
187                 "The character code is an integer between 0 and "
188                     + MAX_8BIT_CHARSET_CHAR_CODE
189                     + " included");
190         }
191         FIGCharacter returnValue = this.eightBitCharacterSet[theCharacterCode];
192         if (returnValue == null)
193         {
194             returnValue = this.eightBitCharacterSet[0];
195         }
196         return this.eightBitCharacterSet[theCharacterCode];
197     }
198 
199     /***
200      * Get informations about the FIGFont.
201      * @return informations about the FIGFont.
202      */
203     public FIGFontInfo getInfo()
204     {
205         return this.info;
206     }
207 
208     /////////////////////////////////////
209     // Private methods
210     /////////////////////////////////////
211 
212     /***
213      * Read the content of a .flf file and initialize the FIGFont
214      * with it.
215      * @param theFontFile an input stream to the content of the
216      * .flf file (or to the content of it first (unziped) entry
217      * if the .flf file is a zip file).
218      * @throws IOException if such an exception occurs reading
219      * theFontFile.
220      * @throws IllegalFIGFontFileException if  theFontFile does
221      * not provide the content of a correct .flf file (see
222      * <A HREF="http://www.figlet.org">http://www.figlet.org</A>
223      * for .flf file specification.
224      */
225     private void readFontFile(InputStream theFontFile)
226         throws IOException, IllegalFIGFontFileException
227     {
228         // Creates a Buffered reader to theFontFile
229         BufferedReader fontFileReader =
230             new BufferedReader(new InputStreamReader(theFontFile));
231         // Read the font file header line
232         String fontFileHeaderLine = fontFileReader.readLine();
233         try
234         {
235             // Get font informations from the header line
236             FIGFontInfo infosFromTheHeader =
237                 populateFIGFontInfoFromFIGFontFileHeader(fontFileHeaderLine);
238             // Add the font name to those informations
239             infosFromTheHeader.setName(this.info.getName());
240             // Set those informations as the current informations
241             this.info = infosFromTheHeader;
242         }
243         catch (IllegalArgumentException e)
244         {
245             // This exception is caused by an uncorrect header line
246             throw new IllegalFIGFontFileException(e.getMessage());
247         }
248         // Read the comments about the font and add them to the
249         // FIGFontInfo
250         for (int i = 0; i < this.info.getCommentLinesNumber(); i++)
251         {
252             this.info.addCommentLine(fontFileReader.readLine());
253         }
254         // Read FIGcharacters 32 through 126
255         for (int currentCharCode = 32;
256             currentCharCode < 127;
257             currentCharCode++)
258         {
259             String[] characterLines = new String[this.info.getHeight()];
260             for (int i = 0; i < this.info.getHeight(); i++)
261             {
262                 characterLines[i] = fontFileReader.readLine();
263             }
264             this.eightBitCharacterSet[currentCharCode] =
265                 new FIGCharacter(characterLines);
266         }
267         // Read required Deutsch characters
268         int[] deutchCharactersCodes = {196, 214, 220, 228, 246, 252, 223};
269         for (int i = 0; i < deutchCharactersCodes.length; i++)
270         {
271             String[] characterLines = new String[this.info.getHeight()];
272             for (int j = 0; j < this.info.getHeight(); j++)
273             {
274                 characterLines[j] = fontFileReader.readLine();
275             }
276             this.eightBitCharacterSet[deutchCharactersCodes[i]] =
277                 new FIGCharacter(characterLines);
278         }
279         // READ EXTRA CHARACTERS...
280         boolean readNextCharacterDefinition = true;
281         while (readNextCharacterDefinition)
282         {
283             // Read the next line if there is one
284             String characterHeader = fontFileReader.readLine();
285             if (characterHeader == null)
286             {
287                 // No more lines to read in the figfont file
288                 readNextCharacterDefinition = false;
289                 break;
290             }
291             else
292             {
293                 StringTokenizer tokenizer =
294                     new StringTokenizer(characterHeader);
295                 // Get the first token before the white space, which is the
296                 // character number
297                 String characterCodeAsString = tokenizer.nextToken(" ");
298                 try
299                 {
300                     int characterCode =
301                         decodeCharacterCode(characterCodeAsString);
302                     // read the character in the FIGFont file
303                     String[] characterLines = new String[this.info.getHeight()];
304                     for (int i = 0; i < this.info.getHeight(); i++)
305                     {
306                         characterLines[i] = fontFileReader.readLine();
307                         if (characterLines[i] == null)
308                         {
309                             throw new IllegalFIGFontFileException(
310                                 "Character which code is "
311                                     + characterCode
312                                     + " is not entirely defined");
313                         }
314                     }
315                     if ((characterCode > 0)
316                         && (characterCode < MAX_8BIT_CHARSET_CHAR_CODE))
317                     {
318                         // Store the extra character in the ascii characters
319                         // table
320                         this.eightBitCharacterSet[characterCode] =
321                             new FIGCharacter(characterLines);
322                     }
323                     else
324                     {
325                         // @TODO : this is a non ascii extra character : store
326                         // it somewhere !
327                     }
328                 }
329                 catch (NumberFormatException e)
330                 {
331                     throw new IllegalFIGFontFileException(
332                         "Line \"" + characterHeader + " is uncorrect");
333                 }
334             }
335         }
336     }
337 
338     /***
339      * Returns a FIGFontInfo object populated with informations readen from
340      * the header line of a .flf file.
341      * @param theFIGFontFileHeader the header line to get font informations
342      * from. An IllegalArgumentException is raised if this is not a legal
343      * font header line, with a message describing the error.
344      * @return a FIGFontInfo object populated with informations readen from
345      * theFIGFontFileHeader.
346      */
347     private FIGFontInfo populateFIGFontInfoFromFIGFontFileHeader(
348                                                 String theFIGFontFileHeader)
349     {
350         FIGFontInfo informations = new FIGFontInfo();
351         // Verify that the header is not null
352         if (theFIGFontFileHeader == null)
353         {
354             throw new IllegalArgumentException("The header is null");
355         }
356         // The old layout value that must be defined defined in the header,
357         // initialized here with an illegal value
358         int oldLayout = -10;
359         // The full layout value that could be defined in the header
360         Integer fullLayout = null;
361         // Build a StrinTokenizer to read entries of the header
362         StringTokenizer tokenizer =
363             new StringTokenizer(
364                 theFIGFontFileHeader,
365                 FIG_FONT_HEADER_TOKEN_SEPARATOR);
366         // the number of token the header is made of
367         int numberOfTokens = tokenizer.countTokens();
368         // According to the figfont spec, there must be at least 6 tokens,
369         // at most 9 
370         if (numberOfTokens < 6)
371         {
372             throw new IllegalArgumentException(
373                         "Not enough tokens in the header");
374         }
375         else if (numberOfTokens > 9)
376         {
377             throw new IllegalArgumentException("Too many tokens in the header");
378         }
379         // First token must be the file signature plus the hardblank character
380         String signatureAndHardblank = tokenizer.nextToken();
381         if (!(FIGFONT_FILE_SIGNATURE
382             .equals(
383                 signatureAndHardblank.substring(
384                     0,
385                     FIGFONT_FILE_SIGNATURE.length()))))
386         {
387             throw new IllegalArgumentException("Uncorrect signature");
388         }
389         // Set the hardblank value in the FIGFontInfo
390         informations.setHardblank(
391             signatureAndHardblank.charAt(FIGFONT_FILE_SIGNATURE.length()));
392         // Second token is the font height
393         try
394         {
395             informations.setHeight(Integer.parseInt(tokenizer.nextToken()));
396         }
397         catch (NumberFormatException e)
398         {
399             throw new IllegalArgumentException(
400                 "Font height must be an integer");
401         }
402         // Third token is the font baseline
403         try
404         {
405             informations.setBaseline(Integer.parseInt(tokenizer.nextToken()));
406         }
407         catch (NumberFormatException e)
408         {
409             throw new IllegalArgumentException(
410                 "Font baseline must be an integer");
411         }
412         // Fourth token is the font max length
413         try
414         {
415             informations.setMaxLength(Integer.parseInt(tokenizer.nextToken()));
416         }
417         catch (NumberFormatException e)
418         {
419             throw new IllegalArgumentException(
420                 "Font max length must be an integer");
421         }
422         // Fifth token is the old layout
423         try
424         {
425             oldLayout = Integer.parseInt(tokenizer.nextToken());
426         }
427         catch (NumberFormatException e)
428         {
429             throw new IllegalArgumentException(
430                 "Old layout must be an integer");
431         }
432         // Sixth token is the number of line of comments
433         try
434         {
435             informations.setCommentLinesNumber(
436                 Integer.parseInt(tokenizer.nextToken()));
437         }
438         catch (NumberFormatException e)
439         {
440             throw new IllegalArgumentException(
441                 "The number of lines of comments must be an integer");
442         }
443         try
444         {
445             try
446             {
447                 // Seventh token, if it exists, is the print direction
448                 Integer printDirection = new Integer(tokenizer.nextToken());
449                 if (printDirection != null)
450                 {
451                     // Verify that the defined print direction has a legal value
452                     if ((printDirection.intValue()
453                         != FIGFontInfo.PRINT_DIRECTION_LEFT_TO_RIGHT)
454                         && (printDirection.intValue()
455                             != FIGFontInfo.PRINT_DIRECTION_RIGHT_TO_LEFT))
456                     {
457                         throw new IllegalArgumentException(
458                             "Accepted values for printDirection are "
459                                 + FIGFontInfo.PRINT_DIRECTION_LEFT_TO_RIGHT
460                                 + " for LEFT_TO_RIGHT and "
461                                 + FIGFontInfo.PRINT_DIRECTION_RIGHT_TO_LEFT
462                                 + " for RIGHT_TO_LEFT");
463                     }
464                     // Add its value to the FIGFontInfo
465                     informations.setPrintDirection(printDirection.intValue());
466                 }
467                 // Eighth token, if it exists, is the full layout
468                 try
469                 {
470                     fullLayout = new Integer(tokenizer.nextToken());
471                 }
472                 catch (NumberFormatException e)
473                 {
474                     throw new IllegalArgumentException(
475                         "The Full Layout must be an integer");
476                 }
477                 // Ninth token, if it exists, is the codetag count.
478                 // IT IS NOT USED BY JFiglet.
479             }
480             catch (NumberFormatException e)
481             {
482                 throw new IllegalArgumentException(
483                     "The print direction must be an integer");
484             }
485         }
486         catch (NoSuchElementException e)
487         {
488             // There's no more token
489         }
490         // Create the layout specification
491         informations.setLayout(new FIGFontLayout(oldLayout, fullLayout));
492         // return the FIGFontInfo
493         return informations;
494     }
495 
496     /***
497      * Decode the code of an extra character.
498      * A character code may be expressed in decimal (as shown above, numbers
499      * we're all familiar with), or in Octal (seldom used) or in hexadecimal.
500      * Character codes expressed in octal must be preceded by "0" (zero), and if
501      * negative, "-" (minus) must precede the "0".  There are eight octal
502      * digits: 01234567.  You may recall octal numbers from school as "base 8
503      * numbers".
504      * Character codes expressed in hexadecimal must be preceded by "0x" or
505      * "0X" (That's also a zero.)  If negative, the "-" must precede the "0x".
506      * There are 16 hexadecimal digits: 01234567890ABCDEF.  (The "letter-digits"
507      * may also be lowercase.)  Hexadecimal is "base 16".
508      * @param theCharacterCodeAsString the code of the character as expressed in
509      * the FIGFont file
510      * @return the code of the character as an int.
511      * @throws NumberFormatException if theCharacterCodeAsString is not
512      * an integer in one of the recognized formats.
513      */
514     private int decodeCharacterCode(String theCharacterCodeAsString)
515     {
516         String codeAsString = new String(theCharacterCodeAsString);
517         // The sign of the integer (1 for positive, -1 for negative)
518         int sign = 1;
519         // The base the character is expressed in
520         int characterCodeRadix = 10;
521         if (codeAsString.charAt(0) == '-')
522         {
523             // First char is '-' : the sign is negative
524             sign = -1;
525             codeAsString = codeAsString.substring(1);
526         }
527         if (codeAsString.charAt(0) == '0')
528         {
529             // First number is a zero : base is 8 or 16
530             characterCodeRadix = 8;
531             codeAsString = codeAsString.substring(1);
532             if ((codeAsString.charAt(0) == 'X')
533                 || (codeAsString.charAt(0) == 'x'))
534             {
535                 // 0 is followed by X : base is 16
536                 characterCodeRadix = 16;
537                 codeAsString = codeAsString.substring(1);
538             }
539         }
540         return (sign * Integer.parseInt(codeAsString, characterCodeRadix));
541     }
542 
543 }