MonospaceTest.java

The MonospaceTest.java program can query for the aspect ratio values of the different fonts installed on a system and perform some computations based on these values.

Run the following program:

import java.awt.GraphicsEnvironment;
import java.awt.Font;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import java.util.Vector;
import java.util.Hashtable;
import java.util.Enumeration;

/*
   Tips for using monospaced fonts for compatibility reports

   1 Specifying the printable area

     The printable area is specified using the API functions fgl_report_configurePageSize(pageWidth,pageHeight) 
     and fgl_report_setPageMargins(topMargin ,bottomMargin ,leftMargin ,rightMargin).
     The printable area is the space within the margins.
   
   2 Description of the general mechanism used to render compatibility reports

   2.2 Font size computation

     Given a monospaced font (e.g. Courier), a printable area (e.g. 210mm-20mm x 297mm-20mm) and a ASCII character layout (e.g. 69 lines x 80 characters),
     the layouter will select the largest possible font size so that no character will be printed outside the printable area. The font size
     is an integer value so that the font might be slightly smaller than actually necessary. This may produce unwanted margins as explained below.

   2.3 The need to specifying "RIGHT MARGIN" or call the API function fgl_report_configureCompatibilityOutput() instead

     As explained in section 2.2, one of the factors that influence the font size computation is the number of characters that will be displayed in the 
     horizontal. A higher number of characters (e.g. 132) per line will cause a smaller font to be chosen (e.g. 10 point) while smaller numbers
     (e.g. 80) may yield a larger font size (e.g. 12 point).
     In a 4GL REPORT this is specified by the "RIGHT MARGIN" parameter in the OUTPUT section. Unfortunately this parameter is optional so
     that many existing 4GL reports are coded without specifying this parameter. In this case the graphical output will assume 80 characters
     per line so that the font may be too small for reports that require less width. For reports that print beyond the 80th column the layouter
     will render the page using the font computed for 80 characters and then scale the result to prevent clipping.
     Since both effects are undesirable, care should be taken to specify the value correctly.
     In cases where the "RIGHT MARGIN" specification would have to be added to a large number of REPORT objects, it is possible to call the API function 
     fgl_report_configureCompatibilityOutput(pageWidthInCharacters,fontName,fidelity,reportName, reportCategory,systemId) instead.
     The parameter "pageWidthInCharacters" allows to overrides the value of "RIGHT MARGIN" specified in the REPORT.

   2.3 Text placement within the printable area
     The text output is printed at the upper left corner of the printable area so that the character at line 1, column 1 will appear in the upper left
     corner. The text can be shifted to the right and downward using the function fgl_report_setPageMargins() as mentioned in section 1.

   2.3.1 Handling of character margin definitions in the ASCII REPORT.
     Left character margin definition "LEFT MARGIN cols" and top margin definitions "TOP MARGIN lines" in the "OUTPUT" section of the report are ignored 
     in the graphical output. Margin values should be specified using the API function fgl_report_setPageMargins() as explained in section 1.
     This solution gives finer control on the placement of the text on the paper and in addition it allows changing the margin values at runtime.
     The downside is that for existing reports where the exact text placement is required (e.g. placement of an address within an envelope window), 
     adjustments will be necessary. As discussed below, there are a number of other issues that make it unlikely that printing with the default settings 
     will produce output that matches the original to the extent that individual character are placed at exactly the same locations.
     Ideally, the most remote character (e.g. the character at line 69, column 80) is printed in the bottom right corner  

   3 Unwanted margins
     Depending on the dimensions of the printable area and the number of lines and columns to be printed one may experience additional margins
     at the right and/or at the bottom of the page.
   3.1 Source of the problem
   3.1.1 Mismatch of aspect ratio
     Looking at several different monospaced fonts displaying at the same height one will find that more often than not, the character width
     differs between the fonts. As an example consider the font "Nimbus Mono" whose characters have a ratio of 3/5 while "Courier" has an aspect ratio of 
     0.425, "Courier New" a ratio of 0.53 and "Courier 10 Pitch" a ratio of 0.602.
     Fonts with lower values will visually appear taller and more condensed than fonts with higher values.
     Besides aesthetic aspects, the ratio has consequences for the margins that appear when the ratio of the font and the ratio of the printable area
     do not match well.
     In order to illustrate the problem consider the hypothetical case that we wanted to print a report that has 10 lines by 10 columns 
     using the font "Nimbus Mono" onto a page in the format "letter" (11" x 8.5") with 0.5" margins on each side. In this case the printable area has
     the dimensions 10" x 7.5". Choosing a font size that produces exactly 10 lines (about 72 point) we will find that there is a 1.5" margin between
     the rightmost column and the right edge of the printable area. The reason for this gap is, that the font "Nimbus Mono" has an aspect ratio 
     of 3/5 so that a rectangle of 10 by 10 that is 10" high will appear 6" wide leaving a 1.5" gap to the horizontally available 7.5".
     Had we chosen 8 lines by 10 columns then there would have been no gap at all. For this particular page layout and font we can find other optimal
     layouts for all integer solutions of the equation cols=lines*5/4.
     All optimal character layouts between 1 and 100 lines are:

     Lines    Columns
        4        5
        8       10
       12       15
       16       20
       20       25
       24       30
       28       35
       32       40
       36       45
       40       50
       44       55
       48       60
       52       65
       56       70
       60       75
       64       80
       68       85
       72       90
       76       95
       80      100
       84      105
       88      110
       92      115
       96      120
      100      125


     The formula can be derived as follows:
         1) characterWidth=7.5"/cols
         2) characterHeight=10"/lines
         3) characterHeight=characterWidth*5/3
         
         4) 3 in 2:
             characterWidth*5/3=10"/lines
             characterWidth=(10"*3)/(lines*5)
         5) 1 in 4:
             7.5"/cols=(10"*3)/(lines*5)
             cols=lines*5/4


     Given a printable area of 11" by 8.5" without margins and a common character layout of 69 lines by 80 columns a font needs to have an aspect ratio
     of 0.666 to not produce any margins as the following calculation shows:

         1) characterWidth=8.5"/80
         2) characterHeight=11"/69
         3) characterHeight=characterWidth/ratio

         4) 3 in 2:
             characterWidth/ratio=11"/69

         5) 1 in 4:
             8.5"/80/ratio=11"/69
             ratio=0.666

     For the paper format ISO A4 the aspect ratio would have to be 0.61 as computed as follows:

         1) pageHeight=sqrt(sqrt(2))/4 // A4 page height in meter
         2) pageWidth=pageHeight/sqrt(2)=1/(sqrt(sqrt(2))*4) // width=height/sqrt(2) for all ISO formats
         3) characterWidth=pageWidth/80=1/(sqrt(sqrt(2))*320)
         4) characterHeight=pageHeight/69=sqrt(sqrt(2))/276
         5) characterHeight=characterWidth/ratio

         6) 5 in 4:
             characterWidth/ratio=sqrt(sqrt(2))/276

         5) 3 in 6:
             1/(sqrt(sqrt(2))*320*ratio)=sqrt(sqrt(2))/276
             ratio=1/(sqrt(2)/276*320)
             ratio=0.61

     With a font that has an aspect ratio of 1/2 (0.5) such as "MS Gothic" one can produce the layout of a line printer in 8 LPI mode.
     The paper format is "letter" (8.5" x 11") with 0.5" margins on all sides and the character layout is 88 lines by 132 columns.

         
         Computing the optimal ratio for a printable area of 7.5" x 10" for a character layout of 88 lines by 132 columns:

         1) characterWidth=8.5"-1"/132 = 7.5"/132
         2) characterHeight=11"-1"/88 = 10"/88
         3) characterHeight=characterWidth/ratio

         4) 3 in 2:
             characterWidth/ratio=10"/88

         5) 1 in 4:
             7.5"/132/ratio=10"/88
             ratio=660/1320
             ratio=0.5
     

     As another  example consider a font that has an aspect ratio of 3/5 (0.6) such as "Nimbus Mono". For this font, one can produce an optimal output for 
     88 lines by 110 columns on the same printable area as shown in the table above.

   3.1.2.1 Measurements of historic line printer formats

     Line printers typically printed at the following formats:

        Character layout              Page size     Font size         Pitch       Character ratio
     1) 66 lines by  80 characters    11" by 8.5"   6 LPI (12 point)  9.41 CPI       0.6375
     2) 88 lines by  80 characters    11" by 8.5"   8 LPI (9  point)  9.41 CPI       0.85
     3) 66 lines by 132 characters    11" by 14"    6 LPI (12 point)  9.43 CPI       0.636
     4) 88 lines by 132 characters    11" by 14"    8 LPI (9  point)  9.43 CPI       0.85

     The CPI values in the table are computed from the page measurements. It is not clear if these values were used or if the rounded
     value "10 pitch" was used. If we assume that the rounded values were used, then 80 characters require only 8" leaving
     a right margin of 0.5". The table of measurements is then:

        Character layout              Page size     Font size         Pitch       Character ratio
     1) 66 lines by  80 characters    11" by 8.5"   6 LPI (12 point)  10 CPI         0.6
     2) 88 lines by  80 characters    11" by 8.5"   8 LPI (9  point)  10 CPI         0.8
     3) 66 lines by 132 characters    11" by 14"    6 LPI (12 point)  10 CPI         0.6
     4) 88 lines by 132 characters    11" by 14"    8 LPI (9  point)  10 CPI         0.8

    It is interesting to observe that option 1 from the table above also fitted well on A4 portrait which has the format 11.7" by 8.27"
    leaving a margin of 0.27" on the right and 0.7" at the bottom.

   3.1.2 Margins due to integer rounding

     Choosing a font with a matching aspect ratio does not guarantee small margins because the layouter only accepts integer values as a point
     size for font selection so that the "perfect" font size has to be rounded down to the nearest integer value.
     As an example consider using the font "MS Gothic" with an aspect ratio of 0.5 which fits perfectly on the format 8.5" x 11" with 0.5" margins on 
     all sides and a character layout 88 x 132. As shown in 3.1.1 this font produces no margins at all for this layout. However, the required font size
     is 8.2125 point which has to be rounded down to 8 point. Using this font produces a gap on the right side of 4.9mm and a gap at the bottom of 6.6mm.
     Using the font "L M Typewriter12 Regular" which has a less optimal aspect ratio of 0.514 produces a gap at the bottom of 6.9mm at its best point size 
     of 8.1 point. However, since the rounding difference between 8.1 and 8 is smaller than with the optimal font, the total margins after rounding
     are actually smaller than those using the optimal font. For this font the margins at 8 point are 1.3mm at the bottom and 8.6mm on the right.

  4 The program "MonospaceTest"
    The program can be used to query for the aspect ratio values of the different fonts installed on a system and it performes some computations
    based on the values.  The program operates in two modes depending on the command line arguments as explained below:
  4.1 List Mode
    By invoking the program without command line arguments, it examines all monospaced fonts on the system and lists them grouped by their specific aspect 
    ratio. For each font group it lists ideal layouts for various common page formats and margin values.
  4.2 Search mode.
    By invoking the program with four arguments "heightOfPrintableAreaInMM", "widthOfPrintableAreaInMM", "lines" and "cols" it will compute
    which font produces the smallest margins.
  4.3 Compiling the program
    Type "javac MonospaceTest.java" to compile the program.
    This will create the following files in the current directory:
    $ls -l *.class
    -rw-r--r-- 1 alex alex       341 2010-08-17 12:24 MonospaceTest$Value.class
    -rw-r--r-- 1 alex alex       654 2010-08-17 12:24 MonospaceTest$MMValue.class
    -rw-r--r-- 1 alex alex       686 2010-08-17 12:24 MonospaceTest$InchValue.class
    -rw-r--r-- 1 alex alex       726 2010-08-17 12:24 MonospaceTest$Entry.class
    -rw-r--r-- 1 alex alex      6944 2010-08-17 12:24 MonospaceTest.class

  4.4 Running the program
    Type "java MonospaceTest [args]" in the directory containing the compiled "class" files to run the program.
*/



class MonospaceTest
{
    final static double A4HeightInMM=Math.sqrt(Math.sqrt(2))/4*1000;
    final static double A4WidthInMM=A4HeightInMM/Math.sqrt(2);
    final static double LetterHeightInMM=11*2.54*10;
    final static double LetterWidthInMM=8.5*2.54*10;
    final static double FourteenInchInMM=14*2.54*10;
    static void usage()
    {
        System.err.println("Usage: java MonospaceTest|java MonospaceTest heightOfPrintableAreaInMM widthOfPrintableAreaInMM lines cols");
        System.exit(1);
    }
    public static void main(String[] args) throws Exception
    {
        if(args.length==4)
        {
            double printableHeightInMM;
            double printableWidthInMM;
            int lines;
            int cols;
            printableHeightInMM=Double.parseDouble(args[0]);
            printableWidthInMM=Double.parseDouble(args[1]);
            lines=Integer.parseInt(args[2]);
            cols=Integer.parseInt(args[3]);
            findBestFont(printableWidthInMM,printableHeightInMM,cols,lines);
        }
        else
        if(args.length==0)
        {
            listAllOptions();
        }
        else
        {
            usage();
        }
    }
    static void findBestFont(double printableWidthInMM,double printableHeightInMM,int cols,int lines)
    {
        System.out.println("Fonts suitable for "+lines+" x "+cols+" characters on a printable area of "+(printableHeightInMM/10)+"cm by "+(printableWidthInMM/10)+"cm (aspect ratio="+(printableWidthInMM/printableHeightInMM)+")\n");
        GraphicsEnvironment ge=GraphicsEnvironment.getLocalGraphicsEnvironment();
        FontRenderContext frc=new FontRenderContext(null,false,true);
        Font[] fonts=ge.getAllFonts();
        double pageAspectRatio=printableWidthInMM/printableHeightInMM;
        boolean[] isMonospaced=new boolean[fonts.length];
        double[] gapOnTheRight=new double[fonts.length];
        double[] gapAtTheBottom=new double[fonts.length];
        double[] gapOnTheRightI=new double[fonts.length];
        double[] gapAtTheBottomI=new double[fonts.length];
        double[] fontAspectRatios=new double[fonts.length];
        double[] fractionalPointSize=new double[fonts.length];
        for(int i=0;i<fonts.length;i++)
        {
            Font f=fonts[i];
            if(isMonospaced(f,frc))
            {
                isMonospaced[i]=true;
                Rectangle2D characterRect=getCharacterDimensions(f,frc);
                fontAspectRatios[i]=getAspectRatio(characterRect);
                double textHeight=lines;
                double textWidth=cols*fontAspectRatios[i];
                double textAspectRatio=textWidth/textHeight;
                if(textAspectRatio<pageAspectRatio) // Page is wider than text -> gap on the right
                {
                    gapOnTheRight[i]=printableWidthInMM-printableHeightInMM*textAspectRatio;
                    gapAtTheBottom[i]=0;
                    double numberOfLines=printableHeightInMM/10/2.54*72.0/characterRect.getHeight();
                    double fontSize=f.getSize2D();
                    fractionalPointSize[i]=fontSize*numberOfLines/lines;
                }
                else // Page is higher than text -> gap at the bottom
                {
                    gapOnTheRight[i]=0;
                    gapAtTheBottom[i]=printableHeightInMM-printableWidthInMM/textAspectRatio;
                    double numberOfCols=printableWidthInMM/10/2.54*72.0/characterRect.getWidth();
                    double fontSize=f.getSize2D();
                    fractionalPointSize[i]=fontSize*numberOfCols/cols;
                }
                double integerPointSizeFactor=Math.floor(fractionalPointSize[i])/fractionalPointSize[i];
                gapOnTheRightI[i]=(printableWidthInMM-gapOnTheRight[i])*(1-integerPointSizeFactor)+gapOnTheRight[i];
                gapAtTheBottomI[i]=(printableHeightInMM-gapAtTheBottom[i])*(1-integerPointSizeFactor)+gapAtTheBottom[i];
            }
        }
        for(int i=0;i<fonts.length;i++)
        {
            double smallestGapArea=Double.MAX_VALUE;
            int smallestIndex=-1;
            for(int j=0;j<fonts.length;j++)
            {
                if(fonts[j]!=null&&isMonospaced[j])
                {
                    double gapArea=gapOnTheRightI[j]*printableHeightInMM+gapAtTheBottomI[j]*printableWidthInMM;
                    if(gapArea<smallestGapArea) 
                    {
                        smallestIndex=j;
                        smallestGapArea=gapArea;
                    }
                }
            }
            if(smallestIndex>=0)
            {
                System.out.println("Font \""+fonts[smallestIndex].getFontName()+"\" with a character aspect ratio of "+fontAspectRatios[smallestIndex]+" at size "+fractionalPointSize[smallestIndex]+"point, produces a gap "+(gapOnTheRight[smallestIndex]>0?"on the right":"at the bottom")+" of "+(gapOnTheRight[smallestIndex]+gapAtTheBottom[smallestIndex])+"mm");
                System.out.println("Using an integer point size of "+(int)Math.floor(fractionalPointSize[smallestIndex])+" will produce a gap on the right of "+gapOnTheRightI[smallestIndex]+"mm and a gap at the bottom of "+gapAtTheBottomI[smallestIndex]+"mm.\n");
                fonts[smallestIndex]=null;
            }
        }
    }
    static void listAllOptions()
    {
        GraphicsEnvironment ge=GraphicsEnvironment.getLocalGraphicsEnvironment();
        FontRenderContext frc=new FontRenderContext(null,false,true);
        Font[] fonts=ge.getAllFonts();
        Hashtable<Double,Entry> entries=new Hashtable<Double,Entry>();
        for(int i=0;i<fonts.length;i++)
        {
            Font f=fonts[i];
            if(isMonospaced(f,frc))
            {
                Double fontAspectRatio=new Double(getAspectRatio(f,frc));
                Entry e=entries.get(fontAspectRatio);
                if(e==null)
                    entries.put(fontAspectRatio,new Entry(fontAspectRatio.doubleValue(),f));
                else
                    e.getFonts().add(f);
            }
        }
        for (Enumeration<Entry> e = entries.elements(); e.hasMoreElements();)
        {
            Entry entry=e.nextElement();
            double fontAspectRatio=entry.getAspectRatio();
            System.out.println("Aspect ratio="+fontAspectRatio+" found in fonts:");
            Vector<Font> fontList=entry.getFonts();
            for(int i=0;i<fontList.size();i++)
                System.out.println(fontList.get(i).getFontName());
            System.out.println("Fitting options:");
            
            String[] paperNames=new String[] {"letter","11\" by 14\"","A4"};
            double[] paperHeights=new double[] {LetterHeightInMM,FourteenInchInMM,A4HeightInMM};
            double[] paperWidths=new double[] {LetterWidthInMM,LetterHeightInMM,A4WidthInMM};
            Value[] margins=new Value[] {new InchValue(0),
                                         new InchValue(0.5),
                                         new InchValue(1),
                                         new InchValue(1.5),
                                         new InchValue(2),
                                         new MMValue(10),
                                         new MMValue(20),
                                         new MMValue(30),
                                         new MMValue(40),
                                         new MMValue(50)};
            
            for(int portrait=0;portrait<2;portrait++)
            {
                boolean isPortrait=portrait==0;
                for(int type=0;type<paperNames.length;type++)
                {
                    for(int m=0;m<margins.length;m++)
                    {
                        double marginValue=margins[m].getMMValue();
                        double pageWidth=(isPortrait?paperWidths[type]:paperHeights[type])-marginValue;
                        double pageHeight=(isPortrait?paperHeights[type]:paperWidths[type])-marginValue;
                        double pageAspectRatio=pageWidth/pageHeight;
                        for(int lines=1;lines<=132;lines++)
                        {
                            for(int cols=1;cols<=132;cols++)
                            {
                                double width=cols*fontAspectRatio;
                                double height=lines;
                                if(Math.abs(width/height-pageAspectRatio)<0.0001)
                                {
                                    System.out.println("Solution for "+paperNames[type]+(isPortrait?" portrait":" landscape")+" with margins of "+margins[m]+": lines="+lines+", cols="+cols);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    static boolean isMonospaced(Font f,FontRenderContext frc)
    {
        GlyphVector v1=f.createGlyphVector(frc,"ii");
        GlyphVector v2=f.createGlyphVector(frc,"WW");
        return v1.getLogicalBounds().equals(v2.getLogicalBounds())&&!v1.equals(v2);
    }
    static Rectangle2D getCharacterDimensions(Font f,FontRenderContext frc)
    {
        //GlyphVector v=f.createGlyphVector(frc,"W");
        //return v.getLogicalBounds();
        return f.getStringBounds("W",frc);
    }
    static double getAspectRatio(Rectangle2D characterRect)
    {
        return characterRect.getWidth()/characterRect.getHeight();
    }
    static double getAspectRatio(Font f,FontRenderContext frc)
    {
        return getAspectRatio(getCharacterDimensions(f,frc));
    }
    static abstract class Value
    {
        double value;
        public Value(double value)
        {
            this.value=value;
        }
        abstract double getMMValue();
    }
    static class InchValue extends Value
    {
        public InchValue(double inchValue)
        {
            super(inchValue);
        }
        double getMMValue()
        {
            return value*2.54*10;
        }
        public String toString()
        {
            return ""+value+"inch";
        }
    }
    static class MMValue extends Value
    {
        public MMValue(double mmValue)
        {
            super(mmValue);
        }
        double getMMValue()
        {
            return value;
        }
        public String toString()
        {
            return ""+value+"mm";
        }
    }
    static class Entry
    {
        double aspectRatio;
        Vector<Font> fonts=new Vector<Font>();
        public Entry(double aspectRatio,Font font)
        {
            this.aspectRatio=aspectRatio;
            fonts.add(font);
        }
        public double getAspectRatio()
        {
            return aspectRatio;
        }
        public Vector<Font> getFonts()
        {
            return fonts;
        }
    }
}