001: /******************************************************************************
002: * TTF2FFT.java
003: * ****************************************************************************/package org.openlaszlo.media;
004:
005: import java.io.InputStream;
006: import java.io.FileInputStream;
007: import java.io.IOException;
008: import java.io.FileNotFoundException;
009: import java.io.File;
010: import java.awt.geom.Rectangle2D;
011:
012: // JGenerator APIs
013: import org.openlaszlo.iv.flash.api.*;
014: import org.openlaszlo.iv.flash.api.shape.*;
015: import org.openlaszlo.iv.flash.api.sound.*;
016: import org.openlaszlo.iv.flash.api.text.Font;
017: import org.openlaszlo.iv.flash.api.image.*;
018: import org.openlaszlo.iv.flash.util.*;
019:
020: import org.openlaszlo.server.LPS;
021:
022: // Logger
023: import org.apache.log4j.*;
024:
025: // Apache Batik TrueType Font Parser
026: import org.apache.batik.svggen.font.*;
027: import org.apache.batik.svggen.font.table.*;
028:
029: /**
030: * TrueType Font to Flash Font converter
031: *
032: * @author <a href="mailto:bloch@laszlosystems.com">Eric Bloch</a>
033: */
034: public class TTF2FFT {
035:
036: /** Character code used by lfc for newline processing
037: * See LzFont.as and LzFontManager.as. 0 doesn't work for some reason.
038: */
039: private final static int LFC_TMARK = 160;
040:
041: /** Logger */
042: private static Logger mLogger = Logger.getLogger(TTF2FFT.class);
043:
044: /** Units per EmSquare for FFTs */
045: private static final int FFT_UnitsPerEm = 1024;
046:
047: /**
048: * @param input input TTF file
049: * @return InputStream FFT
050: */
051: public static InputStream convert(File input)
052: throws TranscoderException, FileNotFoundException {
053:
054: String path = input.getPath();
055:
056: if (!input.exists()) {
057: throw new FileNotFoundException(path);
058: }
059:
060: // Batik should throw an exception when it can't read
061: // the file (for access perms), but it doesn't.
062: if (!input.canRead()) {
063: throw new FileNotFoundException(
064: /* (non-Javadoc)
065: * @i18n.test
066: * @org-mes="Can't read: " + p[0]
067: */
068: org.openlaszlo.i18n.LaszloMessages.getMessage(TTF2FFT.class
069: .getName(), "051018-75", new Object[] { path }));
070: }
071:
072: org.apache.batik.svggen.font.Font ttf;
073: ttf = org.apache.batik.svggen.font.Font.create(input.getPath());
074:
075: NameTable nameTable = ttf.getNameTable();
076: String fontName = "";
077: if (nameTable == null) {
078: fontName = input.getName();
079: int index = fontName.indexOf(".");
080: if (index > 0) {
081: fontName = fontName.substring(0, index);
082: }
083: mLogger
084: .warn(
085: /* (non-Javadoc)
086: * @i18n.test
087: * @org-mes="font missing ttf name table; made name, " + p[0] + ", based on filename "
088: */
089: org.openlaszlo.i18n.LaszloMessages.getMessage(
090: TTF2FFT.class.getName(), "051018-96",
091: new Object[] { fontName }));
092: } else {
093: fontName = nameTable.getRecord((short) 1);
094: }
095: HeadTable headTable = ttf.getHeadTable();
096: HmtxTable hmtxTable = ttf.getHmtxTable();
097:
098: if (headTable == null) {
099: // Bitmap fonts aren't required to have the head table.
100: // We don't support them yet. XXX
101: throw new TranscoderException(
102: /* (non-Javadoc)
103: * @i18n.test
104: * @org-mes=p[0] + " missing ttf head table; this ttf font not supported"
105: */
106: org.openlaszlo.i18n.LaszloMessages.getMessage(TTF2FFT.class
107: .getName(), "051018-113", new Object[] { path }));
108: }
109:
110: if (hmtxTable == null) {
111: throw new TranscoderException(
112: /* (non-Javadoc)
113: * @i18n.test
114: * @org-mes=p[0] + " missing ttf hmtx (horiz. metrics) table; this ttf font not supported"
115: */
116: org.openlaszlo.i18n.LaszloMessages.getMessage(TTF2FFT.class
117: .getName(), "051018-124", new Object[] { path }));
118: }
119:
120: // FFT flags
121: int flags = 0;
122:
123: // Is font bold, italic, or bold-italic?
124: int macStyle = headTable.getMacStyle();
125: boolean isBold = (macStyle & 0x1) != 0;
126: boolean isItalic = (macStyle & 0x2) != 0;
127:
128: boolean isUnicode = false;
129:
130: if (isBold)
131: flags |= org.openlaszlo.iv.flash.api.text.Font.BOLD;
132: if (isItalic)
133: flags |= org.openlaszlo.iv.flash.api.text.Font.ITALIC;
134:
135: // We have font metric info for the ttf
136: flags |= org.openlaszlo.iv.flash.api.text.Font.HAS_LAYOUT;
137:
138: final int maxCodes = 0xffff;
139: int numCodes = 0;
140:
141: int[] codeTable = new int[maxCodes];
142: int[] indexTable = new int[maxCodes];
143: int maxCode = 0;
144:
145: // Add Code 0 (not sure why this is needed. Probably some lfc reason
146: codeTable[0] = 0;
147: indexTable[0] = 0;
148: numCodes = 1;
149:
150: // 3 tries
151: final int NUM_TRIES = 3;
152: short[] cmapPlats = { Table.platformMicrosoft,
153: Table.platformMacintosh, Table.platformMicrosoft, };
154:
155: short[] cmapEncodes = { Table.encodingUGL, Table.encodingRoman,
156: Table.encodingUndefined, };
157:
158: boolean[] cmapIsUnicode = { true, false, false, };
159:
160: int tries = 0;
161:
162: CmapFormat cmapFmt = null;
163: boolean hasTmark = false;
164: int spaceIndex = 0;
165:
166: for (int t = 0; t < NUM_TRIES; t++) {
167:
168: cmapFmt = ttf.getCmapTable().getCmapFormat(cmapPlats[t],
169: cmapEncodes[t]);
170: // Find char codes
171: if (cmapFmt != null) {
172: for (int ch = 0; ch < 0xffff; ch++) {
173: int index = cmapFmt.mapCharCode(ch);
174:
175: if (ch == 32) {
176: spaceIndex = index;
177: }
178:
179: if (index != 0) {
180: if (ch == LFC_TMARK) {
181: hasTmark = true;
182: }
183: codeTable[numCodes] = ch;
184: indexTable[numCodes] = index;
185: numCodes++;
186: if (ch > maxCode) {
187: maxCode = ch;
188: }
189: }
190: }
191: }
192: if (numCodes > 1) {
193: break;
194: }
195: isUnicode = cmapIsUnicode[t];
196: }
197:
198: if (cmapFmt == null) {
199: throw new TranscoderException(
200: /* (non-Javadoc)
201: * @i18n.test
202: * @org-mes="Can't find a cmap table in " + p[0]
203: */
204: org.openlaszlo.i18n.LaszloMessages.getMessage(TTF2FFT.class
205: .getName(), "051018-224", new Object[] { path }));
206: }
207:
208: if (!hasTmark) {
209: if (LFC_TMARK > maxCode) {
210: maxCode = LFC_TMARK;
211: }
212:
213: codeTable[numCodes] = LFC_TMARK;
214: indexTable[numCodes] = spaceIndex;
215: numCodes++;
216: }
217:
218: if (isUnicode)
219: flags |= org.openlaszlo.iv.flash.api.text.Font.UNICODE;
220: else
221: flags |= org.openlaszlo.iv.flash.api.text.Font.ANSI;
222:
223: boolean useWideCodes = (maxCode > 255);
224: if (useWideCodes)
225: flags |= org.openlaszlo.iv.flash.api.text.Font.WIDE_CODES;
226:
227: GlyfTable glyfTable = (GlyfTable) ttf
228: .getTable(org.apache.batik.svggen.font.table.Table.glyf);
229:
230: //int numGlyphs = ttf.getNumGlyphs();
231: int numGlyphs = numCodes;
232: Shape[] shapeTable = new Shape[numGlyphs];
233: Rectangle2D[] boundsTable = new Rectangle2D[numGlyphs];
234:
235: int unitsPerEm = headTable.getUnitsPerEm();
236: double factor = (double) FFT_UnitsPerEm / (double) unitsPerEm;
237:
238: // Get glyph shapes, and bounds.
239: for (int i = 0; i < numGlyphs; i++) {
240: int index = indexTable[i];
241: int code = codeTable[i];
242: GlyfDescript glyf = glyfTable.getDescription(index);
243: TTFGlyph glyph = null;
244:
245: if (glyf != null) {
246: glyph = new TTFGlyph(glyf);
247: glyph.scale(factor);
248: mLogger.debug(
249: /* (non-Javadoc)
250: * @i18n.test
251: * @org-mes="index: " + p[0] + " charcode: " + p[1] + " char: " + p[2] + " numPoints: " + p[3]
252: */
253: org.openlaszlo.i18n.LaszloMessages.getMessage(
254: TTF2FFT.class.getName(), "051018-274",
255: new Object[] { new Integer(index),
256: new Integer(code),
257: new Character((char) code),
258: new Integer(glyph.getNumPoints()) }));
259: } else {
260: mLogger.debug(
261: /* (non-Javadoc)
262: * @i18n.test
263: * @org-mes="index: " + p[0] + " charcode: " + p[1] + " has no glyph."
264: */
265: org.openlaszlo.i18n.LaszloMessages.getMessage(
266: TTF2FFT.class.getName(), "051018-283",
267: new Object[] { new Integer(index),
268: new Integer(code) }));
269: }
270:
271: Shape shape = new Shape();
272: shape.newStyleBlock();
273: convertGlyphToShape(glyph, shape);
274: shapeTable[i] = shape;
275:
276: int x, w, y, h;
277:
278: if (glyf != null) {
279: x = (int) Math.round(glyf.getXMinimum() * factor);
280: y = (int) Math.round(glyf.getYMaximum() * -factor);
281: w = (int) Math.round((glyf.getXMaximum() - glyf
282: .getXMinimum())
283: * factor);
284: h = (int) Math.round((glyf.getYMaximum() - glyf
285: .getYMinimum())
286: * factor);
287: } else {
288: // Heuristic that hopefully works out ok for
289: // missing glyfs. First try space. Then try index0
290: glyf = glyfTable.getDescription(spaceIndex);
291: if (glyf == null) {
292: glyf = glyfTable.getDescription(0);
293: }
294: if (glyf != null) {
295: w = (int) Math.round((glyf.getXMaximum() - glyf
296: .getXMinimum())
297: * factor);
298: } else {
299: w = 0;
300: }
301: x = y = h = 0;
302: }
303: boundsTable[i] = new Rectangle2D.Double(x, y, w, h);
304: shape.setBounds(boundsTable[i]);
305: }
306:
307: // Create a 40K buffer for generating the FFT
308: FlashOutput buf = new FlashOutput(40 * 1024);
309:
310: // write header.
311: final int TWIP = 20;
312:
313: buf.writeByte('F');
314: buf.writeByte('W');
315: buf.writeByte('S');
316: // write version
317: buf.writeByte(5);
318: // skip file size
319: buf.skip(4);
320: // write rect
321: buf.write(new Rectangle2D.Double(0, 0, 5 * TWIP, 5 * TWIP));
322: // write frame rate
323: buf.writeWord(10 << 8);
324:
325: // Frame count
326: buf.writeWord(0);
327:
328: // Remember position
329: int tagPos = buf.getPos();
330:
331: // Skip definefont2 tag header
332: buf.skip(6);
333:
334: // Write font id
335: buf.writeWord(1);
336:
337: // Skip flags
338: int flagsPos = buf.getPos();
339: buf.skip(2);
340:
341: // Write font name
342: buf.writeStringL(fontName);
343:
344: // Write number of glyphs
345: buf.writeWord(numGlyphs);
346:
347: int[] offsetTable = new int[numGlyphs];
348:
349: // Write out the converted shapes into a temporary buffer
350: // And remember their offsets
351: FlashOutput glyphBuf = new FlashOutput(20 * 1024);
352: for (int i = 0; i < numGlyphs; i++) {
353:
354: offsetTable[i] = glyphBuf.getPos();
355:
356: mLogger.debug("Writing shape " + i);
357: // 1 bit of line and fill
358: glyphBuf.writeByte(0x11);
359:
360: ShapeRecords shapeRecords = shapeTable[i].getShapeRecords();
361: shapeRecords.write(glyphBuf, 1, 1);
362: // Write end of shape records
363: glyphBuf.writeBits(0, 6);
364: glyphBuf.flushBits();
365: }
366:
367: // UseWideOffset if glyph buf + offset table + codeTable offset
368: // is bigger than 16bit int
369: boolean useWideOffsets = glyphBuf.getSize() + (numGlyphs + 1)
370: * 2 > 0xffff;
371:
372: // Write offsets and codeTable offset
373: if (useWideOffsets) {
374: int offset = (numGlyphs + 1) * 4;
375: flags |= org.openlaszlo.iv.flash.api.text.Font.WIDE_OFFSETS;
376: for (int i = 0; i < numGlyphs; i++) {
377: buf.writeDWord(offsetTable[i] + offset);
378: }
379: buf.writeDWord(glyphBuf.getSize() + offset);
380: } else {
381: int offset = (numGlyphs + 1) * 2;
382: for (int i = 0; i < numGlyphs; i++) {
383: buf.writeWord(offsetTable[i] + offset);
384: }
385: buf.writeWord(glyphBuf.getSize() + offset);
386: }
387:
388: // Write shapes
389: buf.writeFOB(glyphBuf);
390:
391: // Write out char code table. (glyph index to char code)
392: for (int i = 0; i < numCodes; i++) {
393: if (useWideCodes) {
394: buf.writeWord(codeTable[i]);
395: } else {
396: buf.writeByte(codeTable[i]);
397: }
398: }
399:
400: // Write ascent, descent, (external) leading
401: int ascent = (int) Math.round((ttf.getAscent() * factor));
402: int descent = (int) Math.round((ttf.getDescent() * -factor));
403: int leading = ascent + descent - FFT_UnitsPerEm;
404: mLogger.debug(
405: /* (non-Javadoc)
406: * @i18n.test
407: * @org-mes="Font metrics: " + p[0] + " " + p[1] + " " + p[2]
408: */
409: org.openlaszlo.i18n.LaszloMessages.getMessage(TTF2FFT.class
410: .getName(), "051018-421", new Object[] {
411: new Integer(ascent), new Integer(descent),
412: new Integer(leading) }));
413:
414: buf.writeWord(ascent);
415: buf.writeWord(descent);
416: buf.writeWord(leading);
417:
418: // Write advance table
419: for (int i = 0; i < numCodes; i++) {
420: int index = indexTable[i];
421: buf.writeWord((int) Math.round(hmtxTable
422: .getAdvanceWidth(index)
423: * factor));
424: }
425:
426: // Write bounds table
427: for (int i = 0; i < numCodes; i++) {
428: buf.write(boundsTable[i]);
429: }
430:
431: // Write kerning tables
432: int nKern = 0;
433:
434: KernTable kernTable = (KernTable) ttf.getTable(Table.kern);
435: // TODO: [2003-11-05 bloch] this should be passed in as an argument and taken
436: // from the font definition in the LZX file
437: boolean doKern = LPS.getProperty("lps.font.kerning", "false")
438: .equals("true");
439:
440: if (kernTable != null) {
441: if (doKern) {
442: KernSubtable kst = kernTable.getSubtable(0);
443: nKern = kst.getKerningPairCount();
444: mLogger.debug(
445: /* (non-Javadoc)
446: * @i18n.test
447: * @org-mes=p[0] + " kern pairs"
448: */
449: org.openlaszlo.i18n.LaszloMessages.getMessage(
450: TTF2FFT.class.getName(), "051018-456",
451: new Object[] { new Integer(nKern) }));
452: // We optimize out all 0s
453: int goodKern = nKern;
454: for (int i = 0; i < nKern; i++) {
455: if (kst.getKerningPair(i).getValue() == 0) {
456: goodKern--;
457: }
458: }
459: buf.writeWord(goodKern);
460: mLogger.debug(
461: /* (non-Javadoc)
462: * @i18n.test
463: * @org-mes=p[0] + " non-zero kern pairs"
464: */
465: org.openlaszlo.i18n.LaszloMessages.getMessage(
466: TTF2FFT.class.getName(), "051018-472",
467: new Object[] { new Integer(goodKern) }));
468: for (int i = 0; i < nKern; i++) {
469: KerningPair pair = kst.getKerningPair(i);
470: if (pair.getValue() != 0) {
471: if (useWideCodes) {
472: buf.writeWord(codeTable[pair.getLeft()]);
473: buf.writeWord(codeTable[pair.getRight()]);
474: } else {
475: buf.writeByte(codeTable[pair.getLeft()]);
476: buf.writeByte(codeTable[pair.getRight()]);
477: }
478: buf.writeWord((int) Math.round(pair.getValue()
479: * factor));
480: }
481: }
482: } else {
483: mLogger.warn(
484: /* (non-Javadoc)
485: * @i18n.test
486: * @org-mes="skipping non-empty kerning table in " + p[0]
487: */
488: org.openlaszlo.i18n.LaszloMessages.getMessage(
489: TTF2FFT.class.getName(), "051018-494",
490: new Object[] { path }));
491: }
492: } else {
493: buf.writeWord(0);
494: }
495:
496: // Write the DEFINEFONT2 tag
497: int x = buf.getPos() - tagPos - 6;
498: buf.writeLongTagAt(Tag.DEFINEFONT2, x, tagPos);
499: // Write the flags
500: buf.writeWordAt(flags, flagsPos);
501:
502: // Write the END tag
503: Tag.END_TAG.write(buf);
504:
505: // Write the file size back at the beginning.
506: int filesize = buf.getSize();
507: buf.writeDWordAt(filesize, 4);
508:
509: return buf.getInputStream();
510: }
511:
512: /**
513: * Convert TTF Glyph to Flash Shape
514: * @param glyph
515: * @param shape
516: */
517: private static void convertGlyphToShape(TTFGlyph glyph, Shape shape) {
518:
519: if (glyph == null) {
520: return;
521: }
522: int firstIndex = 0;
523: int count = 0;
524:
525: // Add each contour to the shape.
526: for (int i = 0; i < glyph.getNumPoints(); i++) {
527: count++;
528: if (glyph.getPoint(i).endOfContour) {
529: addContourToShape(shape, glyph, firstIndex, count);
530: firstIndex = i + 1;
531: count = 0;
532: }
533: }
534: }
535:
536: /**
537: * Add glyphs contour starting from index point and going
538: * count number of points to shape.
539: * @param shape
540: * @param glyph
541: * @param startIndex
542: * @param count
543: */
544: private static void addContourToShape(Shape shape, TTFGlyph glyph,
545: int startIndex, int count) {
546:
547: // If this is a single point on it's own, we can't do anything with it
548: if (glyph.getPoint(startIndex).endOfContour) {
549: return;
550: }
551:
552: int offset = 0;
553:
554: while (offset < count) {
555: Point p0 = glyph.getPoint(startIndex + offset % count);
556: Point p1 = glyph
557: .getPoint(startIndex + (offset + 1) % count);
558:
559: if (offset == 0) {
560: shape.movePenTo(p0.x, p0.y);
561: if (startIndex == 0) {
562: StyleChangeRecord scr = new StyleChangeRecord();
563: scr.setFlags(StyleChangeRecord.FILLSTYLE1
564: | StyleChangeRecord.LINESTYLE);
565: scr.setFillStyle1(1);
566: scr.setLineStyle(0);
567: shape.getShapeRecords().addStyleChangeRecord(scr);
568: }
569: }
570:
571: if (p0.onCurve) {
572: if (p1.onCurve) {
573: shape.drawLineTo(p1.x, p1.y);
574: offset++;
575: } else {
576: Point p2;
577: p2 = glyph.getPoint(startIndex + (offset + 2)
578: % count);
579:
580: if (p2.onCurve) {
581: shape.drawCurveTo(p1.x, p1.y, p2.x, p2.y);
582: } else {
583: shape.drawCurveTo(p1.x, p1.y, midValue(p1.x,
584: p2.x), midValue(p1.y, p2.y));
585: }
586: offset += 2;
587: }
588: } else {
589: if (!p1.onCurve) {
590: shape.drawCurveTo(p0.x, p0.y, midValue(p0.x, p1.x),
591: midValue(p0.y, p1.y));
592: } else {
593: shape.drawCurveTo(p0.x, p0.y, p1.x, p1.y);
594: }
595: offset++;
596: }
597: }
598: }
599:
600: /**
601: * @return midpoint of (a,b)
602: * @param a
603: * @param b
604: */
605: private static int midValue(int a, int b) {
606: return (a + b) / 2;
607: }
608: }
|