package totalcross.android.fontgen; import totalcross.*; import java.io.*; import java.nio.*; import java.util.*; import android.graphics.*; import android.os.*; public class FontGenerator { final int MAX_FONT_SIZE=120; int[] fontsizes = {7,8,9,10,11,12,13,14,15,16,17,18,19,20,40,60,80}; static boolean generated; class Range { int s, e; String name; Range(int ss, int ee, String nn) { s = ss; e = ee; name=nn; } } public static byte detailed; // 1 show messages, 2 show msgs + chars String fontName; PalmFont pf; int antialiased; Vector<Range> newRanges; boolean skipBigChars; public FontGenerator(String fontName, String []extraArgs) { try { if (generated) return; generated = true; int i; newRanges = new Vector<Range>(); newRanges.addElement(new Range(32, 255, "u0")); // default range String outName = fontName; // guich@401_11 boolean noBold = false; boolean isMono = false; if (extraArgs != null) 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.equals("/aa")) 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]); } // parse parameters if (fontName.indexOf('_') != -1) // guich@421.27: lets the user pass fonts with spaces fontName = fontName.replace('_',' '); this.fontName = fontName; Typeface ttfp = Typeface.DEFAULT;//createFromAsset(Launcher4A.instance.getContext().getAssets(), "fonts/"+fontName+".ttf"); Typeface ttfb = Typeface.DEFAULT_BOLD;//createFromAsset(Launcher4A.instance.getContext().getAssets(), "fonts/"+fontName+"bd.ttf"); // create fonts println("FontGenerator - Copyright (c) SuperWaba 2002-2014. Processing..."); Vector<TCZ.Entry> v = new Vector<TCZ.Entry>(30); if (antialiased != AA_8BPP) { fontsizes = new int[48-7+1]; for (i = 7; i <= 48; i++) fontsizes[i-7] = i; } for (int s = 0; s < fontsizes.length; s++) { i = fontsizes[s]; convertFont(v, ttfp, i, outName+"$p"+i, newRanges, isMono); if (!noBold) convertFont(v, ttfb, i, outName+"$b"+i, newRanges, isMono); } // write the file try {new File(Environment.getExternalStorageDirectory()+"/"+outName).delete();} catch (Exception e) {} // delete if it exists new TCZ(v, outName, (short)0); } catch (Exception e) {e.printStackTrace();} } private Paint setFontSize(int size, Typeface ttf) { Paint paint = new Paint(); paint.setTypeface(ttf); paint.setColor(Color.BLACK); paint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.LINEAR_TEXT_FLAG | Paint.SUBPIXEL_TEXT_FLAG); Rect r = new Rect(); for (float i = size/2; i < MAX_FONT_SIZE*2; i+=0.01) { paint.setTextSize(i); paint.getTextBounds("�g_",0,3,r); //int maxH = (int)(Math.abs(paint.descent()) + Math.abs(paint.ascent())); int realH = Math.abs(r.top - r.bottom); //AndroidUtils.debug(i+": "+realH); if (realH >= size) { AndroidUtils.debug("logic size: "+size+" real size: "+i); return paint; } } return null; } private void convertFont(Vector<TCZ.Entry> v, Typeface ttf, int size, String fileName, Vector<Range> newRanges, boolean isMono) { println("CONVERTING FONT LOGIC SIZE "+size); Bitmap bmp; Canvas g; Paint paint = setFontSize(size, ttf); Rect r = new Rect(); paint.getTextBounds("�",0,1,r); int yy = Math.abs(r.top - r.bottom); // 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 i; // first, we have to compute the sizes for all ranges //if (detailed == 0) System.out.println("Computing bounds for "+f); int n = newRanges.size(); for (int ri = 0; ri < n; ri++) { Range rr = newRanges.get(ri); int ini = rr.s; int end = rr.e; for (i = ini; i <= end; i++) { String si = String.valueOf((char)i); int w = (int)paint.measureText(si)+1; maxW = Math.max(maxW,w); widths[i] = w; if (detailed==1) println("Width of "+((char)i)+" "+i+" = "+widths[i]); } } if (isMono) // guich@tc113_31 { for (i = 0; i < widths.length; i++) widths[i] = maxW; println("Setting all widths to "+maxW); } int nullCount = 0; // now, for each range, compute the totalbits and the chars for (int ri = 0; ri < n; ri++) { Range rr = newRanges.get(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: "+maxH+", 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 : (int)paint.measureText(" "); // guich@tc115_87 pf.maxWidth = maxW; pf.maxHeight = size; pf.descent = (int)paint.descent(); pf.ascent = (size-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]; if (ww > 0) { bmp = Bitmap.createBitmap(ww, size, Bitmap.Config.ARGB_8888); g = new Canvas(bmp); String si = String.valueOf((char)i); bmp.eraseColor(Color.WHITE); g.drawText(si, gaps[i], yy, paint); IntBuffer ibuf = IntBuffer.allocate(ww*size); bmp.copyPixelsToBuffer(ibuf); computeBits(ibuf.array(), sum, ww, i == '#' && size == 70); } sum += ww; } // save the image v.addElement(pf.save()); } if (nullCount > 0) println("A total of "+nullCount+" characters had no glyphs"); } 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 } private void setNibble4(int pixel, int x, int y) { int nibble = 0xF0 - (pixel & 0xF0); // 4 bits of transparency 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 pf.bitmapTable[x + (y * pf.rowWidthInBytes)] = (byte)nibble; // set } private void computeBits(int pixels[], int xx, int w, boolean print) { final int white = -1; int x=0,y=0; //StringBuffer sb = print ? new StringBuffer(50) : null; 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; } } //if (print) sb.append(pixels[i] != white ? "#" : " "); if (++x == w) { //if (print){ AndroidUtils.debug(sb.toString()); sb.setLength(0);} x = 0; y++; } } } /* 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<Range>(); // first we set all bits defined in the given ranges BitSet iv = new BitSet(65535); iv.set(' ',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.set(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.get(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); } } } static public void println(String s) { AndroidUtils.debug(s); } public static void main(String args[]) { try { if (args.length < 1) { String msg = "Format: java FontGenerator <font name> /rename:newName /detailed:1_or_2 /aa\n" + " /DefaultSizes /NoBold /sizes:<comma-separeted list of sizes> /u <list of ranges>\n" + "\n" + "Parameters are case insensitive, meaning:\n" + ". /monospace To create a monospaced font.\n" + ". /rename:newName to rename the output font name.\n" + ". /detailed:1_or_2 to show detailed information.\n" + ". /aa to create an antialiased font.\n" + ". /NoBold to don't create the bold font.\n" + ". /skipBigChars: useful when creating monospaced fonts; the glyphs that have a width above the W char are skipped.\n" + ". /u to create unicode chars in the range. By default, we create chars in the\n" + "range 32-255. Using this option, you can pass ranges in the form\n" + "\"start0-end0 start1-end1 start2-end2 ...\", which creates a file containing the\n" + "characters ranging from \"startN <= c <= endN\". For example:\n" + "\"/u 32-255 256-383 402-402 20284-40869\". The ranges must be in increased order.\n" + "The /u option must be the LAST option used.\n" + "\n" + "When creating unicode fonts of a wide range, using options /nobold /defaultsizes\n" + "will create a file 1/4 of the original size.\n" + "\n" + "Alternative format: java FontGenerator test <font name>\n" + "This will open a TotalCross app to test the font\n" + "\n" + "Copyright (c) SuperWaba 2002-2011\n"; Launcher4A.alert(msg); } } 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 { ByteArrayOutputStream from = new ByteArrayOutputStream(4096); TCZ.writeShort(from,antialiased); TCZ.writeShort(from,firstChar ); TCZ.writeShort(from,lastChar ); TCZ.writeShort(from,spaceWidth ); TCZ.writeShort(from,maxWidth ); TCZ.writeShort(from,maxHeight ); TCZ.writeShort(from,owTLoc ); TCZ.writeShort(from,ascent ); TCZ.writeShort(from,descent ); TCZ.writeShort(from,rowWords ); from.write(bitmapTable); for (int i=0; i < bitIndexTable.length; i++) TCZ.writeShort(from,bitIndexTable[i]); // compress the font byte[] fromBytes = from.toByteArray(); byte[] toBytes = TCZ.compress(fromBytes); println("Font "+fileName+" stored compressed ("+fromBytes.length+" -> "+toBytes.length+")"); return new TCZ.Entry(toBytes, fileName.toLowerCase(),fromBytes.length); } 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("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]; } } }