/********************************************************************************* * TotalCross Software Development Kit * * Copyright (C) 2000-2012 SuperWaba Ltda. * * All Rights Reserved * * * * This library and virtual machine is distributed in the hope that it will * * be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * * * *********************************************************************************/ package tc.tools; import totalcross.io.*; import totalcross.sys.*; import totalcross.ui.*; import totalcross.ui.event.*; import totalcross.ui.font.*; import totalcross.util.*; import totalcross.util.zip.*; /** Converts a Windows true type font to a pdb file that can be used by TotalCross programs * (and also by other programs) * Must be compiled with JDK 1.2.2 or above. */ public class FontGenerator { public static byte detailed=0; // 1 show messages, 2 show msgs + chars class Range { int s, e; String name; Range(int ss, int ee, String nn) { s = ss; e = ee; name=nn; } } String fontName; PalmFont pf; int antialiased; Vector newRanges; java.awt.Component comp; static IntVector sizes = new IntVector(30); boolean skipBigChars; int[] fontsizes = {7,8,9,10,11,12,13,14,15,16,17,18,19,20,40,60,80}; public FontGenerator(String fontName, String []extraArgs) throws Exception { int i; String sizesArg=null; comp = new java.awt.Frame(); comp.addNotify(); newRanges = new Vector(); newRanges.addElement(new Range(32, 255, "u0")); // default range String jdkversion = System.getProperty("java.version"); if (jdkversion.startsWith("1.1.") || jdkversion.startsWith("1.2.")) throw new Exception("This program requires JDK version greater or equal than 1.3!"); String outName = fontName; // guich@401_11 boolean noBold = false; boolean isMono = false; for (i=1; i < extraArgs.length; i++) // 0 if font name if (extraArgs[i] != null) { String arg = extraArgs[i]; String argLow = arg.toLowerCase(); if (argLow.indexOf("monospace") >= 0) isMono = true; else if (argLow.equals("/skipbigchars")) skipBigChars = true; else if (argLow.equals("/nobold")) noBold = true; else if (argLow.startsWith("/sizes")) sizesArg = argLow.substring(argLow.indexOf(':')+1); else if (argLow.equals("/aa")) antialiased = AA_8BPP; // always force 8bpp else if (argLow.equals("/aa8")) antialiased = AA_8BPP; else if (argLow.startsWith("/rename:")) { outName = arg.substring(8); if (outName.indexOf('.') >= 0) // guich@tc123_11 throw new Exception("Invalid rename parameter: "+outName); } else if (argLow.startsWith("/detailed:")) { detailed = (byte)(arg.charAt(10)-'0'); if (detailed != 1 && detailed != 2) { println("Invalid detailed value. Resetting to 0"); detailed = 0; } } else if (argLow.startsWith("/u")) { String[] ranges = new String[extraArgs.length-i-1]; for (int k =0; ++i < extraArgs.length;) if ((ranges[k++] = extraArgs[i]).indexOf('-') < 0) throw new Exception("Invalid range '"+extraArgs[i]+"' is not in the format 'start-end'"); processRanges(ranges); } else throw new Exception("Invalid argument: "+extraArgs[i]); } totalcross.sys.Settings.showDesktopMessages = false; // parse parameters if (fontName.indexOf('_') != -1) // guich@421_26: lets the user pass fonts with spaces fontName = fontName.replace('_',' '); this.fontName = fontName; String realName; if (!(realName=new java.awt.Font(fontName, java.awt.Font.PLAIN, 14).getFontName()).toLowerCase().startsWith(fontName.toLowerCase())) { println("Font "+fontName+" not found. Was replaced by "+realName+". Run the program without arguments to see the list of possible fonts."); System.exit(1); } // create fonts println("FontGenerator - Copyright (c) SuperWaba 2002-2012. Processing..."); if (sizesArg != null) { String[] ss = totalcross.sys.Convert.tokenizeString(sizesArg, ','); for (i =0; i < ss.length; i++) sizes.addElement(totalcross.sys.Convert.toInt(ss[i])); sizes.qsort(); } else for (i = 0; i < fontsizes.length; i++) sizes.addElement(fontsizes[i]); Vector v = new Vector(30); for (i = 0; i < sizes.size(); i++) { int s = sizes.items[i]; convertFont(v, new java.awt.Font(fontName, java.awt.Font.PLAIN, s), outName+"$p"+s, newRanges, isMono); if (!noBold) convertFont(v, new java.awt.Font(fontName, java.awt.Font.BOLD, s), outName+"$b"+s, newRanges, isMono); } // write the file try {new File(outName).delete();} catch (Exception e) {} // delete if it exists new TCZ(v, outName, (short)0); // test fonts runTestFont(outName); } private void convertFont(Vector v, java.awt.Font f, String fileName, Vector newRanges, boolean isMono) { java.awt.FontMetrics fm = comp.getFontMetrics(f); int width = fm.charWidth('@')*4; int height = fm.getHeight()*4; java.awt.Image img = comp.createImage(width,height); java.awt.Graphics2D g = (java.awt.Graphics2D)img.getGraphics(); try {g.setRenderingHint(java.awt.RenderingHints.KEY_TEXT_ANTIALIASING, antialiased != AA_NO ? java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_ON:java.awt.RenderingHints.VALUE_TEXT_ANTIALIAS_OFF);} catch (Throwable t) {println("Antialiased font not supported!"); System.exit(2);} g.setFont(f); // Note: Sun's JDK does not return a optimal width value for the // characters, notabily chars W,V,T,A. Because of this, we need to // find the width ourselves. // Note2: due to this, monospaced fonts may not be converted properly int widths[] = new int[65536]; byte gaps[] = new byte[65536]; short sum = 0; int totalBits=0; int maxW = 0; int maxH = 0; int i; int wW=0; // first, we have to compute the sizes for all ranges if (detailed == 0) System.out.println("Computing bounds for "+f); int x = width>>2; int n = newRanges.size(); char back = 8; String backs = ""+back+back+back+back+back+back; for (int ri = 0; ri < n; ri++) { Range rr = (Range)newRanges.items[ri]; if (detailed == 0) System.out.print(backs+((ri+1)*100/n)+"%"); int ini = rr.s; int end = rr.e; for (i = ini; i <= end; i++) { int ch = i; g.setColor(java.awt.Color.white); g.fillRect(0,0,width,height); g.setColor(java.awt.Color.black); if (ch > 255 && !f.canDisplay((char)ch)) // guich@tc115_69 { System.out.println("Warning! The true type font cannot display the character number "+i+". Character will be replaced by space."); ch = ' '; } g.drawString(totalcross.sys.Convert.toString((char)ch), x, height>>2); totalcross.ui.gfx.Rect r = computeBounds(getPixels(img, width,height), width); if (r == null) // blank char? { if (detailed==1 && i == ch) println("char "+ch+" is blank"); widths[i] = fm.charWidth(ch); } else { int w = r.width+1; // +1 for interchar spacing - guich@560_15: use java's if monospaced font // guich@tc126_44: skip chars above normal if (wW == 0 && ch == 'W') wW = w; else if (wW > 0 && w > wW) { println("Skipped char "+ch+" "+(char)ch+": "+w); continue; } int h = r.height; int gap = x - r.x; maxW = Math.max(maxW,w); maxH = Math.max(maxH,h); gaps[i] = (byte)gap;//<=0 ? 1 : 0; // most chars Java puts 1 pixel away; some chars Java puts one pixel near; for those chars, we make them one pixel right widths[i] = w; if (detailed==1) println("Width of "+(char)ch+" = "+widths[i]+" - gap: "+gap); } } } if (isMono) // guich@tc113_31 { for (i = 0; i < widths.length; i++) widths[i] = maxW; println("Setting all widths to "+maxW); } maxH = fm.getHeight(); int nullCount = 0; if (detailed == 0) System.out.println(backs+"Saving glyphs"); // now, for each range, compute the totalbits and the chars for (int ri = 0; ri < n; ri++) { Range rr = (Range)newRanges.items[ri]; int ini = rr.s; int end = rr.e; totalBits = 0; for (i = ini; i <= end; i++) totalBits += widths[i]; // Create the PalmFont and set its parameters println((ri+1)+" of "+n+". Original height: "+fm.getHeight()+", ascent: "+fm.getMaxAscent()+", descent: "+fm.getMaxDescent()+", leading: "+fm.getLeading()); pf = new PalmFont(fileName+rr.name); pf.antialiased = antialiased; pf.firstChar = ini; pf.lastChar = end; pf.spaceWidth = isMono ? maxW : fm.charWidth(' '); // guich@tc115_87 pf.maxWidth = maxW; pf.maxHeight = fm.getHeight(); pf.descent = fm.getMaxDescent(); pf.ascent = (maxH-pf.descent); pf.rowWords = ((totalBits+15) / 16); pf.rowWords = (((int)(pf.rowWords+1)/2)*2); // guich@400_67 pf.owTLoc = (pf.rowWords*pf.maxHeight+(pf.lastChar-pf.firstChar)+8); pf.debugParams(); if (detailed>=1) println("totalBits: "+totalBits+", rowWords: "+pf.rowWords); pf.initTables(); // fill in PalmFont tables sum = 0; for (i =ini; i <= end; i++) { pf.bitIndexTable[i-ini] = sum; sum += widths[i]; } pf.bitIndexTable[i-ini] = sum; i++; // draw the chars in the image and decode that image sum = 0; for (i = ini; i <= end; i++) { int ww = widths[i]; g.setColor(java.awt.Color.white); g.fillRect(0,0,width,height); g.setColor(java.awt.Color.black); g.drawString(totalcross.sys.Convert.toString((char)i), gaps[i], pf.ascent); int[] pixels = getPixels(img, ww,maxH); if (pixels != null) // guich@tc110_5 computeBits(pixels, sum, ww); else nullCount++; sum += ww; } // save the image v.addElement(pf.save()); } if (nullCount > 0) System.out.println("A total of "+nullCount+" characters had no glyphs"); } private totalcross.ui.gfx.Rect computeBounds(int pixels[], int w) { int white = -1; int x=0,y=0; int xmin=10000, ymin=10000, xmax=-1, ymax=-1; for (int i = 0; i < pixels.length; i++) { if (pixels[i] != white) { xmin = Math.min(xmin,x); xmax = Math.max(xmax,x); ymin = Math.min(ymin,y); ymax = Math.max(ymax,y); } if (++x == w) { x = 0; y++; } } if (xmin == 10000) // empty char? return null; return new totalcross.ui.gfx.Rect(xmin,ymin,xmax-xmin+1,ymax-ymin+1); } int bits[] = {128,64,32,16,8,4,2,1}; private void setBit(int x, int y) { pf.bitmapTable[(x>>3) + y * pf.rowWidthInBytes] |= bits[x % 8]; // set if (detailed==2) System.out.print((char)('@')); } private void setNibble4(int pixel, int x, int y) { int nibble = 0xF0 - (pixel & 0xF0); // 4 bits of transparency if (detailed==2) System.out.print((char)('a'+nibble)/*'@'*/); if ((x & 1) != 0) nibble >>= 4; pf.bitmapTable[(x>>1) + (y * pf.rowWidthInBytes)] |= nibble; // set } private void setNibble8(int pixel, int x, int y) { int nibble = 0xFF - (pixel & 0xFF); // FFF5F5F5 if (detailed==2) System.out.print((char)('a'+nibble)/*'@'*/); pf.bitmapTable[x + (y * pf.rowWidthInBytes)] = (byte)nibble; // set } private void computeBits(int pixels[], int xx, int w) { int white = -1; int x=0,y=0; for (int i = 0; i < pixels.length; i++) { if (pixels[i] != white) { switch (antialiased) { case AA_NO: setBit(xx+x,y); break; case AA_4BPP: setNibble4(pixels[i], xx+x, y); break; case AA_8BPP: setNibble8(pixels[i], xx+x, y); //AndroidUtils.debug("@"+(xx+x)+","+y+": "+Integer.toHexString(pixels[i]).toUpperCase()+" -> "+Integer.toHexString(pf.bitmapTable[xx+x + (y * pf.rowWidthInBytes)] & 0xFF).toUpperCase()); break; } } else if (detailed==2) System.out.print(' '); if (++x == w) { x = 0; y++; if (detailed==2) println(""); } } } /* chi: 32-127 160-383 402-402 506-511 12288-12291 12296-12309 19968-40869 65281-65374 65504-65510 Jap: 32-127 160-383 402-402 506-511 12288-12291 12296-12309 12353-12435 12449-12534 65281-65374 65504-65510 Kor: 32-127 160-383 402-402 506-511 12288-12291 12296-12309 44032-55203 65281-65374 65504-65510 Internal ranges are stored as 0-255, 256-511, 512-767, ... So, 160-383 is splitted into 160-255, 256-383 */ private void processRanges(String[] ranges) { int n = ranges.length,s,e=0,first,last; newRanges = new Vector(); // first we set all bits defined in the given ranges IntVector iv = new IntVector(); iv.ensureBit(65535); iv.setBit(' ',true); // MUST include the space in the range for (int i =0; i < n; i++) { String r = ranges[i]; String s1 = r.substring(0,r.indexOf('-')); String s2 = r.substring(r.indexOf('-')+1); s = Integer.parseInt(s1); e = Integer.parseInt(s2); while (s <= e) iv.setBit(s++,true); } int max = e/256+1; // now we create the ranges in 256-char groups. for (int i =0; i < max; i++) { s = i * 256; e = (i+1)*256-1; first = last = -1; // find the first and the last bit set in the range for (int j = s; j <= e; j++) if (iv.isBitSet(j)) { if (first == -1) first = j; last = j; } if (first != -1) { Range rr = new Range(first, last, "u"+s); newRanges.addElement(rr); println("Unicode range "+rr.name+": "+rr.s+" - "+rr.e); } } } public static int[] getPixels(java.awt.Image img, int width, int height) { java.awt.image.PixelGrabber pg = new java.awt.image.PixelGrabber(img,0,0,width,height,true); try {pg.grabPixels();} catch (InterruptedException ie) {System.out.println("interrupted!");} return (int [])pg.getPixels(); } static public void println(String s) { System.out.println(s); } public static void runTestFont(String fontName) { if (true) { totalcross.Launcher.main(new String[] { "/scr", "800x600x24", "/scale", "1", "/cmdLine", fontName, "tc.tools.FontGenerator$TestFont" }); // guich@503_6: fixed package name while (true) totalcross.sys.Vm.sleep(100); } else System.exit(0); } public static void main(String args[]) { try { java.awt.Font fonts[] = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts(); if (args.length < 1) { println("Format: java FontGenerator <font name> /rename:newName /detailed:1_or_2 /aa"); println(" /NoBold /sizes:<comma-separeted list of sizes> /u <list of ranges>"); println(""); println("Parameters are case insensitive, meaning:"); println(". /monospace To create a monospaced font."); println(". /rename:newName to rename the output font name."); println(". /detailed:1_or_2 to show detailed information."); println(". /aa to create a 4-bpp antialiased font."); println(". /aa8 to create a 8-bpp antialiased font."); println(" /sizes:<comma-separeted list of sizes> to create a font with the given sizes"); println(". /NoBold to don't create the bold font."); println(". /skipBigChars: useful when creating monospaced fonts; the glyphs that have a width above the W char are skipped."); println(". /u to create unicode chars in the range. By default, we create chars in the"); println("range 32-255. Using this option, you can pass ranges in the form"); println("\"start0-end0 start1-end1 start2-end2 ...\", which creates a file containing the"); println("characters ranging from \"startN <= c <= endN\". For example:"); println("\"/u 32-255 256-383 402-402 20284-40869\". The ranges must be in increased order."); println("The /u option must be the LAST option used."); println(""); println("When creating unicode fonts of a wide range, using options /nobold"); println("will create a file 1/4 of the original size."); println(""); println("Alternative format: java FontGenerator test <font name>"); println("This will open a TotalCross app to test the font"); println(""); println("Copyright (c) SuperWaba 2002-2012"); println("Must use JDK 1.3 or higher!"); println("\nPress enter to list the available fonts, or q+enter to stop."); try { if (Character.toLowerCase((char)System.in.read()) != 'q') for (int i =0 ; i < fonts.length; i++) println(" "+fonts[i].getName()); } catch (java.io.IOException ie) {} } else if (args[0].equalsIgnoreCase("test")) runTestFont(args[1]); else new FontGenerator(args[0],args); } catch (Exception e) { e.printStackTrace(); } } static final int AA_NO = 0; static final int AA_4BPP = 1; static final int AA_8BPP = 2; static class PalmFont { public int antialiased; // true if its antialiased public int firstChar; // ASCII code of first character public int lastChar; // ASCII code of last character public int spaceWidth; // width of the space char public int maxWidth; // width of font rectangle public int maxHeight; // height of font rectangle public int owTLoc; // offset to offset/width table public int ascent; // ascent public int descent; // descent public int rowWords; // row width of bit image / 2 public int rowWidthInBytes; public int bitmapTableSize; public byte []bitmapTable; public int []bitIndexTable; public String fileName; public TCZ.Entry save() { try { ByteArrayStream from = new ByteArrayStream(4096); ByteArrayStream to = new ByteArrayStream(2048); DataStreamLE ds = new DataStreamLE(from); ds.writeShort(antialiased); // note that writeShort already writes an unsigned short (its the same method) ds.writeShort(firstChar ); ds.writeShort(lastChar ); ds.writeShort(spaceWidth ); ds.writeShort(maxWidth ); ds.writeShort(maxHeight ); ds.writeShort(owTLoc ); ds.writeShort(ascent ); ds.writeShort(descent ); ds.writeShort(rowWords ); ds.writeBytes(bitmapTable); for (int i=0; i < bitIndexTable.length; i++) ds.writeShort(bitIndexTable[i]); // compress the font int f = from.getPos(),s; from.mark(); s = ZLib.deflate(from, to, 9); // write the name uncompressed before it println("Font "+fileName+" stored compressed ("+f+" -> "+s+")"); return new TCZ.Entry(to.toByteArray(), fileName.toLowerCase(),f); } catch (Exception e) {e.printStackTrace();} return null; } public PalmFont(String fileName) { this.fileName = fileName; } public void debugParams() { println("antialiased: "+antialiased); println("firstChar : "+firstChar ); println("lastChar : "+lastChar ); println("spaceWidth : "+spaceWidth ); println("maxWidth : "+maxWidth ); println("maxHeight : "+maxHeight ); println("owTLoc : "+owTLoc ); println("ascent : "+ascent ); println("descent : "+descent ); println("rowWords : "+rowWords ); } public void initTables() { rowWidthInBytes = 2 * rowWords * (antialiased == AA_NO ? 1 : antialiased == AA_4BPP ? 4 : 8); // 4 bits of transparency or 1 bit (B/W) bitmapTableSize = (int)rowWidthInBytes * (int)maxHeight; bitmapTable = new byte[bitmapTableSize]; bitIndexTable = new int[lastChar - firstChar + 1 + 1]; } public int charWidth(char ch) { int index = ch - firstChar; return index < 0 || index > lastChar ? spaceWidth : bitIndexTable[index+1] - bitIndexTable[index]; } } public static class TestFont extends MainWindow { private Button btn; public TestFont() { super("", TAB_ONLY_BORDER); setUIStyle(Settings.Vista); } public void initUI() { Label l; String fontName = getCommandLine(); setTitle("Test Font - "+fontName); if (fontName == null || fontName.length() == 0) exit(1); String tit = "__ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz12345678890`~!@#$%^&*()_+=-{}\\][:;\"'<,>./?�������"; if (sizes.size() == 0) // occurs when the user call the TestFont directly { for (int i = Font.MIN_FONT_SIZE; i <= Font.MAX_FONT_SIZE; i++) sizes.addElement(i); } for (int ii = 0; ii < sizes.size(); ii++) { int i = sizes.items[ii]; // how only the fonts that match this name. Font ff = Font.getFont(fontName, false, i); if (ff.name.equals(fontName)) { l = new Label(i+" "+tit); l.setFont(ff); add(l,LEFT,AFTER); } ff = Font.getFont(fontName, true, i); if (ff.name.equals(fontName)) { l = new Label(i+" "+tit); l.setFont(ff); add(l,LEFT,AFTER); } } add(btn = new Button("Exit"), RIGHT,0); } public void onEvent(Event e) { if (e.type == ControlEvent.PRESSED && e.target == btn) exit(0); } } }