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;
}
}
}