don't do cvs commit.
[mftrace.git] / mftrace.py
blob9b513bff22b00c3acf3fff46f9eecb2069206ca8
1 #!@PYTHON@
4 # mftrace - Generate Type1 or TrueType font from Metafont source.
6 # Copyright (c) 2001--2006 by
7 # Han-Wen Nienhuys, Jan Nieuwenhuizen
8 #
9 # Distributed under terms of the GNU General Public License. It comes with
10 # NO WARRANTY.
13 import string
14 import os
15 import optparse
16 import sys
17 import re
18 import tempfile
19 import shutil
21 prefix = '@prefix@'
22 bindir = '@bindir@'
23 datadir = '@datadir@'
24 localedir = datadir + '/locale'
25 libdir = '@libdir@'
26 exec_prefix = '@exec_prefix@'
28 def interpolate (str):
29 str = string.replace (str, '{', '(')
30 str = string.replace (str, '}', ')s')
31 str = string.replace (str, '$', '%')
32 return str
34 if prefix != '@' + 'prefix@':
35 exec_prefix = interpolate (exec_prefix) % vars ()
36 bindir = interpolate (bindir) % vars ()
37 datadir = os.path.join (interpolate (datadir) % vars (), 'mftrace')
38 libdir = interpolate (libdir) % vars ()
40 if datadir == '@' + "datadir" + "@":
41 datadir = os.getcwd ()
42 bindir = os.getcwd ()
44 sys.path.append (datadir)
46 import afm
47 import tfm
49 errorport = sys.stderr
51 ################################################################
52 # lilylib.py -- options and stuff
54 # source file of the GNU LilyPond music typesetter
56 try:
57 import gettext
58 gettext.bindtextdomain ('mftrace', localedir)
59 gettext.textdomain ('mftrace')
60 _ = gettext.gettext
61 except:
62 def _ (s):
63 return s
65 def shell_escape_filename (str):
66 str = re.sub ('([\'" ])', r'\\\1', str)
67 return str
69 def identify (port):
70 port.write ('%s %s\n' % (program_name, program_version))
72 def warranty ():
73 identify (sys.stdout)
74 sys.stdout.write ('\n')
75 sys.stdout.write (_ ('Copyright (c) %s by' % ' 2001--2004'))
76 sys.stdout.write ('\n')
77 sys.stdout.write (' Han-Wen Nienhuys')
78 sys.stdout.write (' Jan Nieuwenhuizen')
79 sys.stdout.write ('\n')
80 sys.stdout.write (_ (r'''
81 Distributed under terms of the GNU General Public License. It comes with
82 NO WARRANTY.'''))
83 sys.stdout.write ('\n')
85 def progress (s):
86 errorport.write (s)
88 def warning (s):
89 errorport.write (_ ("warning: ") + s)
91 def error (s):
92 '''Report the error S. Exit by raising an exception. Please
93 do not abuse by trying to catch this error. If you do not want
94 a stack trace, write to the output directly.
96 RETURN VALUE
98 None
102 errorport.write (_ ("error: ") + s + '\n')
103 raise _ ("Exiting ... ")
105 def setup_temp ():
107 Create a temporary directory, and return its name.
109 global temp_dir
110 if not options.keep_temp_dir:
111 temp_dir = tempfile.mkdtemp (program_name)
113 try:
114 os.mkdir (temp_dir, 0700)
115 except OSError:
116 pass
118 return temp_dir
120 def popen (cmd, mode = 'r', ignore_error = 0):
121 if options.verbose:
122 progress (_ ("Opening pipe `%s\'") % cmd)
123 pipe = os.popen (cmd, mode)
124 if options.verbose:
125 progress ('\n')
126 return pipe
128 def system (cmd, ignore_error = 0):
129 """Run CMD. If IGNORE_ERROR is set, don't complain when CMD returns non zero.
131 RETURN VALUE
133 Exit status of CMD
136 if options.verbose:
137 progress (_ ("Invoking `%s\'\n") % cmd)
138 st = os.system (cmd)
139 if st:
140 name = re.match ('[ \t]*([^ \t]*)', cmd).group (1)
141 msg = name + ': ' + _ ("command exited with value %d") % st
142 if ignore_error:
143 warning (msg + ' ' + _ ("(ignored)") + ' ')
144 else:
145 error (msg)
146 if options.verbose:
147 progress ('\n')
148 return st
150 def cleanup_temp ():
151 if not options.keep_temp_dir:
152 if options.verbose:
153 progress (_ ("Cleaning %s...") % temp_dir)
154 shutil.rmtree (temp_dir)
157 def strip_extension (f, ext):
158 (p, e) = os.path.splitext (f)
159 if e == ext:
160 e = ''
161 return p + e
164 ################################################################
165 # END Library
169 options = None
170 exit_value = 0
171 backend_options = ''
172 program_name = 'mftrace'
173 temp_dir = os.path.join (os.getcwd (), program_name + '.dir')
174 program_version = '@VERSION@'
175 origdir = os.getcwd ()
177 coding_dict = {
179 # from TeTeX
180 'TeX typewriter text': '09fbbfac.enc', # cmtt10
181 'TeX math symbols':'10037936.enc ', # cmbsy
182 'ASCII caps and digits':'1b6d048e', # cminch
183 'TeX math italic': 'aae443f0.enc ', # cmmi10
184 'TeX extended ASCII':'d9b29452.enc',
185 'TeX text': 'f7b6d320.enc',
186 'TeX text without f-ligatures': '0ef0afca.enc',
188 'Extended TeX Font Encoding - Latin': 'tex256.enc',
190 # LilyPond.
191 'fetaBraces': 'feta-braces-a.enc',
192 'fetaNumber': 'feta-nummer10.enc',
193 'fetaMusic': 'feta20.enc',
194 'parmesanMusic': 'parmesan20.enc',
198 def find_file (nm):
199 for d in include_dirs:
200 p = os.path.join (d, nm)
201 try:
202 open (p)
203 return p
204 except IOError:
205 pass
207 p = popen ('kpsewhich %s' % shell_escape_filename (nm)).read ()
208 p = p.strip ()
210 if options.dos_kpath:
211 orig = p
212 p = string.lower (p)
213 p = re.sub ('^([a-z]):', '/cygdrive/\\1', p)
214 p = re.sub ('\\\\', '/', p)
215 sys.stderr.write ("Got `%s' from kpsewhich, using `%s'\n" % (orig, p))
216 return p
219 def flag_error ():
220 global exit_value
221 exit_value = 1
223 ################################################################
224 # TRACING.
225 ################################################################
227 def autotrace_command (fn, opts):
228 opts = " " + opts + " --background-color=FFFFFF --output-format=eps --input-format=pbm "
229 return options.trace_binary + opts + backend_options \
230 + " --output-file=char.eps %s " % fn
232 def potrace_command (fn, opts):
233 return options.trace_binary + opts \
234 + ' -u %d ' % options.grid_scale \
235 + backend_options \
236 + " -q -c --eps --output=char.eps %s " % (fn)
238 trace_command = None
239 path_to_type1_ops = None
241 def trace_one (pbmfile, id):
243 Run tracer, do error handling
246 status = system (trace_command (pbmfile, ''), 1)
248 if status == 2:
249 sys.stderr.write ("\nUser interrupt. Exiting\n")
250 sys.exit (2)
252 if status == 0 and options.keep_temp_dir:
253 shutil.copy2 (pbmfile, '%s.pbm' % id)
254 shutil.copy2 ('char.eps', '%s.eps' % id)
256 if status != 0:
257 error_file = os.path.join (origdir, 'trace-bug-%s.pbm' % id)
258 shutil.copy2 (pbmfile, error_file)
259 msg = """Trace failed on bitmap. Bitmap left in `%s\'
260 Failed command was:
264 Please submit a bugreport to %s development.""" \
265 % (error_file, trace_command (error_file, ''), options.trace_binary)
267 if options.keep_trying:
268 warning (msg)
269 sys.stderr.write ("\nContinuing trace...\n")
270 flag_error ()
271 else:
272 msg = msg + '\nRun mftrace with --keep-trying to produce a font anyway\n'
273 error (msg)
274 else:
275 return 1
277 if status != 0:
278 warning ("Failed, skipping character.\n")
279 return 0
280 else:
281 return 1
283 def make_pbm (filename, outname, char_number):
284 """ Extract bitmap from the PK file FILENAME (absolute) using `gf2pbm'.
285 Return FALSE if the glyph is not valid.
288 command = "%s/gf2pbm -n %d -o %s %s" % (bindir, char_number, outname, filename)
289 status = system (command, ignore_error = 1)
290 return (status == 0)
292 def read_encoding (file):
293 sys.stderr.write (_ ("Using encoding file: `%s'\n") % file)
295 str = open (file).read ()
296 str = re.sub ("%.*", '', str)
297 str = re.sub ("[\n\t \f]+", ' ', str)
298 m = re.search ('/([^ ]+) \[([^\]]+)\] def', str)
299 if not m:
300 error ("Encoding file is invalid")
302 name = m.group (1)
303 cod = m.group (2)
304 cod = re.sub ('[ /]+', ' ', cod)
305 cods = string.split (cod)
307 return (name, cods)
309 def zip_to_pairs (as):
310 r = []
311 while as:
312 r.append ((as[0], as[1]))
313 as = as[2:]
314 return r
316 def unzip_pairs (tups):
317 lst = []
318 while tups:
319 lst = lst + list (tups[0])
320 tups = tups[1:]
321 return lst
323 def autotrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
324 inv_scale = 1000.0 / magnification
326 (size_y, size_x, off_x, off_y) = map (lambda m, s = inv_scale: m * s,
327 bitmap_metrics)
328 ls = open (at_file).readlines ()
329 bbox = (10000, 10000, -10000, -10000)
331 while ls and ls[0] != '*u\n':
332 ls = ls[1:]
334 if ls == []:
335 return (bbox, '')
337 ls = ls[1:]
339 commands = []
342 while ls[0] != '*U\n':
343 ell = ls[0]
344 ls = ls[1:]
346 toks = string.split (ell)
348 if len (toks) < 1:
349 continue
350 cmd = toks[-1]
351 args = map (lambda m, s = inv_scale: s * float (m),
352 toks[:-1])
353 if options.round_to_int:
354 args = zip_to_pairs (map (round, args))
355 else:
356 args = zip_to_pairs (args)
357 commands.append ((cmd, args))
359 expand = {
360 'l': 'rlineto',
361 'm': 'rmoveto',
362 'c': 'rrcurveto',
363 'f': 'closepath',
366 cx = 0
367 cy = size_y - off_y - inv_scale
369 # t1asm seems to fuck up when using sbw. Oh well.
370 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
371 bbox = (10000, 10000, -10000, -10000)
373 for (c, args) in commands:
375 na = []
376 for a in args:
377 (nx, ny) = a
378 if c == 'l' or c == 'c':
379 bbox = update_bbox_with_point (bbox, a)
381 na.append ((nx - cx, ny - cy))
382 (cx, cy) = (nx, ny)
384 a = na
385 c = expand[c]
386 if options.round_to_int:
387 a = map (lambda x: '%d' % int (round (x)),
388 unzip_pairs (a))
389 else:
390 a = map (lambda x: '%d %d div' \
391 % (int (round (x * options.grid_scale/inv_scale)),
392 int (round (options.grid_scale/inv_scale))),
393 unzip_pairs (a))
395 t1_outline = t1_outline + ' %s %s\n' % (string.join (a), c)
397 t1_outline = t1_outline + ' endchar '
398 t1_outline = '{\n %s } |- \n' % t1_outline
400 return (bbox, t1_outline)
402 # FIXME: Cut and paste programming
403 def potrace_path_to_type1_ops (at_file, bitmap_metrics, tfm_wid, magnification):
404 inv_scale = 1000.0 / magnification
406 (size_y, size_x, off_x, off_y) = map (lambda m,
407 s = inv_scale: m * s,
408 bitmap_metrics)
409 ls = open (at_file).readlines ()
410 bbox = (10000, 10000, -10000, -10000)
412 while ls and ls[0] != '0 setgray\n':
413 ls = ls[1:]
415 if ls == []:
416 return (bbox, '')
417 ls = ls[1:]
418 commands = []
420 while ls and ls[0] != 'grestore\n':
421 ell = ls[0]
422 ls = ls[1:]
424 if ell == 'fill\n':
425 continue
427 toks = string.split (ell)
429 if len (toks) < 1:
430 continue
431 cmd = toks[-1]
432 args = map (lambda m, s = inv_scale: s * float (m),
433 toks[:-1])
434 args = zip_to_pairs (args)
435 commands.append ((cmd, args))
437 # t1asm seems to fuck up when using sbw. Oh well.
438 t1_outline = ' %d %d hsbw\n' % (- off_x, tfm_wid)
439 bbox = (10000, 10000, -10000, -10000)
441 # Type1 fonts have relative coordinates (doubly relative for
442 # rrcurveto), so must convert moveto and rcurveto.
444 z = (0.0, size_y - off_y - 1.0)
445 for (c, args) in commands:
446 args = map (lambda x: (x[0] * (1.0 / options.grid_scale),
447 x[1] * (1.0 / options.grid_scale)), args)
449 if c == 'moveto':
450 args = [(args[0][0] - z[0], args[0][1] - z[1])]
452 zs = []
453 for a in args:
454 lz = (z[0] + a[0], z[1] + a[1])
455 bbox = update_bbox_with_point (bbox, lz)
456 zs.append (lz)
458 if options.round_to_int:
459 last_discr_z = (int (round (z[0])), int (round (z[1])))
460 else:
461 last_discr_z = (z[0], z[1])
462 args = []
463 for a in zs:
464 if options.round_to_int:
465 a = (int (round (a[0])), int (round (a[1])))
466 else:
467 a = (a[0], a[1])
468 args.append ((a[0] - last_discr_z[0],
469 a[1] - last_discr_z[1]))
471 last_discr_z = a
473 if zs:
474 z = zs[-1]
475 c = { 'rcurveto': 'rrcurveto',
476 'moveto': 'rmoveto',
477 'closepath': 'closepath',
478 'rlineto': 'rlineto'}[c]
480 if c == 'rmoveto':
481 t1_outline += ' closepath '
483 if options.round_to_int:
484 args = map (lambda x: '%d' % int (round (x)),
485 unzip_pairs (args))
486 else:
487 args = map (lambda x: '%d %d div' \
488 % (int (round (x*options.grid_scale/inv_scale)),
489 int (round (options.grid_scale/inv_scale))),
490 unzip_pairs (args))
492 t1_outline = t1_outline + ' %s %s\n' % (string.join (args), c)
494 t1_outline = t1_outline + ' endchar '
495 t1_outline = '{\n %s } |- \n' % t1_outline
497 return (bbox, t1_outline)
499 def read_gf_dims (name, c):
500 str = popen ('%s/gf2pbm -n %d -s %s' % (bindir, c, name)).read ()
501 m = re.search ('size: ([0-9]+)+x([0-9]+), offset: \(([0-9-]+),([0-9-]+)\)', str)
503 return tuple (map (int, m.groups ()))
505 def trace_font (fontname, gf_file, metric, glyphs, encoding,
506 magnification, fontinfo):
507 t1os = []
508 font_bbox = (10000, 10000, -10000, -10000)
510 progress (_ ("Tracing bitmaps... "))
512 # for single glyph testing.
513 # glyphs = []
514 for a in glyphs:
515 if encoding[a] == ".notavail":
516 continue
517 valid = metric.has_char (a)
518 if not valid:
519 encoding[a] = ".notavail"
520 continue
522 valid = make_pbm (gf_file, 'char.pbm', a)
523 if not valid:
524 encoding[a] = ".notavail"
525 continue
527 (w, h, xo, yo) = read_gf_dims (gf_file, a)
529 if not options.verbose:
530 sys.stderr.write ('[%d' % a)
531 sys.stderr.flush ()
533 # this wants the id, not the filename.
534 success = trace_one ("char.pbm", '%s-%d' % (options.gffile, a))
535 if not success:
536 sys.stderr.write ("(skipping character)]")
537 sys.stderr.flush ()
538 encoding[a] = ".notavail"
539 continue
541 if not options.verbose:
542 sys.stderr.write (']')
543 sys.stderr.flush ()
544 metric_width = metric.get_char (a).width
545 tw = int (round (metric_width / metric.design_size * 1000))
546 (bbox, t1o) = path_to_type1_ops ("char.eps", (h, w, xo, yo),
547 tw, magnification)
549 if t1o == '':
550 encoding[a] = ".notavail"
551 continue
553 font_bbox = update_bbox_with_bbox (font_bbox, bbox)
555 t1os.append ('\n/%s %s ' % (encoding[a], t1o))
557 progress ('\n')
558 to_type1 (t1os, font_bbox, fontname, encoding, magnification, fontinfo)
560 def ps_encode_encoding (encoding):
561 str = ' %d array\n0 1 %d {1 index exch /.notdef put} for\n' \
562 % (len (encoding), len (encoding)-1)
564 for i in range (0, len (encoding)):
565 if encoding[i] != ".notavail":
566 str = str + 'dup %d /%s put\n' % (i, encoding[i])
568 return str
571 def gen_unique_id (dict):
572 nm = 'FullName'
573 return 4000000 + (hash (nm) % 1000000)
575 def to_type1 (outlines, bbox, fontname, encoding, magnification, fontinfo):
578 Fill in the header template for the font, append charstrings,
579 and shove result through t1asm
581 template = r"""%%!PS-AdobeFont-1.0: %(FontName)s %(VVV)s.%(WWW)s
582 13 dict begin
583 /FontInfo 16 dict dup begin
584 /version (%(VVV)s.%(WWW)s) readonly def
585 /Notice (%(Notice)s) readonly def
586 /FullName (%(FullName)s) readonly def
587 /FamilyName (%(FamilyName)s) readonly def
588 /Weight (%(Weight)s) readonly def
589 /ItalicAngle %(ItalicAngle)s def
590 /isFixedPitch %(isFixedPitch)s def
591 /UnderlinePosition %(UnderlinePosition)s def
592 /UnderlineThickness %(UnderlineThickness)s def
593 end readonly def
594 /FontName /%(FontName)s def
595 /FontType 1 def
596 /PaintType 0 def
597 /FontMatrix [%(xrevscale)f 0 0 %(yrevscale)f 0 0] readonly def
598 /FontBBox {%(llx)d %(lly)d %(urx)d %(ury)d} readonly def
599 /Encoding %(Encoding)s readonly def
600 currentdict end
601 currentfile eexec
602 dup /Private 20 dict dup begin
603 /-|{string currentfile exch readstring pop}executeonly def
604 /|-{noaccess def}executeonly def
605 /|{noaccess put}executeonly def
606 /lenIV 4 def
607 /password 5839 def
608 /MinFeature {16 16} |-
609 /BlueValues [] |-
610 /OtherSubrs [ {} {} {} {} ] |-
611 /ForceBold false def
612 /Subrs 1 array
613 dup 0 { return } |
615 2 index
616 /CharStrings %(CharStringsLen)d dict dup begin
617 %(CharStrings)s
620 /.notdef { 0 0 hsbw endchar } |-
623 readonly put
624 noaccess put
625 dup/FontName get exch definefont
626 pop mark currentfile closefile
627 cleartomark
629 ## apparently, some fonts end the file with cleartomark. Don't know why.
631 copied_fields = ['FontName', 'FamilyName', 'FullName', 'DesignSize',
632 'ItalicAngle', 'isFixedPitch', 'Weight']
634 vars = {
635 'VVV': '001',
636 'WWW': '001',
637 'Notice': 'Generated from MetaFont bitmap by mftrace %s, http://www.xs4all.nl/~hanwen/mftrace/ ' % program_version,
638 'UnderlinePosition': '-100',
639 'UnderlineThickness': '50',
640 'xrevscale': 1.0/1000.0,
641 'yrevscale': 1.0/1000.0,
642 'llx': bbox[0],
643 'lly': bbox[1],
644 'urx': bbox[2],
645 'ury': bbox[3],
646 'Encoding': ps_encode_encoding (encoding),
648 # need one extra entry for .notdef
649 'CharStringsLen': len (outlines) + 1,
650 'CharStrings': string.join (outlines),
651 'CharBBox': '0 0 0 0',
654 for k in copied_fields:
655 vars[k] = fontinfo[k]
657 open ('mftrace.t1asm', 'w').write (template % vars)
659 def update_bbox_with_point (bbox, pt):
660 (llx, lly, urx, ury) = bbox
661 llx = min (pt[0], llx)
662 lly = min (pt[1], lly)
663 urx = max (pt[0], urx)
664 ury = max (pt[1], ury)
666 return (llx, lly, urx, ury)
668 def update_bbox_with_bbox (bb, dims):
669 (llx, lly, urx, ury) = bb
670 llx = min (llx, dims[0])
671 lly = min (lly, dims[1])
672 urx = max (urx, dims[2])
673 ury = max (ury, dims[3])
675 return (llx, lly, urx, ury)
677 def get_binary (name):
678 search_path = string.split (os.environ['PATH'], ':')
679 for p in search_path:
680 nm = os.path.join (p, name)
681 if os.path.exists (nm):
682 return nm
684 return ''
686 def get_fontforge_command ():
687 fontforge_cmd = ''
688 for ff in ['fontforge', 'pfaedit']:
689 if get_binary(ff):
690 fontforge_cmd = ff
692 stat = 1
693 if fontforge_cmd:
694 stat = system ("%s -usage > pfv 2>&1 " % fontforge_cmd,
695 ignore_error = 1)
697 if stat != 0:
698 warning ("Command `%s -usage' failed. Cannot simplify or convert to TTF.\n" % fontforge_cmd)
700 if fontforge_cmd == 'pfaedit' \
701 and re.search ("-script", open ('pfv').read ()) == None:
702 warning ("pfaedit does not support -script. Install 020215 or later.\nCannot simplify or convert to TTF.\n")
703 return ''
704 return fontforge_cmd
706 def tfm2kpx (tfmname, encoding):
707 kpx_lines = []
708 pl = popen ("tftopl %s" % (tfmname))
710 label_pattern = re.compile (
711 "\A \(LABEL ([DOHC]{1}) ([A-Za-z0-9]*)\)")
712 krn_pattern = re.compile (
713 "\A \(KRN ([DOHC]{1}) ([A-Za-z0-9]*) R (-?[\d\.]+)\)")
715 first = 0
716 second = 0
718 for line in pl.readlines ():
720 label_match = label_pattern.search (line)
721 if not (label_match is None):
722 if label_match.group (1) == "D":
723 first = int (label_match.group (2))
724 elif label_match.group (1) == "O":
725 first = int (label_match.group (2), 8)
726 elif label_match.group (1) == "C":
727 first = ord (label_match.group (2))
729 krn_match = krn_pattern.search (line)
730 if not (krn_match is None):
731 if krn_match.group (1) == "D":
732 second = int (krn_match.group (2))
733 elif krn_match.group (1) == "O":
734 second = int (krn_match.group (2), 8)
735 elif krn_match.group (1) == "C":
736 second = ord (krn_match.group (2))
738 krn = round (float (krn_match.group (3)) * 1000)
740 if (encoding[first] != '.notavail' and
741 encoding[first] != '.notdef' and
742 encoding[second] != '.notavail' and
743 encoding[second] != '.notdef'):
745 kpx_lines.append ("KPX %s %s %d\n" % (
746 encoding[first], encoding[second], krn))
748 return kpx_lines
750 def get_afm (t1_path, tfmname, encoding, out_path):
751 afm_stream = popen ("printafm %s" % (t1_path))
752 afm_lines = []
753 kpx_lines = tfm2kpx (tfmname, encoding)
755 for line in afm_stream.readlines ():
756 afm_lines.append (line)
758 if re.match (r"^EndCharMetrics", line, re.I):
759 afm_lines.append ("StartKernData\n")
760 afm_lines.append ("StartKernPairs %d\n" % len (kpx_lines))
762 for kpx_line in kpx_lines:
763 afm_lines.append (kpx_line)
765 afm_lines.append ("EndKernPairs\n")
766 afm_lines.append ("EndKernData\n")
768 progress (_ ("Writing metrics to `%s'... ") % out_path)
769 afm_file = open (out_path, 'w')
770 afm_file.writelines (afm_lines)
771 afm_file.flush ()
772 afm_file.close ()
774 progress ('\n')
776 def assemble_font (fontname, format, is_raw):
777 ext = '.' + format
778 asm_opt = '--pfa'
780 if format == 'pfb':
781 asm_opt = '--pfb'
783 if is_raw:
784 ext = ext + '.raw'
786 outname = fontname + ext
788 progress (_ ("Assembling raw font to `%s'... ") % outname)
789 system ('t1asm %s mftrace.t1asm %s' % (asm_opt, shell_escape_filename (outname)))
790 progress ('\n')
791 return outname
793 def make_outputs (fontname, formats, encoding):
795 run pfaedit to convert to other formats
798 ff_needed = 0
799 ff_command = ""
801 if (options.simplify or options.round_to_int or 'ttf' in formats or 'svg' in formats):
802 ff_needed = 1
803 if ff_needed:
804 ff_command = get_fontforge_command ()
806 if ff_needed and ff_command:
807 raw_name = assemble_font (fontname, 'pfa', 1)
809 simplify_cmd = ''
810 if options.round_to_int:
811 simplify_cmd = 'RoundToInt ();'
812 generate_cmds = ''
813 for f in formats:
814 generate_cmds += 'Generate("%s");' % (fontname + '.' + f)
816 if options.simplify:
817 simplify_cmd ='''SelectAll ();
819 AddExtrema();
820 Simplify ();
821 %(simplify_cmd)s
822 AutoHint ();''' % vars()
824 pe_script = ('''#!/usr/bin/env %(ff_command)s
825 Open ($1);
826 MergeKern($2);
827 %(simplify_cmd)s
828 %(generate_cmds)s
829 Quit (0);
830 ''' % vars())
832 open ('to-ttf.pe', 'w').write (pe_script)
833 if options.verbose:
834 print 'Fontforge script', pe_script
835 system ("%s -script to-ttf.pe %s %s" % (ff_command,
836 shell_escape_filename (raw_name), shell_escape_filename (options.tfm_file)))
837 else:
838 t1_path = ''
840 if ('pfa' in formats):
841 t1_path = assemble_font (fontname, 'pfa', 0)
843 if ('pfb' in formats):
844 t1_path = assemble_font (fontname, 'pfb', 0)
846 if (t1_path != '' and 'afm' in formats):
847 get_afm (t1_path, options.tfm_file, encoding, fontname + '.afm')
850 def getenv (var, default):
851 if os.environ.has_key (var):
852 return os.environ[var]
853 else:
854 return default
856 def gen_pixel_font (filename, metric, magnification):
858 Generate a GF file for FILENAME, such that `magnification'*mfscale
859 (default 1000 * 1.0) pixels fit on the designsize.
861 base_dpi = 1200
863 size = metric.design_size
865 size_points = size * 1/72.27 * base_dpi
867 mag = magnification / size_points
869 prod = mag * base_dpi
870 try:
871 open ('%s.%dgf' % (filename, prod))
872 except IOError:
873 os.environ['KPSE_DOT'] = '%s:' % origdir
875 os.environ['MFINPUTS'] = '%s:%s' % (origdir,
876 getenv ('MFINPUTS', ''))
877 os.environ['TFMFONTS'] = '%s:%s' % (origdir,
878 getenv ('TFMINPUTS', ''))
880 # FIXME: we should not change to another (tmp) dir?
881 # or else make all relavitive dirs in paths absolute.
882 def abs_dir (x, dir):
883 if x and os.path.abspath (x) != x:
884 return os.path.join (dir, x)
885 return x
887 def abs_path (path, dir):
888 # Python's ABSPATH means ABSDIR
889 dir = os.path.abspath (dir)
890 return string.join (map (lambda x: abs_dir (x, dir),
891 string.split (path,
892 os.pathsep)),
893 os.pathsep)
895 os.environ['MFINPUTS'] = abs_path (os.environ['MFINPUTS'],
896 origdir)
897 os.environ['TFMFONTS'] = abs_path (os.environ['TFMFONTS'],
898 origdir)
900 progress (_ ("Running Metafont..."))
902 cmdstr = r"mf '\mode:=lexmarks; mag:=%f; nonstopmode; input %s'" % (mag, filename)
903 if not options.verbose:
904 cmdstr = cmdstr + ' 1>/dev/null 2>/dev/null'
905 st = system (cmdstr, ignore_error = 1)
906 progress ('\n')
908 logfile = '%s.log' % filename
909 log = ''
910 prod = 0
911 if os.path.exists (logfile):
912 log = open (logfile).read ()
913 m = re.search ('Output written on %s.([0-9]+)gf' % re.escape (filename), log)
914 prod = int (m.group (1))
916 if st:
917 sys.stderr.write ('\n\nMetafont failed. Excerpt from the log file: \n\n*****')
918 m = re.search ("\n!", log)
919 start = m.start (0)
920 short_log = log[start:start+200]
921 sys.stderr.write (short_log)
922 sys.stderr.write ('\n*****\n')
923 if re.search ('Arithmetic overflow', log):
924 sys.stderr.write ("""
926 Apparently, some numbers overflowed. Try using --magnification with a
927 lower number. (Current magnification: %d)
928 """ % magnification)
930 if not options.keep_trying or prod == 0:
931 sys.exit (1)
932 else:
933 sys.stderr.write ('\n\nTrying to proceed despite of the Metafont errors...\n')
937 return "%s.%d" % (filename, prod)
939 def parse_command_line ():
940 p = optparse.OptionParser (version="""mftrace @VERSION@
942 This program is free software. It is covered by the GNU General Public
943 License and you are welcome to change it and/or distribute copies of it
944 under certain conditions. Invoke as `mftrace --warranty' for more
945 information.
947 Copyright (c) 2005--2006 by
948 Han-Wen Nienhuys <hanwen@xs4all.nl>
950 """)
951 p.usage = "mftrace [OPTION]... FILE..."
952 p.description = _ ("Generate Type1 or TrueType font from Metafont source.")
954 p.add_option ('-k', '--keep',
955 action="store_true",
956 dest="keep_temp_dir",
957 help=_ ("Keep all output in directory %s.dir") % program_name)
958 p.add_option ('','--magnification',
959 dest="magnification",
960 metavar="MAG",
961 default=1000.0,
962 type="float",
963 help=_("Set magnification for MF to MAG (default: 1000)"))
964 p.add_option ('-V', '--verbose',
965 action='store_true',
966 default=False,
967 help=_ ("Be verbose"))
968 p.add_option ('-f', '--formats',
969 action="append",
970 dest="formats",
971 default=[],
972 help=_("Which formats to generate (choices: AFM, PFA, PFB, TTF, SVG)"))
973 p.add_option ('', '--simplify',
974 action="store_true",
975 dest="simplify",
976 help=_ ("Simplify using fontforge"))
977 p.add_option ('', '--gffile',
978 dest="gffile",
979 help= _("Use gf FILE instead of running Metafont"))
980 p.add_option ('-I', '--include',
981 dest="include_dirs",
982 action="append",
983 default=[],
984 help=_("Add to path for searching files"))
985 p.add_option ('','--glyphs',
986 default=[],
987 action="append",
988 dest="glyphs",
989 metavar="LIST",
990 help= _('Process only these glyphs. LIST is comma separated'))
991 p.add_option ('', '--tfmfile',
992 metavar='FILE',
993 action='store',
994 dest='tfm_file')
995 p.add_option ('-e', '--encoding',
996 metavar="FILE",
997 action='store',
998 dest="encoding_file",
999 default="",
1000 help= _ ("Use encoding file FILE"))
1001 p.add_option ('','--keep-trying',
1002 dest='keep_trying',
1003 default=False,
1004 action="store_true",
1005 help= _ ("Don't stop if tracing fails"))
1006 p.add_option ('-w', '--warranty',
1007 action="store_true",
1008 help=_ ("show warranty and copyright"))
1009 p.add_option ('','--dos-kpath',
1010 dest="dos_kpath",
1011 help=_("try to use Miktex kpsewhich"))
1012 p.add_option ('', '--potrace',
1013 dest='potrace',
1014 help=_ ("Use potrace"))
1015 p.add_option ('', '--autotrace',
1016 dest='autotrace',
1017 help=_ ("Use autotrace"))
1018 p.add_option ('', '--no-afm',
1019 action='store_false',
1020 dest="read_afm",
1021 default=True,
1022 help=_("Don't read AFM file"))
1023 p.add_option ('','--noround',
1024 action="store_false",
1025 dest='round_to_int',
1026 default=True,
1027 help= ("Do not round coordinates of control points to integer values (use with --grid)"))
1028 p.add_option ('','--grid',
1029 metavar='SCALE',
1030 dest='grid_scale',
1031 type='float',
1032 default = 1.0,
1033 help=_ ("Set reciprocal grid size in em units"))
1034 p.add_option ('-D','--define',
1035 metavar="SYMBOL=VALUE",
1036 dest="defs",
1037 default=[],
1038 action='append',help=_("Set the font info SYMBOL to VALUE"))
1040 global options
1041 (options, files) = p.parse_args ()
1043 if not files:
1044 sys.stderr.write ('Need argument on command line \n')
1045 p.print_help ()
1046 sys.exit (2)
1048 if options.warranty :
1049 warranty ()
1050 sys.exit (0)
1052 options.font_info = {}
1053 for d in options.defs:
1054 kv = d.split('=')
1055 if len (kv) == 1:
1056 options.font_info[kv] = 'true'
1057 elif len (kv) > 1:
1058 options.font_info[kv[0]] = '='.join (kv[1:])
1060 def comma_sepped_to_list (x):
1061 fs = []
1062 for f in x:
1063 fs += f.lower ().split (',')
1064 return fs
1066 options.formats = comma_sepped_to_list (options.formats)
1068 new_glyphs = []
1069 for r in options.glyphs:
1070 new_glyphs += r.split (',')
1071 options.glyphs = new_glyphs
1073 glyph_range = []
1074 for r in options.glyphs:
1075 glyph_subrange = map (int, string.split (r, '-'))
1076 if len (glyph_subrange) == 2 and glyph_subrange[0] < glyph_subrange[1] + 1:
1077 glyph_range += range (glyph_subrange[0], glyph_subrange[1] + 1)
1078 else:
1079 glyph_range.append (glyph_subrange[0])
1081 options.glyphs = glyph_range
1083 options.trace_binary = ''
1084 if options.potrace:
1085 options.trace_binary = 'potrace'
1086 elif options.autotrace:
1087 options.trace_binary = 'autotrace'
1089 if options.formats == []:
1090 options.formats = ['pfa']
1094 global trace_command
1095 global path_to_type1_ops
1097 stat = os.system ('potrace --version > /dev/null 2>&1 ')
1098 if options.trace_binary != 'autotrace' and stat == 0:
1099 options.trace_binary = 'potrace'
1101 trace_command = potrace_command
1102 path_to_type1_ops = potrace_path_to_type1_ops
1104 stat = os.system ('autotrace --version > /dev/null 2>&1 ')
1105 if options.trace_binary != 'potrace' and stat == 0:
1106 options.trace_binary = 'autotrace'
1107 trace_command = autotrace_command
1108 path_to_type1_ops = autotrace_path_to_type1_ops
1110 if not options.trace_binary:
1111 error (_ ("No tracing program found.\nInstall potrace or autotrace."))
1113 return files
1116 def derive_font_name (family, fullname):
1117 fullname = re.sub (family, '', fullname)
1118 family = re.sub (' ', '', family)
1119 fullname = re.sub ('Oldstyle Figures', 'OsF', fullname)
1120 fullname = re.sub ('Small Caps', 'SC', fullname)
1121 fullname = re.sub ('[Mm]edium', '', fullname)
1122 fullname = re.sub ('[^A-Za-z0-9]', '', fullname)
1123 return '%s-%s' % (family, fullname)
1125 def cm_guess_font_info (filename, fontinfo):
1126 # urg.
1127 filename = re.sub ("cm(.*)tt", r"cmtt\1", filename)
1128 m = re.search ("([0-9]+)$", filename)
1129 design_size = ''
1130 if m:
1131 design_size = int (m.group (1))
1132 fontinfo['DesignSize'] = design_size
1134 prefixes = [("cmtt", "Computer Modern Typewriter"),
1135 ("cmvtt", "Computer Modern Variable Width Typewriter"),
1136 ("cmss", "Computer Modern Sans"),
1137 ("cm", "Computer Modern")]
1139 family = ''
1140 for (k, v) in prefixes:
1141 if re.search (k, filename):
1142 family = v
1143 if k == 'cmtt':
1144 fontinfo['isFixedPitch'] = 'true'
1145 filename = re.sub (k, '', filename)
1146 break
1148 # shapes
1149 prefixes = [("r", "Roman"),
1150 ("mi", "Math italic"),
1151 ("u", "Unslanted italic"),
1152 ("sl", "Oblique"),
1153 ("csc", "Small Caps"),
1154 ("ex", "Math extension"),
1155 ("ti", "Text italic"),
1156 ("i", "Italic")]
1157 shape = ''
1158 for (k, v) in prefixes:
1159 if re.search (k, filename):
1160 shape = v
1161 filename = re.sub (k, '', filename)
1163 prefixes = [("b", "Bold"),
1164 ("d", "Demi bold")]
1165 weight = 'Regular'
1166 for (k, v) in prefixes:
1167 if re.search (k, filename):
1168 weight = v
1169 filename = re.sub (k, '', filename)
1171 prefixes = [("c", "Condensed"),
1172 ("x", "Extended")]
1173 stretch = ''
1174 for (k, v) in prefixes:
1175 if re.search (k, filename):
1176 stretch = v
1177 filename = re.sub (k, '', filename)
1179 fontinfo['ItalicAngle'] = 0
1180 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1181 a = -14
1182 if re.search ("Sans", family):
1183 a = -12
1185 fontinfo ["ItalicAngle"] = a
1187 fontinfo['Weight'] = weight
1188 fontinfo['FamilyName'] = family
1189 full = '%s %s %s %s %dpt' \
1190 % (family, shape, weight, stretch, design_size)
1191 full = re.sub (" +", ' ', full)
1193 fontinfo['FullName'] = full
1194 fontinfo['FontName'] = derive_font_name (family, full)
1196 return fontinfo
1198 def ec_guess_font_info (filename, fontinfo):
1199 design_size = 12
1200 m = re.search ("([0-9]+)$", filename)
1201 if m:
1202 design_size = int (m.group (1))
1203 fontinfo['DesignSize'] = design_size
1205 prefixes = [("ecss", "European Computer Modern Sans"),
1206 ("ectt", "European Computer Modern Typewriter"),
1207 ("ec", "European Computer Modern")]
1209 family = ''
1210 for (k, v) in prefixes:
1211 if re.search (k, filename):
1212 if k == 'ectt':
1213 fontinfo['isFixedPitch'] = 'true'
1214 family = v
1215 filename = re.sub (k, '', filename)
1216 break
1218 # shapes
1219 prefixes = [("r", "Roman"),
1220 ("mi", "Math italic"),
1221 ("u", "Unslanted italic"),
1222 ("sl", "Oblique"),
1223 ("cc", "Small caps"),
1224 ("ex", "Math extension"),
1225 ("ti", "Italic"),
1226 ("i", "Italic")]
1228 shape = ''
1229 for (k, v) in prefixes:
1230 if re.search (k, filename):
1231 shape = v
1232 filename = re.sub (k, '', filename)
1234 prefixes = [("b", "Bold"),
1235 ("d", "Demi bold")]
1236 weight = 'Regular'
1237 for (k, v) in prefixes:
1238 if re.search (k, filename):
1239 weight = v
1240 filename = re.sub (k, '', filename)
1242 prefixes = [("c", "Condensed"),
1243 ("x", "Extended")]
1244 stretch = ''
1245 for (k, v) in prefixes:
1246 if re.search (k, filename):
1247 stretch = v
1248 filename = re.sub (k, '', filename)
1250 fontinfo['ItalicAngle'] = 0
1251 if re.search ('[Ii]talic', shape) or re.search ('[Oo]blique', shape):
1252 a = -14
1253 if re.search ("Sans", family):
1254 a = -12
1256 fontinfo ["ItalicAngle"] = a
1258 fontinfo['Weight'] = weight
1259 fontinfo['FamilyName'] = family
1260 full = '%s %s %s %s %dpt' \
1261 % (family, shape, weight, stretch, design_size)
1262 full = re.sub (" +", ' ', full)
1264 fontinfo['FontName'] = derive_font_name (family, full)
1265 fontinfo['FullName'] = full
1267 return fontinfo
1270 def guess_fontinfo (filename):
1271 fi = {
1272 'FontName': filename,
1273 'FamilyName': filename,
1274 'Weight': 'Regular',
1275 'ItalicAngle': 0,
1276 'DesignSize' : 12,
1277 'isFixedPitch' : 'false',
1278 'FullName': filename,
1281 if re.search ('^cm', filename):
1282 fi.update (cm_guess_font_info (filename, fi))
1283 elif re.search ("^ec", filename):
1284 fi.update (ec_guess_font_info (filename, fi))
1285 elif options.read_afm:
1286 global afmfile
1287 if not afmfile:
1288 afmfile = find_file (filename + '.afm')
1290 if afmfile:
1291 afmfile = os.path.abspath (afmfile)
1292 afm_struct = afm.read_afm_file (afmfile)
1293 fi.update (afm_struct.__dict__)
1294 return fi
1295 else:
1296 sys.stderr.write ("Warning: no extra font information for this font.\n"
1297 + "Consider writing a XX_guess_font_info() routine.\n")
1299 return fi
1301 def do_file (filename):
1302 encoding_file = options.encoding_file
1303 global include_dirs
1304 include_dirs = options.include_dirs
1305 include_dirs.append (origdir)
1307 basename = strip_extension (filename, '.mf')
1308 progress (_ ("Font `%s'..." % basename))
1309 progress ('\n')
1311 if not options.tfm_file:
1312 options.tfm_file = find_file (basename + '.tfm')
1314 if not options.tfm_file:
1315 options.tfm_file = popen ("mktextfm %s 2>/dev/null" % shell_escape_filename (basename)).read ()
1316 if options.tfm_file:
1317 options.tfm_file = options.tfm_file[:-1]
1319 if not options.tfm_file:
1320 error (_ ("Can not find a TFM file to match `%s'") % basename)
1322 options.tfm_file = os.path.abspath (options.tfm_file)
1323 metric = tfm.read_tfm_file (options.tfm_file)
1325 fontinfo = guess_fontinfo (basename)
1326 fontinfo.update (options.font_info)
1328 if encoding_file and not os.path.exists (encoding_file):
1329 encoding_file = find_file (encoding_file)
1332 if not encoding_file:
1333 codingfile = 'tex256.enc'
1334 if not coding_dict.has_key (metric.coding):
1335 sys.stderr.write ("Unknown encoding `%s'; assuming tex256.\n" % metric.coding)
1336 else:
1337 codingfile = coding_dict[metric.coding]
1339 encoding_file = find_file (codingfile)
1340 if not encoding_file:
1341 error (_ ("can't find file `%s'" % codingfile))
1343 (enc_name, encoding) = read_encoding (encoding_file)
1345 if not len (options.glyphs):
1346 options.glyphs = range (0, len (encoding))
1348 global temp_dir
1349 temp_dir = setup_temp ()
1351 if options.verbose:
1352 progress ('Temporary directory is `%s\' ' % temp_dir)
1354 os.chdir (temp_dir)
1356 if not options.gffile:
1357 # run mf
1358 base = gen_pixel_font (basename, metric, options.magnification)
1359 options.gffile = base + 'gf'
1360 else:
1361 options.gffile = find_file (options.gffile)
1363 # the heart of the program:
1364 trace_font (basename, options.gffile, metric, options.glyphs, encoding,
1365 options.magnification, fontinfo)
1367 make_outputs (basename, options.formats, encoding)
1368 for format in options.formats:
1369 shutil.copy2 (basename + '.' + format, origdir)
1371 os.chdir (origdir)
1372 cleanup_temp ()
1378 afmfile = ''
1379 backend_options = getenv ('MFTRACE_BACKEND_OPTIONS', '')
1380 def main ():
1381 files = parse_command_line ()
1382 identify (sys.stderr)
1384 for filename in files:
1385 do_file (filename)
1386 sys.exit (exit_value)
1388 if __name__ =='__main__':
1389 main()